Skip to content

EAST Built-in Methods

Built-in Methods For EAST¤

Module for retrieving and calculating data for DIII-D physics methods.

EastPhysicsMethods ¤

A class to retrieve and calculate physics-related data for EAST.

Source code in disruption_py/machine/east/physics.py
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
class EastPhysicsMethods:
    """
    A class to retrieve and calculate physics-related data for EAST.
    """

    @staticmethod
    @physics_method(columns=["time_until_disrupt"], tokamak=Tokamak.EAST)
    def get_time_until_disrupt(params: PhysicsMethodParams):
        """
        Calculate the time until disruption.

        Currently, the disruption time is queried from the `DISRUPTIONS` table
        in the SQL database of each machine. These disruption times were calculated
        using Robert Granetz's routine.

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the disruption information and times.

        Returns
        -------
        dict
            A dictionary with a single key `time_until_disrupt`.

        References
        -------
        - issues: #[223](https://github.com/MIT-PSFC/disruption-py/issues/223)
        """
        if params.disrupted:
            return {"time_until_disrupt": params.disruption_time - params.times}
        return {"time_until_disrupt": [np.nan]}

    @staticmethod
    @physics_method(
        columns=[
            "ip",
            "ip_prog",
            "ip_error",
            "ip_error_normalized",
            "dip_dt",
            "dipprog_dt",
        ],
        tokamak=Tokamak.EAST,
    )
    def get_ip_parameters(params: PhysicsMethodParams):
        """
        This routine retrieves the plasma current signals. It then calculates the error
        between the programmed and the actual plasma current, and the time derivatives
        of the actual and the programmed plasma current.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the following keys:

            - `ip`: Measured plasma current.
            - `ip_prog`: Programmed plasma current.
            - `ip_error`: Error between the actual and programmed plasma currents.
            - `ip_error_normalized`: `ip_error` normalized to `ip_prog`.
            - `dip_dt`: Time derivative of the measured plasma current.
            - `dipprog_dt`: Time derivative of the programmed plasma current.

        References
        -------
        - original source: [get_Ip_parameters.m](https://github.com/MIT-PSFC/disruption-py
        /blob/matlab/EAST/get_Ip_parameters.m)
        """
        ip = [np.nan]
        ip_prog = [np.nan]
        ip_error = [np.nan]
        ip_error_normalized = [np.nan]
        dip_dt = [np.nan]
        dipprog_dt = [np.nan]

        ip, ip_time = EastUtilMethods.retrieve_ip(params.mds_conn, params.shot_id)
        # Calculate dip_dt
        dip_dt = np.gradient(ip, ip_time)

        # Get the programmed plasma current.  For most times, the EAST plasma
        # control system (PCS) programming is signal "\lmtipref" in the pcs_east
        # tree.  However, there is an alternate control scheme, "isoflux" control,
        # which uses one of four possible signals (\ietip, \idtip, \istip, iutip).
        # Note that only one of these 4 signals is defined at any given time during
        # the shot.  The transition from normal control to isoflux control is
        # identified by several signals, one of which is called "\sytps1".  When
        # \sytps1 = 0, use \lmtipref for ip_prog.  When \sytps1 > 0, use whichever
        # isoflux or transition signal is defined.  Also, accordingly to Yuan
        # Qiping (EAST PCS expert), no shot is ever programmed to have a
        # back-transition, i.e. after a transition to isoflux control, no shot is
        # ever programmed to transition back to standard control.

        # Start with \lmtipref (standard Ip programming), which is defined for the
        # entire shot, and defines the timebase for the programmed Ip signal.
        ip_prog, ip_prog_time = params.mds_conn.get_data_with_dims(
            r"\lmtipref*1e6", tree_name="pcs_east"
        )  # [A], [s]

        # Now check to see if there is a transition to isoflux control
        sytps1, time_sytps1 = params.mds_conn.get_data_with_dims(
            r"\sytps1", tree_name="pcs_east"
        )
        (sytps1_indices,) = np.where(sytps1 > 0)

        # If PCS switches to isoflux control, then read in the 4 possible Ip target
        # signals, and interpolate each one onto the \lmtipref timebase.  There will
        # be many NaN values.

        if len(sytps1_indices) > 0 and time_sytps1[0] <= ip_prog_time[-1]:
            try:
                ietip, ietip_time = params.mds_conn.get_data_with_dims(
                    r"\ietip", tree_name="pcs_east"
                )
                ietip = interp1(ietip_time, ietip, ip_prog_time)
            except mdsExceptions.MdsException:
                ietip = np.full(len(ip_prog_time), np.nan)

            try:
                idtip, idtip_time = params.mds_conn.get_data_with_dims(
                    r"\idtip", tree_name="pcs_east"
                )
                idtip = interp1(idtip_time, idtip, ip_prog_time)
            except mdsExceptions.MdsException:
                idtip = np.full(len(ip_prog_time), np.nan)

            try:
                istip, istip_time = params.mds_conn.get_data_with_dims(
                    r"\istip", tree_name="pcs_east"
                )
                istip = interp1(istip_time, istip, ip_prog_time)
            except mdsExceptions.MdsException:
                istip = np.full(len(ip_prog_time), np.nan)

            try:
                iutip, iutip_time = params.mds_conn.get_data_with_dims(
                    r"\iutip", tree_name="pcs_east"
                )
                iutip = interp1(iutip_time, iutip, ip_prog_time)
            except mdsExceptions.MdsException:
                iutip = np.full(len(ip_prog_time), np.nan)

            # Okay, now loop through all times, selecting the alternate isoflux target
            # Ip value for each time that isoflux control is enabled (i.e. for each
            # time that \sytps1 ~= 0).  (Only one of the 4 possible isoflux target Ip
            # signals is defined at each time.)
            tstart_isoflux_control = time_sytps1[sytps1_indices[0]]
            (indices,) = np.where(ip_prog_time >= tstart_isoflux_control)

            for it in range(len(indices)):
                if not np.isnan(ietip[it]):
                    ip_prog[it] = ietip[it]
                elif not np.isnan(idtip[it]):
                    ip_prog[it] = idtip[it]
                elif not np.isnan(istip[it]):
                    ip_prog[it] = istip[it]
                elif not np.isnan(iutip[it]):
                    ip_prog[it] = iutip[it]

            # End of block for dealing with isoflux control

        # For shots before year 2014, the LMTIPREF timebase needs to be shifted
        # by 17.0 ms
        if params.shot_id < 44432:
            ip_prog_time -= 0.0170

        # Calculate dipprog_dt
        dipprog_dt = np.gradient(ip_prog, ip_prog_time)

        # Interpolate all retrieved signals to the requested timebase
        ip = interp1(ip_time, ip, params.times)
        dip_dt = interp1(ip_time, dip_dt, params.times)
        ip_prog = interp1(ip_prog_time, ip_prog, params.times)
        dipprog_dt = interp1(ip_prog_time, dipprog_dt, params.times)

        # Calculate ip_error and ip_error_normalized
        ip_error = ip - ip_prog
        ip_error_normalized = ip_error / ip_prog

        return {
            "ip": ip,
            "ip_prog": ip_prog,
            "ip_error": ip_error,
            "ip_error_normalized": ip_error_normalized,
            "dip_dt": dip_dt,
            "dipprog_dt": dipprog_dt,
        }

    @staticmethod
    @physics_method(columns=["v_loop"], tokamak=Tokamak.EAST)
    def get_v_loop(params: PhysicsMethodParams):
        r"""
        retrieve the loop voltage signal.

        By default, this routine gets the loop voltage from `\vp1_s` from the `east` tree.
        This signal is derived by taking the time derivative of a flux loop
        near the inboard midplane. If `\vp1_s` isn't available, the method will fall back
        to reading `\pcvloop` from the `pcs_east` tree instead.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the loop voltage (`v_loop`).

        References
        -------
        - original source: [get_v_loop.m](https://github.com/MIT-PSFC/disruption-py
        /blob/matlab/EAST/get_v_loop.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/
        411), #[451](https://github.com/MIT-PSFC/disruption-py/pull/451)
        """
        v_loop = [np.nan]

        # Get "\vp1_s" signal from the EAST tree.  (This signal is a sub-sampled
        # version of "vp1".)
        try:
            v_loop, v_loop_time = params.mds_conn.get_data_with_dims(
                r"\vp1_s", tree_name="east"
            )
        except mdsExceptions.MdsException:
            params.logger.verbose(
                r"v_loop: Failed to get \vp1_s data. Use \pcvloop from pcs_east instead."
            )
            v_loop, v_loop_time = params.mds_conn.get_data_with_dims(
                r"\pcvloop", tree_name="pcs_east"
            )  # [V]

        # Interpolate the signal onto the requested timebase
        v_loop = interp1(v_loop_time, v_loop, params.times)

        return {"v_loop": v_loop}

    @staticmethod
    @physics_method(
        columns=[
            "zcur",
            "z_prog",
            "z_error",
            "zcur_lmsz",
            "z_error_lmsz",
            "zcur_lmsz_normalized",
            "z_error_lmsz_normalized",
        ],
        tokamak=Tokamak.EAST,
    )
    def get_z_error(params: PhysicsMethodParams):
        r"""
        Retrieve the programmed and measured vertical positions of the plasma current
        centroid, then calculate the control error.

        This routine uses two methods to compute `z_error`. The first and original method
        gets the z-centroid from `\zcur` which is calculated by EFIT. The second method uses
        the z-centroid from the `\lmsz` signal which is calculated by the plasma control
        system (PCS). For the *lmsz*-derived signals, this routine also returns their normalized
        version by dividing them by the plasma minor radius.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the programmed vertical position (`z_prog`), the measured
            vertical positions (`zcur` and `zcur_lmsz)`, the errors (`z_error` and
            `z_error_lmsz`), and the normalized *lmsz*-derived signals (`zcur_lmsz_normalized`
            and `z_error_lmsz_normalized`).

        References
        -------
        - original source: [get_Z_error.m](https://github.com/MIT-PSFC/disruption-py/
        blob/matlab/EAST/get_Z_error.m)
        """
        # Read in the calculated zcur from EFIT
        zcur, zcur_time = params.mds_conn.get_data_with_dims(
            r"\efit_aeqdsk:zcur", tree_name="_efit_tree"
        )  # [A], [s]
        # Deal with rare bug
        zcur_time, unique_indices = np.unique(zcur_time, return_index=True)
        zcur = zcur[unique_indices]

        # Read in aminor from EFIT
        # TODO: use \aminor or \aout? -- MATLAB: \aminor
        aminor = params.mds_conn.get_data(r"\aminor", tree_name="_efit_tree")  # [m]
        aminor = aminor[unique_indices]

        # Next, get the programmed/requested/target Z from PCS, and the
        # calculated Z-centroid from PCS
        z_prog, z_prog_time = params.mds_conn.get_data_with_dims(
            r"\lmtzref/100", tree_name="pcs_east"
        )  # [m], [s] (Node says 'm' but it's wrong)
        zcur_lmsz, lmsz_time = params.mds_conn.get_data_with_dims(
            r"\lmsz", tree_name="pcs_east"
        )  # [m], [s]

        # Interpolate all retrieved signals to the requested timebase
        zcur = interp1(zcur_time, zcur, params.times)
        aminor = interp1(zcur_time, aminor, params.times)
        z_prog = interp1(z_prog_time, z_prog, params.times)
        zcur_lmsz = interp1(lmsz_time, zcur_lmsz, params.times)

        # Calculate zcur_lmsz_normalized
        zcur_lmsz_normalized = zcur_lmsz / aminor

        # Calculate both versions of z_error and z_error_lmsz_normalized
        z_error = zcur - z_prog
        z_error_lmsz = zcur_lmsz - z_prog
        z_error_lmsz_normalized = z_error_lmsz / aminor

        return {
            "zcur": zcur,
            "z_prog": z_prog,
            "z_error": z_error,
            "zcur_lmsz": zcur_lmsz,
            "z_error_lmsz": z_error_lmsz,
            "zcur_lmsz_normalized": zcur_lmsz_normalized,
            "z_error_lmsz_normalized": z_error_lmsz_normalized,
        }

    @staticmethod
    @physics_method(
        columns=["n_e", "greenwald_fraction", "dn_dt"], tokamak=Tokamak.EAST
    )
    def get_density_parameters(params: PhysicsMethodParams):
        r"""
        Calculate the electron density, its time derivative, and the Greenwald fraction.

        The Greenwald fraction is the ratio of the measured electron density $n_e$ and
        the Greenwald density limit $n_G$ which is defined as [^1]:

        $$
        n_G = \frac{I_p}{\pi a^2}
        $$

        where $n_G$ is given in $10^{20} m^{-3}$ and $I_p$ is in MA.

        The line-averaged electron density is obtained from the HCN vertical chord.
        This signal is also used by the plasma control system (PCS) for feedback control of
        the density.

        [^1]: https://wiki.fusion.ciemat.es/wiki/Greenwald_limit

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing electron density (`n_e`), its time derivative (`dn_dt`),
            and the Greenwald fraction (`greenwald_fraction`).

        References
        -------
        - original source: [get_density_parameters.m](https://github.com/MIT-PSFC/disruption-py
        /blob/matlab/EAST/get_density_parameters.m)
        """
        ne = [np.nan]
        greenwald_fraction = [np.nan]
        dn_dt = [np.nan]

        # Get the density and calculate dn_dt
        ne, netime = params.mds_conn.get_data_with_dims(
            r"\dfsdev*1e19", tree_name="pcs_east"
        )  # [m^-3], [s]
        dn_dt = np.gradient(ne, netime)  # [m^-3/s]

        # Interpolate ne and dn_dt to the requested timebase
        ne = interp1(netime, ne, params.times)
        dn_dt = interp1(netime, dn_dt, params.times)

        # Calculate Greenwald density
        # TODO: use \aminor or \aout? -- MATLAB: \aout
        aminor, efittime = params.mds_conn.get_data_with_dims(
            r"\efit_aeqdsk:aout", tree_name="_efit_tree"
        )  # [m], [s]
        aminor = interp1(efittime, aminor, params.times)
        ip = EastPhysicsMethods.get_ip_parameters(params)["ip"]  # [A]
        n_g = 1e20 * (ip / 1e6) / (np.pi * aminor**2)  # [m^-3]
        greenwald_fraction = ne / n_g

        return {"n_e": ne, "greenwald_fraction": greenwald_fraction, "dn_dt": dn_dt}

    @staticmethod
    def _get_raw_axuv_data(params: PhysicsMethodParams):
        """
        Get the raw (uncalibrated) data from the AXUV arrays for calculating
        the total radiated power and prad_peaking. The AXUV diagnostic contains
        4 arrays of 16 channels each.

        Note that the original implementations in Prad_bulk_xuv2014_2016.m and
        get_prad_peaking.m are slightly different in where the calibration factors
        are applied. This difference isn't explained in the scripts so I keep
        these functions different.
        """
        # Get XUV data
        (xuvtime,) = params.mds_conn.get_dims(r"\pxuv1", tree_name="east_1")  # [s]
        # There are 64 AXUV chords, arranged in 4 arrays of 16 channels each
        xuv = np.full((len(xuvtime), 64), np.nan)

        dt = xuvtime[1] - xuvtime[0]
        smoothing_time = 1e-3
        smoothing_window = int(max([round(smoothing_time / dt, 1)]))

        for iarray in range(4):
            for ichan in range(16):
                ichord = 16 * iarray + ichan
                signal = params.mds_conn.get_data(
                    r"\pxuv" + str(ichord + 1), tree_name="east_1"
                )
                # Subtract baseline
                signal = signal - np.mean(signal[:100])
                # TODO: change this to causal smoothing
                xuv[:, ichord] = (
                    matlab_smooth(signal, smoothing_window) * 1e3
                )  # [kW] -> [W]
        return xuv, xuvtime

    @staticmethod
    @physics_method(columns=["p_rad"], tokamak=Tokamak.EAST)
    def get_radiated_power(params: PhysicsMethodParams):
        """
        Calculate total radiated power in bulk plasma measured by
        the AXUV arrays.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the total radiated power (`p_rad`).

        References
        -------
        - original source: Prad_bulk_xuv2014_2016.m (currently not available in the repository)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
        """
        # Get the raw AXUV data
        try:
            xuv, xuvtime = EastPhysicsMethods._get_raw_axuv_data(params)  # [W], [s]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get raw AXUS data")
            return {"p_rad": [np.nan]}

        # Get calibration factors
        calib_factors = EastUtilMethods.get_axuv_calib_factors()
        # Apply calibration factors Fac1 to Fac4
        for i in range(64):
            xuv[:, i] *= (
                calib_factors["Fac1"][i % 16]
                * calib_factors["Fac2"][i // 16][i % 16]
                * calib_factors["Fac3"][i // 16]
                * calib_factors["Fac4"]
            )
        # Correction for bad channels (from Duan Yanmin's program)
        # TODO: why not [35] = ([34]+[36])/2 when [35] is the bad channel?
        xuv[:, 11] = 0.5 * (xuv[:, 10] + xuv[:, 12])
        xuv[:, 35] = 0.5 * (xuv[:, 34] + xuv[:, 35])
        # Apply geometric calibrations and Fac5
        for i in range(64):
            xuv[:, i] *= (
                2
                * np.pi
                * calib_factors["Maj_R"]
                * calib_factors["Del_r"][i]
                * calib_factors["Fac5"]
            )

        # Subtract divertor measurements and overlapping measurments
        core_indices = (
            list(range(7, 15))
            + list(range(20, 32))
            + list(range(32, 48))
            + list(range(53, 59))
        )
        p_rad = np.nansum(xuv[:, core_indices], axis=1)
        # Interpolate to requested time base
        p_rad = interp1(xuvtime, p_rad, params.times, kind="linear", fill_value=0)
        return {"p_rad": p_rad}

    @staticmethod
    @physics_method(
        columns=[
            "p_ecrh",
            "p_lh",
            "p_icrf",
            "p_nbi",
            "rad_input_frac",
            "rad_loss_frac",
            "p_input",
        ],
        tokamak=Tokamak.EAST,
    )
    def get_power(params: PhysicsMethodParams):
        r"""
        This routine gets the auxiliary heating powers: electron cyclotron
        resonance heating (`p_ecrh`), neutral beam injection system (`p_nbi`)
        ion cyclotron (`p_icrf`), and lower hybrid (`p_lh`). If any of the auxiliary
        heating powers were not available during a shot, then it returns an array
        of zeros for them.

        This routine also calculates the total input power, the radiated input
        fraction, and the radiated loss function. These parameters are defined as:
        $$
        p_\text{input} = p_\text{ohmic} + p_\text{lh} + p_\text{icrf} + p_\text{ecrh}
        + p_\text{nbi}
        $$
        $$
        f_\text{rad input} = \frac{p_\text{rad}}{p_\text{input}}
        $$
        $$
        f_\text{rad loss} = \frac{p_\text{rad}}{p_\text{rad} + dW_\text{mhd}/dt}
        $$

        Note that currently we assume that the MDSplus trees always have the corresponding
        tree nodes for each of the powers, even if a heating source/diagnostic was turned
        off during that shot. If the routine fails to find a tree or a tree node,
        then it assumes that tree is broken and returns the corresponding power as an array
        of `nans`. The routine will also skip calculating `p_input` or the radiated fractions if
        any of its inputs is an array of `nans`.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the auxiliary heating powers (`p_ecrh`, `p_lh`, `p_icrf`,
            `p_nbi`), the total input power (`p_input`), and the radiated fractions
            (`rad_input_frac`, `rad_loss_frac`).

        References
        -------
        - original source: [get_power.m](https://github.com/MIT-PSFC/disruption-py/blob/
        matlab/EAST/get_power.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411), #[451](https:
        //github.com/MIT-PSFC/disruption-py/pull/451)
        """
        p_ecrh = [np.nan]
        p_lh = [np.nan]
        p_icrf = [np.nan]
        p_nbi = [np.nan]
        rad_input_frac = [np.nan]
        rad_loss_frac = [np.nan]
        p_input = [np.nan]

        def get_heating_power(nodes, tree):
            """
            Helper function to pack p_lh, p_icrf, and p_nbi computations
            """
            heating_power = np.zeros(params.times.shape)
            for node in nodes:
                power_node, time_node = params.mds_conn.get_data_with_dims(
                    node, tree_name=tree
                )
                heating_power += interp1(
                    time_node,
                    power_node,
                    params.times,
                    kind="linear",
                    fill_value=0,
                )
            return heating_power

        # Get lower hybrid power

        # Note: the timebase for the LH power signal does not extend over the full
        # time span of the discharge.  Therefore, when interpolating the LH power
        # signal onto the "timebase" array, the LH signal has to be extrapolated
        # with zero values.  This is an option in the 'interp1' routine.  If the
        # extrapolation is not done, then the 'interp1' routine will assign NaN
        # (Not-a-Number) values for times outside the LH timebase, and the NaN's
        # will propagate into p_input, rad_input_frac, and rad_loss_frac, which is
        # not desirable.

        # TODO: check if the extrapolation method actually works
        lh_nodes = [
            r"\plhi1*1e3",  # LHW 2.45 GHz data
            r"\plhi2*1e3",  # LHW 4.66 GHz data
        ]
        try:
            p_lh = get_heating_power(lh_nodes, "east_1")  # [W]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get LH heating power")
            p_lh = [np.nan]

        # Get ECRH power
        try:
            p_ecrh, ecrh_time = params.mds_conn.get_data_with_dims(
                r"\pecrh1i*1e3", tree_name="analysis"
            )  # [W], [s]
            (baseline_indices,) = np.where(ecrh_time < 0)
            if len(baseline_indices) > 0:
                p_ecrh_baseline = np.mean(p_ecrh[baseline_indices])
                p_ecrh -= p_ecrh_baseline
            p_ecrh = interp1(
                ecrh_time, p_ecrh, params.times, kind="linear", fill_value=0
            )
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get ECRH heating power")
            p_ecrh = [np.nan]

        # Get ICRF power
        # p_ICRF = (p_icrfii - p_icrfir) + (p_icrfbi - p_icrfbr)
        icrf_nodes = [
            r"\picrfii*1e3",
            r"\picrfir*-1e3",
            r"\picrfbi*1e3",
            r"\picrfbr*-1e3",
        ]
        try:
            p_icrf = get_heating_power(icrf_nodes, "icrf_east")  # [W]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get ICRF heating power")
            p_icrf = [np.nan]

        # Get NBI power
        nbi_nodes = [
            r"\pnbi1lsource*1e3",
            r"\pnbi1rsource*1e3",
            r"\pnbi2lsource*1e3",
            r"\pnbi2rsource*1e3",
        ]
        try:
            p_nbi = get_heating_power(nbi_nodes, "nbi_east")  # [W]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get NBI heating power")
            p_nbi = [np.nan]

        # Get ohmic power
        p_ohm = EastPhysicsMethods.get_p_ohm(params)["p_oh"]  # [W]

        # Get radiated power
        p_rad = EastPhysicsMethods.get_radiated_power(params)["p_rad"]  # [W]

        # Calculate p_input
        if ~(
            np.isnan(p_ohm).all()
            or np.isnan(p_lh).all()
            or np.isnan(p_icrf).all()
            or np.isnan(p_ecrh).all()
            or np.isnan(p_nbi).all()
        ):
            p_input = p_ohm + p_lh + p_icrf + p_ecrh + p_nbi
        else:
            p_input = [np.nan]

        # Get Wmhd, calculate dWmhd_dt, and calculate p_loss
        try:
            wmhd, efittime = params.mds_conn.get_data_with_dims(
                r"\efit_aeqdsk:wmhd", tree_name="_efit_tree"
            )  # [W], [s]
            dwmhd_dt = np.gradient(wmhd, efittime)
            dwmhd_dt = interp1(efittime, dwmhd_dt, params.times)
            if not np.isnan(p_input).all():
                p_loss = p_input - dwmhd_dt
            else:
                p_loss = [np.nan]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get proper signals to compute p_loss")
            p_loss = [np.nan]

        rad_input_frac = np.full(len(params.times), np.nan)
        rad_loss_frac = np.full(len(params.times), np.nan)
        if ~(np.isnan(p_rad).all() or np.isnan(p_input).all()):
            (valid_indices,) = np.where((p_input != 0) & ~np.isnan(p_input))
            rad_input_frac[valid_indices] = (
                p_rad[valid_indices] / p_input[valid_indices]
            )
        if ~(np.isnan(p_rad).all() or np.isnan(p_loss).all()):
            (valid_indices,) = np.where((p_loss != 0) & ~np.isnan(p_loss))
            rad_loss_frac[valid_indices] = p_rad[valid_indices] / p_loss[valid_indices]

        output = {
            "p_ecrh": p_ecrh,
            "p_lh": p_lh,
            "p_icrf": p_icrf,
            "p_nbi": p_nbi,
            "rad_input_frac": rad_input_frac,
            "rad_loss_frac": rad_loss_frac,
            "p_input": p_input,
        }
        return output

    @staticmethod
    @physics_method(columns=["p_oh"], tokamak=Tokamak.EAST)
    def get_p_ohm(params: PhysicsMethodParams):
        r"""
        Calculate the ohmic heating power from the loop voltage, inductive voltage, and
        plasma current.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the ohmic heating power (`p_oh`).

        References
        -------
        - original source: [get_P_ohm.m](https://github.com/MIT-PSFC/disruption-py
        /blob/matlab/EAST/get_P_ohm.m)
        """
        # Get raw signals
        try:
            vloop, vloop_time = params.mds_conn.get_data_with_dims(
                r"\pcvloop", tree_name="pcs_east"
            )  # [V]
            li, li_time = params.mds_conn.get_data_with_dims(
                r"\efit_aeqdsk:li", tree_name="_efit_tree"
            )  # [H]
            # Fetch raw ip signal to calculate dip_dt and apply smoothing
            ip, ip_time = params.mds_conn.get_data_with_dims(
                r"\pcrl01", tree_name="pcs_east"
            )  # [A]
        except mdsExceptions.MdsException:
            params.logger.warning("Failed to get necessary signals to compute p_ohm")
            return {"p_oh": [np.nan]}
        sign_ip = np.sign(sum(ip))
        dipdt = np.gradient(ip, ip_time)
        # TODO: switch to causal boxcar smoothing
        dipdt_smoothed = matlab_smooth(dipdt, 11)  # Use 11-point boxcar smoothing

        # Interpolate fetched signals to the requested timebase
        vloop = interp1(vloop_time, vloop, params.times, kind="linear", bounds_error=0)
        li = interp1(li_time, li, params.times, kind="linear", bounds_error=0)
        ip = interp1(ip_time, ip, params.times, kind="linear", bounds_error=0)
        dipdt_smoothed = interp1(
            ip_time, dipdt_smoothed, params.times, kind="linear", bounds_error=0
        )

        # Calculate p_ohm
        # TODO: replace 1.85 with fetched r0 signal
        inductance = 4e-7 * np.pi * 1.85 * li / 2  # For EAST use R0 = 1.85 m
        v_inductive = -inductance * dipdt_smoothed
        v_resistive = vloop - v_inductive
        (v_resistive_indices,) = np.where(v_resistive * sign_ip < 0)
        v_resistive[v_resistive_indices] = 0
        p_ohm = ip * v_resistive

        return {"p_oh": p_ohm}

    @staticmethod
    @physics_method(
        columns=[
            "n_equal_1_mode",
            "n_equal_1_phase",
            "n_equal_1_normalized",
            "rmp_n_equal_1",
            "rmp_n_equal_1_phase",
        ],
        tokamak=Tokamak.EAST,
    )
    def get_n_equal_1_data(params: PhysicsMethodParams):
        """
        Compute the amplitude and phase of the $n$=1 Fourier component of the net
        saddle signals (total saddle signals minus the calculated pickup from
        the RMP coils).

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the following keys:

            - `n_equal_1_mode`: Amplitude of $n$=1 Fourier component of saddle signals
            after subtracting the calculated pickup from the RMP coil currents.
            - `n_equal_1_phase`: Toroidal phase of the $n$=1 mode.
            - `n_equal_1_normalized`: `n_equal_1_mode` normalized to the toroidal magnetic
            field (`btor`).
            - `rmp_n_equal_1`: Amplitude of the $n$=1 Fourier component of the calculated
            pickup of the RMP coils on the saddle signals.
            - `rmp_n_equal_1_phase`: toroidal phase of the $n$=1 rmp pickup.

        References
        -------
        - original source: [get_n_equal_1_data.m](https://github.com/MIT-PSFC/disruption-
        py/blob/matlab/EAST/get_n_equal_1_data.m), [get_rmp_and_saddle_signals.m](https://
        github.com/MIT-PSFC/disruption-py/blob/matlab/EAST/get_rmp_and_saddle_signals.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
        """
        n_equal_1_mode = [np.nan]
        n_equal_1_phase = [np.nan]
        n_equal_1_normalized = [np.nan]
        rmp_n_equal_1 = [np.nan]
        rmp_n_equal_1_phase = [np.nan]

        # Get the rmp coil currents
        # Translated from get_rmp_and_saddle_signals.m
        (rmptime,) = params.mds_conn.get_dims(r"\irmpu1", tree_name="east")
        rmp = np.full((len(rmptime), 16), np.nan)
        for i in range(8):
            # Get irmpu1 to irmpu8
            signal = params.mds_conn.get_data(rf"\irmpu{i+1}", tree_name="east")
            if len(signal) == len(rmptime):
                rmp[:, i] = signal
            # Get irmpl1 to irmpl8
            signal = params.mds_conn.get_data(rf"\irmpl{i+1}", tree_name="east")
            if len(signal) == len(rmptime):
                rmp[:, i + 8] = signal
        # Get saddle coil signals
        (saddletime,) = params.mds_conn.get_dims(r"\sad_pa", tree_name="east")
        saddle = np.full((len(saddletime), 8), np.nan)
        saddle_nodes = [
            r"\sad_pa",
            r"\sad_bc",
            r"\sad_de",
            r"\sad_fg",
            r"\sad_hi",
            r"\sad_jk",
            r"\sad_lm",
            # r"\sad_no",   # not operational in 2015
        ]
        for i, node in enumerate(saddle_nodes[:7]):
            try:
                saddle[:, i] = params.mds_conn.get_data(node, tree_name="east")
            except mdsExceptions.MdsException:
                saddle[:, i] = 0
        sad_lo = params.mds_conn.get_data(r"\sad_lo", tree_name="east")
        sad_lm = params.mds_conn.get_data(r"\sad_lm", tree_name="east")
        saddle[:, 7] = sad_lo - sad_lm

        # Calculate RMP n=1 Fourier component amplitude and phase (on the timebase
        # of the saddle signals)
        coeff_matrix = EastUtilMethods.load_rmp_saddle_coeff_matrix()
        rmp_pickup = np.transpose(np.matmul(coeff_matrix, np.transpose(rmp)))
        # Interpolate rmp_pickup onto the saddle time timebase
        rmp_pickup = interp1(rmptime, rmp_pickup, saddletime, axis=0)

        # Calculate fast Fourier transforms of the RMP-induced signals, and get
        # mode amplitudes and phases
        # Take FFT along 2nd dimension (phi)
        rmp_fft_output = scipy.fft.fft(rmp_pickup, axis=1)
        amplitude = abs(rmp_fft_output) / len(rmp_fft_output[0])
        amplitude[:, 1:] *= 2  # TODO: Why?
        phase = np.arctan2(np.imag(rmp_fft_output), np.real(rmp_fft_output))
        # Only want n=1 Fourier component
        rmp_n_equal_1 = amplitude[:, 1]
        rmp_n_equal_1_phase = phase[:, 1]
        # Interpolate onto the requested timebase
        # TODO: figure out how to interpolate phase
        rmp_n_equal_1 = interp1(saddletime, rmp_n_equal_1, params.times)
        rmp_n_equal_1_phase = interp1(saddletime, rmp_n_equal_1_phase, params.times)

        # The saddle signals can include direct pickup from the RMP coils, when the
        # RMP coils are active.  This direct pickup must be subtracted from the
        # saddle signals to leave just the plasma response and baseline drifts.
        saddle -= rmp_pickup
        # Calculate fast Fourier transforms and get mode amplitudes and phases
        # Take FFT along 2nd dimension (phi)
        saddle_fft_output = scipy.fft.fft(saddle, axis=1)
        amplitude = abs(saddle_fft_output) / len(saddle_fft_output[0])
        amplitude[:, 1:] *= 2  # TODO: Why?
        phase = np.arctan2(np.imag(saddle_fft_output), np.real(saddle_fft_output))
        # Only want n=1 Fourier component
        n_equal_1_mode = amplitude[:, 1]
        n_equal_1_phase = phase[:, 1]
        # Interpolate onto the requested timebase
        n_equal_1_mode = interp1(saddletime, n_equal_1_mode, params.times)
        n_equal_1_phase = interp1(saddletime, n_equal_1_phase, params.times)

        # Next, get the toroidal magnetic field and compute n_equal_1_normalized
        btor = EastPhysicsMethods.get_btor(params)["btor"]
        n_equal_1_normalized = n_equal_1_mode / abs(btor)

        return {
            "n_equal_1_mode": n_equal_1_mode,
            "n_equal_1_phase": n_equal_1_phase,
            "n_equal_1_normalized": n_equal_1_normalized,
            "rmp_n_equal_1": rmp_n_equal_1,
            "rmp_n_equal_1_phase": rmp_n_equal_1_phase,
        }

    @staticmethod
    @physics_method(columns=["btor"], tokamak=Tokamak.EAST)
    def get_btor(params: PhysicsMethodParams):
        r"""
        Calculate the toroidal magnetic field.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the toroidal magnetic field (`btor`).

        References
        -------
        - original sources: [get_n_equal_1_data.m](https://github.com/MIT-PSFC/disr
        uption-py/blob/matlab/EAST/get_n_equal_1_data.m), [get_mirnov_std.m](https:
        //github.com/MIT-PSFC/disruption-py/blob/matlab/EAST/get_mirnov_std.m),
        [get_n1rms_n2rms.m](https://github.com/MIT-PSFC/disruption-py/blob/matlab/
        EAST/get_n1rms_n2rms.m)
        """
        if 60000 <= params.shot_id <= 65165:
            tree = "pcs_east"
        else:
            tree = "eng_tree"
        itf, btor_time = params.mds_conn.get_data_with_dims(r"\it", tree_name=tree)
        btor = (
            (4 * np.pi * 1e-7) * itf * (16 * 130) / (2 * np.pi * 1.8)
        )  # about 4,327 amps/tesla
        if params.shot_id < 60000:
            # Btor is constant in time (superconducting magnet).
            # Construct 2-point signal from scalar value
            btor = np.mean(btor)
            btor = [btor, btor]
            btor_time = [0, 1000]

        # Interpolate onto the requested timebase
        btor = interp1(btor_time, btor, params.times)

        return {"btor": btor}

    @staticmethod
    @physics_method(columns=["kappa_area"], tokamak=Tokamak.EAST)
    def get_kappa_area(params: PhysicsMethodParams):
        r"""
        Calculate the plasma's ellipticity (*kappa*, also known as
        the elongation) using its area and minor radius. It is defined as:

        $$
        \kappa_{area} = \frac{A}{\pi a^2}
        $$

        where $A$ is the plasma cross-sectional area and $a$ is the minor radius.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the `kappa_area` signal.

        References
        -------
        - original source: [get_kappa_area.m](https://github.com/MIT-PSFC/disrup
        tion-py/blob/matlab/EAST/get_kappa_area.m)
        """
        # Get area and aminor from EastEfitMethods
        efit_params = EastEfitMethods.get_efit_parameters(params=params)
        area = efit_params["area"]
        aminor = efit_params["aminor"]
        # Compute kappa_area
        with np.errstate(divide="ignore", invalid="ignore"):
            kappa_area = area / (np.pi * aminor**2)

        return {"kappa_area": kappa_area}

    @staticmethod
    @physics_method(columns=["pkappa_area"], tokamak=Tokamak.EAST)
    def get_pkappa_area(params: PhysicsMethodParams):
        r"""
        Calculate the plasma's ellipticity (*kappa*) using the area and minor
        radius data from the P-EFIT tree. `kappa_area` is defined as:

        $$
        \kappa_{area} = \frac{A}{\pi a^2}
        $$

        where $A$ is the plasma cross-sectional area and $a$ is the minor radius.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the `pkappa_area` signal.

        References
        -------
        - original source: [get_PEFIT_parameters.m](https://github.com/MIT-PSFC/
        disruption-py/blob/matlab/EAST/get_PEFIT_parameters.m)
        """
        # Get area and aminor from EastEfitMethods
        pefit_params = EastEfitMethods.get_pefit_parameters(params=params)
        area = pefit_params["parea"]
        aminor = pefit_params["paminor"]
        # Compute kappa_area
        with np.errstate(divide="ignore", invalid="ignore"):
            kappa_area = area / (np.pi * aminor**2)

        return {"pkappa_area": kappa_area}

    @staticmethod
    @physics_method(
        columns=["ip_error_rt", "q95_rt", "beta_p_rt", "li_rt", "wmhd_rt"],
        tokamak=Tokamak.EAST,
    )
    def get_pcs_parameters(params: PhysicsMethodParams):
        """
        Retrieve a subset of the real-time diagnostic signals that are used in the EAST
        plasma control system (PCS). These signals are either calculated by RT-EFIT (pre-2018)
        or P-EFIT (since 2018).

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the MDS connection and shot information.

        Returns
        -------
        dict
            A dictionary containing `ip_error_rt`, `q95_rt`, `beta_p_rt`, `li_rt`, and `wmhd_rt`.

        References
        -------
        - original source: [get_pcs.m](https://github.com/MIT-PSFC/disruption-py/
        blob/matlab/EAST/get_pcs.m)
        """
        output = {}

        # Get ip_error_rt, beta_p_rt, li_rt, and wmhd_rt
        signals = {
            "ip_error_rt": r"\lmeip",
            "beta_p_rt": r"\pfsbetap",
            "li_rt": r"\pfsli",
            "wmhd_rt": r"\pfswmhd",
        }
        for name, node in signals.items():
            try:
                signal, timearray = params.mds_conn.get_data_with_dims(
                    node, tree_name="pcs_east"
                )
                signal = interp1(
                    timearray, signal, params.times, kind="linear", bounds_error=0
                )
                output[name] = signal
            except mdsExceptions.MdsException:
                output[name] = [np.nan]

        # Get q95_rt
        try:
            q95_rt, q95_rt_time = params.mds_conn.get_data_with_dims(
                r"\q95", tree_name="pefitrt_east"
            )
            # Deal with bug
            q95_rt_time, unique_indices = np.unique(q95_rt_time, return_index=True)
            q95_rt = q95_rt[unique_indices]
            q95_rt = interp1(
                q95_rt_time, q95_rt, params.times, kind="linear", bounds_error=0
            )
            output["q95_rt"] = q95_rt
        except mdsExceptions.MdsException:
            output["q95_rt"] = [np.nan]

        return output

    @staticmethod
    @physics_method(columns=["p_rad_rt", "p_lh_rt", "p_nbi_rt"], tokamak=Tokamak.EAST)
    def get_pcs_power(params: PhysicsMethodParams):
        """
        Retrieve and calculate real-time power signals that are available
        in the EAST plasma control system (PCS).

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the MDS connection and shot information.

        Returns
        -------
        dict
            A dictionary containing the following keys:

            - `p_rad_rt`: radiated power.
            - `p_lh_RT`: lower hybrid heating power.
            - `p_nbi_RT`: neutral beam injection heating power.

        References
        -------
        - original source: [get_pcs.m](https://github.com/MIT-PSFC/disruption-py
        /blob/matlab/EAST/get_pcs.m)
        """
        # Get p_rad_rt
        try:
            p_rad_rt, timearray = params.mds_conn.get_data_with_dims(
                r"\pcprad", tree_name="pcs_east"
            )
            p_rad_rt = interp1(
                timearray, p_rad_rt, params.times, kind="linear", bounds_error=0
            )
        except mdsExceptions.MdsException:
            p_rad_rt = [np.nan]

        # TODO: Verify the outputs, then modify get_heating_power() to make it compatible with this
        # Get p_nbi_rt
        nbi_nodes = {
            "i_nbil_rt": r"\pcnbi1li",
            "v_nbil_rt": r"\pcnbi1lv",
            "i_nbir_rt": r"\pcnbi1ri",
            "v_nbir_rt": r"\pcnbi1rv",
        }
        try:
            nbi_signals = {}
            for name, node in nbi_nodes.items():
                signal, timearray = params.mds_conn.get_data_with_dims(
                    node, tree_name="pefitrt_east"
                )
                signal = interp1(
                    timearray, signal, params.times, kind="linear", bounds_error=0
                )
                nbi_signals[name] = signal
            p_nbi_rt = (
                nbi_signals["i_nbil_rt"] * nbi_signals["v_nbil_rt"]
                + nbi_signals["i_nbir_rt"] * nbi_signals["v_nbir_rt"]
            )
        except mdsExceptions.MdsException:
            p_nbi_rt = [np.nan]

        # Get p_lh_rt
        lh_nodes = {
            "p_lh_46_inj_rt": r"\pcplhi",
            "p_lh_46_ref_rt": r"\pcplhr",
            "p_lh_245_inj_rt": r"\pcplhi2",
            "p_lh_245_ref_rt": r"\pcplhr2",
        }
        try:
            lh_signals = {}
            for name, node in lh_nodes.items():
                signal, timearray = params.mds_conn.get_data_with_dims(
                    node, tree_name="pefitrt_east"
                )
                signal = interp1(
                    timearray, signal, params.times, kind="linear", bounds_error=0
                )
                lh_signals[name] = signal
            p_lh_rt = (
                lh_signals["p_lh_46_inj_rt"]
                - lh_signals["p_lh_46_ref_rt"]
                + lh_signals["p_lh_245_inj_rt"]
                - lh_signals["p_lh_245_ref_rt"]
            )
        except mdsExceptions.MdsException:
            p_lh_rt = [np.nan]

        # Q.P. Yuan:
        # There is no signals from ICRF or ECRH connected to PCS. And I checked the
        # signals for NBI, there is no effective value, just noise. We will make
        # the NBI signals available in this EAST campaign. The PLHI2 and PLHR2 for
        # 2.45G LHW system have signals but are not calibrated yet.

        # Get p_ohm
        # "Note, I'm not bothering to calculate a real time version" -- Whoever who wrote this
        # p_ohm = EastPhysicsMethods.get_p_ohm(params)['p_ohm']

        # Compute total power and radiation fractions
        # p_input_rt = p_ohm + p_lh_rt + p_icrf_rt + p_ecrh_rt + p_nbi_rt
        # rad_input_frac_rt = p_rad_rt / p_input_rt
        # rad_loss_frac_rt = p_rad_rt / (p_input_rt - dwmhd_dt)

        return {
            "p_rad_rt": p_rad_rt,
            "p_lh_rt": p_lh_rt,
            "p_nbi_rt": p_nbi_rt,
        }

    @staticmethod
    @physics_method(columns=["prad_peaking"], tokamak=Tokamak.EAST)
    def get_prad_peaking(params: PhysicsMethodParams):
        """
        Calculates the peaking factor of the radiated power measured by
        the AXUV arrays. It is defined as the ratio of the average of the
        centralmost 6 channels to the average of all of the non-divertor-
        viewing channels (core-to-average).

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the MDS connection and shot information.

        Returns
        -------
        dict
            A dictionary containing the peaking factor for the radiated
            power profile (`prad_peaking`).

        References
        -------
        - original source: [get_prad_peaking.m](https://github.com/MIT-PSFC/disr
        uption-py/blob/matlab/EAST/get_prad_peaking.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/
        411), #[451](https://github.com/MIT-PSFC/disruption-py/pull/451)
        """
        xuv, xuvtime = EastPhysicsMethods._get_raw_axuv_data(params)

        # Get calibration factors
        calib_factors = EastUtilMethods.get_axuv_calib_factors()
        # Apply calibration factors
        for i in range(64):
            # Original script has Del_r(ichan) which should be a mistake
            xuv[:, i] *= (
                calib_factors["Fac1"][i % 16]
                * calib_factors["Fac2"][i // 16][i % 16]
                * calib_factors["Fac3"][i // 16]
                * calib_factors["Fac4"]
                * 2
                * np.pi
                * calib_factors["Maj_R"]
                * calib_factors["Del_r"][i]
                # * calib_factors["Fac5"]
            )
        # Correction for bad channels (from Duan Yanmin's program)
        # get_radiated_power does these 2 lines first and then apply geometric correction.
        xuv[:, 11] = 0.5 * (xuv[:, 10] + xuv[:, 12])
        # xuv[:, 35] = 0.5 * (xuv[:, 34] + xuv[:, 35])

        # Define the core chords to be #28 to #37 (centermost 10 chords), and
        # define the non-divertor chords to be #09 to #56.  (Chords #1-8 view the
        # lower divertor region, and chords #57-64 view the upper divertor region.)
        ch_core = np.zeros(64)
        ch_core[29:35] = 1 / 6
        ch_all = np.zeros(64)
        ch_all[8:56] = 1 / 48

        prad_peaking = np.full((len(xuvtime)), np.nan)
        # TODO: find better way to do this for loop
        for itime in range(len(xuvtime)):
            prad_core = np.dot(xuv[itime, :], ch_core)
            prad_all = np.dot(xuv[itime, :], ch_all)
            prad_peaking[itime] = prad_core / prad_all

        # Interpret to the requested timebase
        prad_peaking = interp1(xuvtime, prad_peaking, params.times)

        return {"prad_peaking": prad_peaking}

    @staticmethod
    @physics_method(
        columns=["mirnov_std", "mirnov_std_normalized"], tokamak=Tokamak.EAST
    )
    def get_mirnov_std(params: PhysicsMethodParams):
        """
        Fetch the signal from a single Mirnov sensor, then compute the rolling
        standard deviation over time using a fixed 1 ms window.

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the MDS connection and shot information.

        Returns
        -------
        dict
            A dictionary containing the following keys:

            - `mirnov_std`: rolling standard deviation of a mirnov sensor.
            - `mirnov_std_normalized`: `mirnov_std` normalized to the toroidal
            magnetic field (`btor`).

        References
        -------
        - original source: [get_mirnov_std.m](https://github.com/MIT-PSFC/disrupt
        ion-py/blob/matlab/EAST/get_mirnov_std.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
        """
        mirnov_std = np.full(len(params.times), np.nan)
        mirnov_std_normalized = [np.nan]

        # Get the toroidal magnetic field.
        btor = EastPhysicsMethods.get_btor(params)["btor"]

        # Get the Mirnov signal from \cmp1t (5 MHz)
        time_window = 0.001
        bp_dot, bp_dot_time = params.mds_conn.get_data_with_dims(
            r"\cmp1t", tree_name="east"
        )  # [T/s], [s]
        for i, time in enumerate(params.times):
            (indices,) = np.where(
                ((time - time_window) < bp_dot_time) & (bp_dot_time < time)
            )
            mirnov_std[i] = np.nanstd(bp_dot[indices], ddof=1)

        mirnov_std_normalized = mirnov_std / abs(btor)

        return {
            "mirnov_std": mirnov_std,
            "mirnov_std_normalized": mirnov_std_normalized,
        }

    @staticmethod
    @physics_method(
        columns=["n1rms", "n2rms", "n1rms_normalized", "n2rms_normalized"],
        tokamak=Tokamak.EAST,
    )
    def get_n1rms_n2rms(params: PhysicsMethodParams):
        """
        Retrieve the Mirnov sensor array data and compute the $n$=1 and 2 Fourier
        mode amplitudes. Then calculate the rolling standard deviation of the amplitudes
        over a 1 ms window and normalize them to the toroidal magnetic field (`btor`).

        Parameters
        ----------
        params : PhysicsMethodParams
            The parameters containing the MDS connection and shot information.

        Returns
        -------
        dict
            A dictionary containing the following keys:

            - `n1rms`: rolling standard deviation of the $n$=1 mode from the Mirnov array.
            - `n2rms`: rolling standard deviation the $n$=2 mode from the Mirnov array.
            - `n1rms_normalized`: `n1rms` normalized to `btor`.
            - `n2rms_normalized`: `n2rms` normalized to `btor`.

        References
        -------
        - original source: [get_n1rms_n2rms.m](https://github.com/MIT-PSFC/disruption-py/blob
        /matlab/EAST/get_n1rms_n2rms.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
        """
        n1rms = np.full(len(params.times), np.nan)
        n2rms = np.full(len(params.times), np.nan)
        n1rms_normalized = [np.nan]
        n2rms_normalized = [np.nan]

        (mirtime,) = params.mds_conn.get_dims(r"\mitab2", tree_name="east")
        mir = np.full((len(mirtime), 16), np.nan)
        mir_nodes = [
            r"\mitab2",
            r"\mitbc2",
            r"\mitcd2",
            r"\mitde2",
            r"\mitef2",
            r"\mitfg2",
            r"\mitgh2",
            r"\mithi2",
            r"\mitij2",
            r"\mitjk2",
            r"\mitkl2",
            r"\mitlm2",
            r"\mitmn2",
            r"\mitno2",
            r"\mitop2",
            r"\mitpa2",
        ]
        for i, node in enumerate(mir_nodes):
            try:
                mir[:, i] = params.mds_conn.get_data(node, tree_name="east")
            except mdsExceptions.MdsException:
                continue

        # Get btor data
        btor = EastPhysicsMethods.get_btor(params)["btor"]

        # Compute the n=1 and n=2 mode signals from the Mirnov array signals
        mir_fft_output = scipy.fft.fft(mir, axis=1)
        amplitude = abs(mir_fft_output) / len(mir_fft_output[0])
        amplitude[:, 1:] *= 2  # TODO: Why?
        phase = np.arctan2(np.imag(mir_fft_output), np.real(mir_fft_output))
        n1 = amplitude[:, 1] * np.cos(phase[:, 1])
        n2 = amplitude[:, 2] * np.cos(phase[:, 2])

        # Calculate n1rms & n2rms
        time_window = 0.001
        for i, time in enumerate(params.times):
            (indices,) = np.where(
                (time - time_window < mirtime) & (mirtime < time + time_window)
            )
            n1rms[i] = np.nanstd(n1[indices], ddof=1)
            n2rms[i] = np.nanstd(n2[indices], ddof=1)

        # Calculate the normalized signals
        n1rms_normalized = n1rms / abs(btor)
        n2rms_normalized = n2rms / abs(btor)

        return {
            "n1rms": n1rms,
            "n2rms": n2rms,
            "n1rms_normalized": n1rms_normalized,
            "n2rms_normalized": n2rms_normalized,
        }

    @staticmethod
    @physics_method(columns=["h98"], tokamak=Tokamak.EAST)
    def get_h98(params: PhysicsMethodParams):
        """
        Get the *H98y2* energy confinement factor.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the *H98y2* energy confinement factor (`h98`).

        References
        -------
        - original source: [get_h98.m](https://github.com/MIT-PSFC/disruption-py/blob/
        matlab/EAST/get_h98.m)
        """
        h98_y2 = [np.nan]

        h98_y2, h98_y2_time = params.mds_conn.get_data_with_dims(
            r"\h98_mhd", tree_name="energy_east"
        )

        # Interpolate the signal onto the requested timebase
        h98_y2 = interp1(h98_y2_time, h98_y2, params.times)

        return {"h98": h98_y2}

    @staticmethod
    def _get_efit_gaps(params: PhysicsMethodParams, tree: str = "_efit_tree"):
        """
        Hidden method to calculate the EFIT and P-EFIT gaps
        """
        upper_gap = [np.nan]
        lower_gap = [np.nan]

        # Get plasma boundary data
        data, efittime = params.mds_conn.get_data_with_dims(
            r"\top.results.geqdsk:bdry", tree_name=tree
        )
        # Convert the order of indices to MATLAB order
        # MATLAB (0, 1, 2) -> Python (2, 1, 0)
        data = np.transpose(data, [2, 1, 0])
        xcoords, ycoords = data

        # Get first wall geometry data
        xfirstwall = params.mds_conn.get_data(
            r"\top.results.geqdsk:xlim", tree_name=tree
        )
        yfirstwall = params.mds_conn.get_data(
            r"\top.results.geqdsk:ylim", tree_name=tree
        )
        seed = np.ones((len(xcoords), 1))
        xfirstwall = np.reshape(xfirstwall, (-1, 1))
        yfirstwall = np.reshape(yfirstwall, (-1, 1))
        xfirstwall_mat = np.tile(xfirstwall, (1, len(efittime)))
        yfirstwall_mat = np.tile(yfirstwall, (1, len(efittime)))

        # Calculate upper & lower gaps
        index_upperwall, _ = np.where(yfirstwall > 0.6)
        index_lowerwall, _ = np.where(yfirstwall < -0.6)

        xupperwall = xfirstwall_mat[index_upperwall, :]
        xupperwall_mat = np.reshape(
            np.kron(xupperwall, seed), (len(seed), -1, len(efittime))
        )
        xlowerwall = xfirstwall_mat[index_lowerwall, :]
        xlowerwall_mat = np.reshape(
            np.kron(xlowerwall, seed), (len(seed), -1, len(efittime))
        )

        yupperwall = yfirstwall_mat[index_upperwall, :]
        yupperwall_mat = np.reshape(
            np.kron(yupperwall, seed), (len(seed), -1, len(efittime))
        )
        ylowerwall = yfirstwall_mat[index_lowerwall, :]
        ylowerwall_mat = np.reshape(
            np.kron(ylowerwall, seed), (len(seed), -1, len(efittime))
        )

        xupperplasma_mat = np.reshape(
            np.tile(xcoords, (len(xupperwall), 1)), (-1, len(xupperwall), len(efittime))
        )
        yupperplasma_mat = np.reshape(
            np.tile(ycoords, (len(yupperwall), 1)), (-1, len(yupperwall), len(efittime))
        )
        xlowerplasma_mat = np.reshape(
            np.tile(xcoords, (len(xlowerwall), 1)), (-1, len(xlowerwall), len(efittime))
        )
        ylowerplasma_mat = np.reshape(
            np.tile(ycoords, (len(ylowerwall), 1)), (-1, len(ylowerwall), len(efittime))
        )

        uppergap_mat = np.sqrt(
            np.square(xupperplasma_mat - xupperwall_mat)
            + np.square(yupperplasma_mat - yupperwall_mat)
        )
        lowergap_mat = np.sqrt(
            np.square(xlowerplasma_mat - xlowerwall_mat)
            + np.square(ylowerplasma_mat - ylowerwall_mat)
        )
        upper_gap = uppergap_mat.min(axis=(0, 1))
        lower_gap = lowergap_mat.min(axis=(0, 1))

        # Interpolate to the requested timebase
        upper_gap = interp1(efittime, upper_gap, params.times)
        lower_gap = interp1(efittime, lower_gap, params.times)

        return {"upper_gap": upper_gap, "lower_gap": lower_gap}

    @staticmethod
    @physics_method(
        columns=["upper_gap", "lower_gap", "pupper_gap", "plower_gap"],
        tokamak=Tokamak.EAST,
    )
    def get_efit_gaps(params: PhysicsMethodParams):
        """
        Calculate the upper and lower gaps from the EFIT/P-EFIT
        plasma boundary and first wall geometry information.

        Parameters
        ----------
        params : PhysicsMethodParams
            Parameters containing MDS connection and shot information

        Returns
        -------
        dict
            A dictionary containing the upper and lower gaps computed from
            EFIT (`upper_gap` and `lower_gap`) and P-EFIT (`pupper_gap` and
            `plower_gap`) data.

        References
        -------
        - original sources: [get_EFIT_gaps.m](https://github.com/MIT-PSFC/disruption-
        py/blob/matlab/EAST/get_EFIT_gaps.m), [get_PEFIT_gaps.m](https://github.com/M
        IT-PSFC/disruption-py/blob/matlab/EAST/get_PEFIT_gaps.m)
        - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
        """
        efit_gaps = EastPhysicsMethods._get_efit_gaps(params=params, tree="_efit_tree")
        pefit_gaps = EastPhysicsMethods._get_efit_gaps(params=params, tree="pefit_east")

        return {
            "upper_gap": efit_gaps["upper_gap"],
            "lower_gap": efit_gaps["lower_gap"],
            "pupper_gap": pefit_gaps["upper_gap"],
            "plower_gap": pefit_gaps["lower_gap"],
        }

get_btor staticmethod ¤

get_btor(params: PhysicsMethodParams)

Calculate the toroidal magnetic field.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the toroidal magnetic field (btor).

References
Source code in disruption_py/machine/east/physics.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
@staticmethod
@physics_method(columns=["btor"], tokamak=Tokamak.EAST)
def get_btor(params: PhysicsMethodParams):
    r"""
    Calculate the toroidal magnetic field.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the toroidal magnetic field (`btor`).

    References
    -------
    - original sources: [get_n_equal_1_data.m](https://github.com/MIT-PSFC/disr
    uption-py/blob/matlab/EAST/get_n_equal_1_data.m), [get_mirnov_std.m](https:
    //github.com/MIT-PSFC/disruption-py/blob/matlab/EAST/get_mirnov_std.m),
    [get_n1rms_n2rms.m](https://github.com/MIT-PSFC/disruption-py/blob/matlab/
    EAST/get_n1rms_n2rms.m)
    """
    if 60000 <= params.shot_id <= 65165:
        tree = "pcs_east"
    else:
        tree = "eng_tree"
    itf, btor_time = params.mds_conn.get_data_with_dims(r"\it", tree_name=tree)
    btor = (
        (4 * np.pi * 1e-7) * itf * (16 * 130) / (2 * np.pi * 1.8)
    )  # about 4,327 amps/tesla
    if params.shot_id < 60000:
        # Btor is constant in time (superconducting magnet).
        # Construct 2-point signal from scalar value
        btor = np.mean(btor)
        btor = [btor, btor]
        btor_time = [0, 1000]

    # Interpolate onto the requested timebase
    btor = interp1(btor_time, btor, params.times)

    return {"btor": btor}

get_density_parameters staticmethod ¤

get_density_parameters(params: PhysicsMethodParams)

Calculate the electron density, its time derivative, and the Greenwald fraction.

The Greenwald fraction is the ratio of the measured electron density \(n_e\) and the Greenwald density limit \(n_G\) which is defined as 1:

\[ n_G = \frac{I_p}{\pi a^2} \]

where \(n_G\) is given in \(10^{20} m^{-3}\) and \(I_p\) is in MA.

The line-averaged electron density is obtained from the HCN vertical chord. This signal is also used by the plasma control system (PCS) for feedback control of the density.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing electron density (n_e), its time derivative (dn_dt), and the Greenwald fraction (greenwald_fraction).

References
Source code in disruption_py/machine/east/physics.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
@staticmethod
@physics_method(
    columns=["n_e", "greenwald_fraction", "dn_dt"], tokamak=Tokamak.EAST
)
def get_density_parameters(params: PhysicsMethodParams):
    r"""
    Calculate the electron density, its time derivative, and the Greenwald fraction.

    The Greenwald fraction is the ratio of the measured electron density $n_e$ and
    the Greenwald density limit $n_G$ which is defined as [^1]:

    $$
    n_G = \frac{I_p}{\pi a^2}
    $$

    where $n_G$ is given in $10^{20} m^{-3}$ and $I_p$ is in MA.

    The line-averaged electron density is obtained from the HCN vertical chord.
    This signal is also used by the plasma control system (PCS) for feedback control of
    the density.

    [^1]: https://wiki.fusion.ciemat.es/wiki/Greenwald_limit

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing electron density (`n_e`), its time derivative (`dn_dt`),
        and the Greenwald fraction (`greenwald_fraction`).

    References
    -------
    - original source: [get_density_parameters.m](https://github.com/MIT-PSFC/disruption-py
    /blob/matlab/EAST/get_density_parameters.m)
    """
    ne = [np.nan]
    greenwald_fraction = [np.nan]
    dn_dt = [np.nan]

    # Get the density and calculate dn_dt
    ne, netime = params.mds_conn.get_data_with_dims(
        r"\dfsdev*1e19", tree_name="pcs_east"
    )  # [m^-3], [s]
    dn_dt = np.gradient(ne, netime)  # [m^-3/s]

    # Interpolate ne and dn_dt to the requested timebase
    ne = interp1(netime, ne, params.times)
    dn_dt = interp1(netime, dn_dt, params.times)

    # Calculate Greenwald density
    # TODO: use \aminor or \aout? -- MATLAB: \aout
    aminor, efittime = params.mds_conn.get_data_with_dims(
        r"\efit_aeqdsk:aout", tree_name="_efit_tree"
    )  # [m], [s]
    aminor = interp1(efittime, aminor, params.times)
    ip = EastPhysicsMethods.get_ip_parameters(params)["ip"]  # [A]
    n_g = 1e20 * (ip / 1e6) / (np.pi * aminor**2)  # [m^-3]
    greenwald_fraction = ne / n_g

    return {"n_e": ne, "greenwald_fraction": greenwald_fraction, "dn_dt": dn_dt}

get_efit_gaps staticmethod ¤

get_efit_gaps(params: PhysicsMethodParams)

Calculate the upper and lower gaps from the EFIT/P-EFIT plasma boundary and first wall geometry information.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the upper and lower gaps computed from EFIT (upper_gap and lower_gap) and P-EFIT (pupper_gap and plower_gap) data.

References
Source code in disruption_py/machine/east/physics.py
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
@staticmethod
@physics_method(
    columns=["upper_gap", "lower_gap", "pupper_gap", "plower_gap"],
    tokamak=Tokamak.EAST,
)
def get_efit_gaps(params: PhysicsMethodParams):
    """
    Calculate the upper and lower gaps from the EFIT/P-EFIT
    plasma boundary and first wall geometry information.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the upper and lower gaps computed from
        EFIT (`upper_gap` and `lower_gap`) and P-EFIT (`pupper_gap` and
        `plower_gap`) data.

    References
    -------
    - original sources: [get_EFIT_gaps.m](https://github.com/MIT-PSFC/disruption-
    py/blob/matlab/EAST/get_EFIT_gaps.m), [get_PEFIT_gaps.m](https://github.com/M
    IT-PSFC/disruption-py/blob/matlab/EAST/get_PEFIT_gaps.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
    """
    efit_gaps = EastPhysicsMethods._get_efit_gaps(params=params, tree="_efit_tree")
    pefit_gaps = EastPhysicsMethods._get_efit_gaps(params=params, tree="pefit_east")

    return {
        "upper_gap": efit_gaps["upper_gap"],
        "lower_gap": efit_gaps["lower_gap"],
        "pupper_gap": pefit_gaps["upper_gap"],
        "plower_gap": pefit_gaps["lower_gap"],
    }

get_h98 staticmethod ¤

get_h98(params: PhysicsMethodParams)

Get the H98y2 energy confinement factor.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the H98y2 energy confinement factor (h98).

References
Source code in disruption_py/machine/east/physics.py
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
@staticmethod
@physics_method(columns=["h98"], tokamak=Tokamak.EAST)
def get_h98(params: PhysicsMethodParams):
    """
    Get the *H98y2* energy confinement factor.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the *H98y2* energy confinement factor (`h98`).

    References
    -------
    - original source: [get_h98.m](https://github.com/MIT-PSFC/disruption-py/blob/
    matlab/EAST/get_h98.m)
    """
    h98_y2 = [np.nan]

    h98_y2, h98_y2_time = params.mds_conn.get_data_with_dims(
        r"\h98_mhd", tree_name="energy_east"
    )

    # Interpolate the signal onto the requested timebase
    h98_y2 = interp1(h98_y2_time, h98_y2, params.times)

    return {"h98": h98_y2}

get_ip_parameters staticmethod ¤

get_ip_parameters(params: PhysicsMethodParams)

This routine retrieves the plasma current signals. It then calculates the error between the programmed and the actual plasma current, and the time derivatives of the actual and the programmed plasma current.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the following keys:

  • ip: Measured plasma current.
  • ip_prog: Programmed plasma current.
  • ip_error: Error between the actual and programmed plasma currents.
  • ip_error_normalized: ip_error normalized to ip_prog.
  • dip_dt: Time derivative of the measured plasma current.
  • dipprog_dt: Time derivative of the programmed plasma current.
References
Source code in disruption_py/machine/east/physics.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
@staticmethod
@physics_method(
    columns=[
        "ip",
        "ip_prog",
        "ip_error",
        "ip_error_normalized",
        "dip_dt",
        "dipprog_dt",
    ],
    tokamak=Tokamak.EAST,
)
def get_ip_parameters(params: PhysicsMethodParams):
    """
    This routine retrieves the plasma current signals. It then calculates the error
    between the programmed and the actual plasma current, and the time derivatives
    of the actual and the programmed plasma current.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the following keys:

        - `ip`: Measured plasma current.
        - `ip_prog`: Programmed plasma current.
        - `ip_error`: Error between the actual and programmed plasma currents.
        - `ip_error_normalized`: `ip_error` normalized to `ip_prog`.
        - `dip_dt`: Time derivative of the measured plasma current.
        - `dipprog_dt`: Time derivative of the programmed plasma current.

    References
    -------
    - original source: [get_Ip_parameters.m](https://github.com/MIT-PSFC/disruption-py
    /blob/matlab/EAST/get_Ip_parameters.m)
    """
    ip = [np.nan]
    ip_prog = [np.nan]
    ip_error = [np.nan]
    ip_error_normalized = [np.nan]
    dip_dt = [np.nan]
    dipprog_dt = [np.nan]

    ip, ip_time = EastUtilMethods.retrieve_ip(params.mds_conn, params.shot_id)
    # Calculate dip_dt
    dip_dt = np.gradient(ip, ip_time)

    # Get the programmed plasma current.  For most times, the EAST plasma
    # control system (PCS) programming is signal "\lmtipref" in the pcs_east
    # tree.  However, there is an alternate control scheme, "isoflux" control,
    # which uses one of four possible signals (\ietip, \idtip, \istip, iutip).
    # Note that only one of these 4 signals is defined at any given time during
    # the shot.  The transition from normal control to isoflux control is
    # identified by several signals, one of which is called "\sytps1".  When
    # \sytps1 = 0, use \lmtipref for ip_prog.  When \sytps1 > 0, use whichever
    # isoflux or transition signal is defined.  Also, accordingly to Yuan
    # Qiping (EAST PCS expert), no shot is ever programmed to have a
    # back-transition, i.e. after a transition to isoflux control, no shot is
    # ever programmed to transition back to standard control.

    # Start with \lmtipref (standard Ip programming), which is defined for the
    # entire shot, and defines the timebase for the programmed Ip signal.
    ip_prog, ip_prog_time = params.mds_conn.get_data_with_dims(
        r"\lmtipref*1e6", tree_name="pcs_east"
    )  # [A], [s]

    # Now check to see if there is a transition to isoflux control
    sytps1, time_sytps1 = params.mds_conn.get_data_with_dims(
        r"\sytps1", tree_name="pcs_east"
    )
    (sytps1_indices,) = np.where(sytps1 > 0)

    # If PCS switches to isoflux control, then read in the 4 possible Ip target
    # signals, and interpolate each one onto the \lmtipref timebase.  There will
    # be many NaN values.

    if len(sytps1_indices) > 0 and time_sytps1[0] <= ip_prog_time[-1]:
        try:
            ietip, ietip_time = params.mds_conn.get_data_with_dims(
                r"\ietip", tree_name="pcs_east"
            )
            ietip = interp1(ietip_time, ietip, ip_prog_time)
        except mdsExceptions.MdsException:
            ietip = np.full(len(ip_prog_time), np.nan)

        try:
            idtip, idtip_time = params.mds_conn.get_data_with_dims(
                r"\idtip", tree_name="pcs_east"
            )
            idtip = interp1(idtip_time, idtip, ip_prog_time)
        except mdsExceptions.MdsException:
            idtip = np.full(len(ip_prog_time), np.nan)

        try:
            istip, istip_time = params.mds_conn.get_data_with_dims(
                r"\istip", tree_name="pcs_east"
            )
            istip = interp1(istip_time, istip, ip_prog_time)
        except mdsExceptions.MdsException:
            istip = np.full(len(ip_prog_time), np.nan)

        try:
            iutip, iutip_time = params.mds_conn.get_data_with_dims(
                r"\iutip", tree_name="pcs_east"
            )
            iutip = interp1(iutip_time, iutip, ip_prog_time)
        except mdsExceptions.MdsException:
            iutip = np.full(len(ip_prog_time), np.nan)

        # Okay, now loop through all times, selecting the alternate isoflux target
        # Ip value for each time that isoflux control is enabled (i.e. for each
        # time that \sytps1 ~= 0).  (Only one of the 4 possible isoflux target Ip
        # signals is defined at each time.)
        tstart_isoflux_control = time_sytps1[sytps1_indices[0]]
        (indices,) = np.where(ip_prog_time >= tstart_isoflux_control)

        for it in range(len(indices)):
            if not np.isnan(ietip[it]):
                ip_prog[it] = ietip[it]
            elif not np.isnan(idtip[it]):
                ip_prog[it] = idtip[it]
            elif not np.isnan(istip[it]):
                ip_prog[it] = istip[it]
            elif not np.isnan(iutip[it]):
                ip_prog[it] = iutip[it]

        # End of block for dealing with isoflux control

    # For shots before year 2014, the LMTIPREF timebase needs to be shifted
    # by 17.0 ms
    if params.shot_id < 44432:
        ip_prog_time -= 0.0170

    # Calculate dipprog_dt
    dipprog_dt = np.gradient(ip_prog, ip_prog_time)

    # Interpolate all retrieved signals to the requested timebase
    ip = interp1(ip_time, ip, params.times)
    dip_dt = interp1(ip_time, dip_dt, params.times)
    ip_prog = interp1(ip_prog_time, ip_prog, params.times)
    dipprog_dt = interp1(ip_prog_time, dipprog_dt, params.times)

    # Calculate ip_error and ip_error_normalized
    ip_error = ip - ip_prog
    ip_error_normalized = ip_error / ip_prog

    return {
        "ip": ip,
        "ip_prog": ip_prog,
        "ip_error": ip_error,
        "ip_error_normalized": ip_error_normalized,
        "dip_dt": dip_dt,
        "dipprog_dt": dipprog_dt,
    }

get_kappa_area staticmethod ¤

get_kappa_area(params: PhysicsMethodParams)

Calculate the plasma's ellipticity (kappa, also known as the elongation) using its area and minor radius. It is defined as:

\[ \kappa_{area} = \frac{A}{\pi a^2} \]

where \(A\) is the plasma cross-sectional area and \(a\) is the minor radius.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the kappa_area signal.

References
Source code in disruption_py/machine/east/physics.py
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
@staticmethod
@physics_method(columns=["kappa_area"], tokamak=Tokamak.EAST)
def get_kappa_area(params: PhysicsMethodParams):
    r"""
    Calculate the plasma's ellipticity (*kappa*, also known as
    the elongation) using its area and minor radius. It is defined as:

    $$
    \kappa_{area} = \frac{A}{\pi a^2}
    $$

    where $A$ is the plasma cross-sectional area and $a$ is the minor radius.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the `kappa_area` signal.

    References
    -------
    - original source: [get_kappa_area.m](https://github.com/MIT-PSFC/disrup
    tion-py/blob/matlab/EAST/get_kappa_area.m)
    """
    # Get area and aminor from EastEfitMethods
    efit_params = EastEfitMethods.get_efit_parameters(params=params)
    area = efit_params["area"]
    aminor = efit_params["aminor"]
    # Compute kappa_area
    with np.errstate(divide="ignore", invalid="ignore"):
        kappa_area = area / (np.pi * aminor**2)

    return {"kappa_area": kappa_area}

get_mirnov_std staticmethod ¤

get_mirnov_std(params: PhysicsMethodParams)

Fetch the signal from a single Mirnov sensor, then compute the rolling standard deviation over time using a fixed 1 ms window.

PARAMETER DESCRIPTION
params

The parameters containing the MDS connection and shot information.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the following keys:

  • mirnov_std: rolling standard deviation of a mirnov sensor.
  • mirnov_std_normalized: mirnov_std normalized to the toroidal magnetic field (btor).
References
Source code in disruption_py/machine/east/physics.py
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
@staticmethod
@physics_method(
    columns=["mirnov_std", "mirnov_std_normalized"], tokamak=Tokamak.EAST
)
def get_mirnov_std(params: PhysicsMethodParams):
    """
    Fetch the signal from a single Mirnov sensor, then compute the rolling
    standard deviation over time using a fixed 1 ms window.

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the MDS connection and shot information.

    Returns
    -------
    dict
        A dictionary containing the following keys:

        - `mirnov_std`: rolling standard deviation of a mirnov sensor.
        - `mirnov_std_normalized`: `mirnov_std` normalized to the toroidal
        magnetic field (`btor`).

    References
    -------
    - original source: [get_mirnov_std.m](https://github.com/MIT-PSFC/disrupt
    ion-py/blob/matlab/EAST/get_mirnov_std.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
    """
    mirnov_std = np.full(len(params.times), np.nan)
    mirnov_std_normalized = [np.nan]

    # Get the toroidal magnetic field.
    btor = EastPhysicsMethods.get_btor(params)["btor"]

    # Get the Mirnov signal from \cmp1t (5 MHz)
    time_window = 0.001
    bp_dot, bp_dot_time = params.mds_conn.get_data_with_dims(
        r"\cmp1t", tree_name="east"
    )  # [T/s], [s]
    for i, time in enumerate(params.times):
        (indices,) = np.where(
            ((time - time_window) < bp_dot_time) & (bp_dot_time < time)
        )
        mirnov_std[i] = np.nanstd(bp_dot[indices], ddof=1)

    mirnov_std_normalized = mirnov_std / abs(btor)

    return {
        "mirnov_std": mirnov_std,
        "mirnov_std_normalized": mirnov_std_normalized,
    }

get_n1rms_n2rms staticmethod ¤

get_n1rms_n2rms(params: PhysicsMethodParams)

Retrieve the Mirnov sensor array data and compute the \(n\)=1 and 2 Fourier mode amplitudes. Then calculate the rolling standard deviation of the amplitudes over a 1 ms window and normalize them to the toroidal magnetic field (btor).

PARAMETER DESCRIPTION
params

The parameters containing the MDS connection and shot information.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the following keys:

  • n1rms: rolling standard deviation of the \(n\)=1 mode from the Mirnov array.
  • n2rms: rolling standard deviation the \(n\)=2 mode from the Mirnov array.
  • n1rms_normalized: n1rms normalized to btor.
  • n2rms_normalized: n2rms normalized to btor.
References
Source code in disruption_py/machine/east/physics.py
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
@staticmethod
@physics_method(
    columns=["n1rms", "n2rms", "n1rms_normalized", "n2rms_normalized"],
    tokamak=Tokamak.EAST,
)
def get_n1rms_n2rms(params: PhysicsMethodParams):
    """
    Retrieve the Mirnov sensor array data and compute the $n$=1 and 2 Fourier
    mode amplitudes. Then calculate the rolling standard deviation of the amplitudes
    over a 1 ms window and normalize them to the toroidal magnetic field (`btor`).

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the MDS connection and shot information.

    Returns
    -------
    dict
        A dictionary containing the following keys:

        - `n1rms`: rolling standard deviation of the $n$=1 mode from the Mirnov array.
        - `n2rms`: rolling standard deviation the $n$=2 mode from the Mirnov array.
        - `n1rms_normalized`: `n1rms` normalized to `btor`.
        - `n2rms_normalized`: `n2rms` normalized to `btor`.

    References
    -------
    - original source: [get_n1rms_n2rms.m](https://github.com/MIT-PSFC/disruption-py/blob
    /matlab/EAST/get_n1rms_n2rms.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
    """
    n1rms = np.full(len(params.times), np.nan)
    n2rms = np.full(len(params.times), np.nan)
    n1rms_normalized = [np.nan]
    n2rms_normalized = [np.nan]

    (mirtime,) = params.mds_conn.get_dims(r"\mitab2", tree_name="east")
    mir = np.full((len(mirtime), 16), np.nan)
    mir_nodes = [
        r"\mitab2",
        r"\mitbc2",
        r"\mitcd2",
        r"\mitde2",
        r"\mitef2",
        r"\mitfg2",
        r"\mitgh2",
        r"\mithi2",
        r"\mitij2",
        r"\mitjk2",
        r"\mitkl2",
        r"\mitlm2",
        r"\mitmn2",
        r"\mitno2",
        r"\mitop2",
        r"\mitpa2",
    ]
    for i, node in enumerate(mir_nodes):
        try:
            mir[:, i] = params.mds_conn.get_data(node, tree_name="east")
        except mdsExceptions.MdsException:
            continue

    # Get btor data
    btor = EastPhysicsMethods.get_btor(params)["btor"]

    # Compute the n=1 and n=2 mode signals from the Mirnov array signals
    mir_fft_output = scipy.fft.fft(mir, axis=1)
    amplitude = abs(mir_fft_output) / len(mir_fft_output[0])
    amplitude[:, 1:] *= 2  # TODO: Why?
    phase = np.arctan2(np.imag(mir_fft_output), np.real(mir_fft_output))
    n1 = amplitude[:, 1] * np.cos(phase[:, 1])
    n2 = amplitude[:, 2] * np.cos(phase[:, 2])

    # Calculate n1rms & n2rms
    time_window = 0.001
    for i, time in enumerate(params.times):
        (indices,) = np.where(
            (time - time_window < mirtime) & (mirtime < time + time_window)
        )
        n1rms[i] = np.nanstd(n1[indices], ddof=1)
        n2rms[i] = np.nanstd(n2[indices], ddof=1)

    # Calculate the normalized signals
    n1rms_normalized = n1rms / abs(btor)
    n2rms_normalized = n2rms / abs(btor)

    return {
        "n1rms": n1rms,
        "n2rms": n2rms,
        "n1rms_normalized": n1rms_normalized,
        "n2rms_normalized": n2rms_normalized,
    }

get_n_equal_1_data staticmethod ¤

get_n_equal_1_data(params: PhysicsMethodParams)

Compute the amplitude and phase of the \(n\)=1 Fourier component of the net saddle signals (total saddle signals minus the calculated pickup from the RMP coils).

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the following keys:

  • n_equal_1_mode: Amplitude of \(n\)=1 Fourier component of saddle signals after subtracting the calculated pickup from the RMP coil currents.
  • n_equal_1_phase: Toroidal phase of the \(n\)=1 mode.
  • n_equal_1_normalized: n_equal_1_mode normalized to the toroidal magnetic field (btor).
  • rmp_n_equal_1: Amplitude of the \(n\)=1 Fourier component of the calculated pickup of the RMP coils on the saddle signals.
  • rmp_n_equal_1_phase: toroidal phase of the \(n\)=1 rmp pickup.
References
Source code in disruption_py/machine/east/physics.py
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
@staticmethod
@physics_method(
    columns=[
        "n_equal_1_mode",
        "n_equal_1_phase",
        "n_equal_1_normalized",
        "rmp_n_equal_1",
        "rmp_n_equal_1_phase",
    ],
    tokamak=Tokamak.EAST,
)
def get_n_equal_1_data(params: PhysicsMethodParams):
    """
    Compute the amplitude and phase of the $n$=1 Fourier component of the net
    saddle signals (total saddle signals minus the calculated pickup from
    the RMP coils).

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the following keys:

        - `n_equal_1_mode`: Amplitude of $n$=1 Fourier component of saddle signals
        after subtracting the calculated pickup from the RMP coil currents.
        - `n_equal_1_phase`: Toroidal phase of the $n$=1 mode.
        - `n_equal_1_normalized`: `n_equal_1_mode` normalized to the toroidal magnetic
        field (`btor`).
        - `rmp_n_equal_1`: Amplitude of the $n$=1 Fourier component of the calculated
        pickup of the RMP coils on the saddle signals.
        - `rmp_n_equal_1_phase`: toroidal phase of the $n$=1 rmp pickup.

    References
    -------
    - original source: [get_n_equal_1_data.m](https://github.com/MIT-PSFC/disruption-
    py/blob/matlab/EAST/get_n_equal_1_data.m), [get_rmp_and_saddle_signals.m](https://
    github.com/MIT-PSFC/disruption-py/blob/matlab/EAST/get_rmp_and_saddle_signals.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
    """
    n_equal_1_mode = [np.nan]
    n_equal_1_phase = [np.nan]
    n_equal_1_normalized = [np.nan]
    rmp_n_equal_1 = [np.nan]
    rmp_n_equal_1_phase = [np.nan]

    # Get the rmp coil currents
    # Translated from get_rmp_and_saddle_signals.m
    (rmptime,) = params.mds_conn.get_dims(r"\irmpu1", tree_name="east")
    rmp = np.full((len(rmptime), 16), np.nan)
    for i in range(8):
        # Get irmpu1 to irmpu8
        signal = params.mds_conn.get_data(rf"\irmpu{i+1}", tree_name="east")
        if len(signal) == len(rmptime):
            rmp[:, i] = signal
        # Get irmpl1 to irmpl8
        signal = params.mds_conn.get_data(rf"\irmpl{i+1}", tree_name="east")
        if len(signal) == len(rmptime):
            rmp[:, i + 8] = signal
    # Get saddle coil signals
    (saddletime,) = params.mds_conn.get_dims(r"\sad_pa", tree_name="east")
    saddle = np.full((len(saddletime), 8), np.nan)
    saddle_nodes = [
        r"\sad_pa",
        r"\sad_bc",
        r"\sad_de",
        r"\sad_fg",
        r"\sad_hi",
        r"\sad_jk",
        r"\sad_lm",
        # r"\sad_no",   # not operational in 2015
    ]
    for i, node in enumerate(saddle_nodes[:7]):
        try:
            saddle[:, i] = params.mds_conn.get_data(node, tree_name="east")
        except mdsExceptions.MdsException:
            saddle[:, i] = 0
    sad_lo = params.mds_conn.get_data(r"\sad_lo", tree_name="east")
    sad_lm = params.mds_conn.get_data(r"\sad_lm", tree_name="east")
    saddle[:, 7] = sad_lo - sad_lm

    # Calculate RMP n=1 Fourier component amplitude and phase (on the timebase
    # of the saddle signals)
    coeff_matrix = EastUtilMethods.load_rmp_saddle_coeff_matrix()
    rmp_pickup = np.transpose(np.matmul(coeff_matrix, np.transpose(rmp)))
    # Interpolate rmp_pickup onto the saddle time timebase
    rmp_pickup = interp1(rmptime, rmp_pickup, saddletime, axis=0)

    # Calculate fast Fourier transforms of the RMP-induced signals, and get
    # mode amplitudes and phases
    # Take FFT along 2nd dimension (phi)
    rmp_fft_output = scipy.fft.fft(rmp_pickup, axis=1)
    amplitude = abs(rmp_fft_output) / len(rmp_fft_output[0])
    amplitude[:, 1:] *= 2  # TODO: Why?
    phase = np.arctan2(np.imag(rmp_fft_output), np.real(rmp_fft_output))
    # Only want n=1 Fourier component
    rmp_n_equal_1 = amplitude[:, 1]
    rmp_n_equal_1_phase = phase[:, 1]
    # Interpolate onto the requested timebase
    # TODO: figure out how to interpolate phase
    rmp_n_equal_1 = interp1(saddletime, rmp_n_equal_1, params.times)
    rmp_n_equal_1_phase = interp1(saddletime, rmp_n_equal_1_phase, params.times)

    # The saddle signals can include direct pickup from the RMP coils, when the
    # RMP coils are active.  This direct pickup must be subtracted from the
    # saddle signals to leave just the plasma response and baseline drifts.
    saddle -= rmp_pickup
    # Calculate fast Fourier transforms and get mode amplitudes and phases
    # Take FFT along 2nd dimension (phi)
    saddle_fft_output = scipy.fft.fft(saddle, axis=1)
    amplitude = abs(saddle_fft_output) / len(saddle_fft_output[0])
    amplitude[:, 1:] *= 2  # TODO: Why?
    phase = np.arctan2(np.imag(saddle_fft_output), np.real(saddle_fft_output))
    # Only want n=1 Fourier component
    n_equal_1_mode = amplitude[:, 1]
    n_equal_1_phase = phase[:, 1]
    # Interpolate onto the requested timebase
    n_equal_1_mode = interp1(saddletime, n_equal_1_mode, params.times)
    n_equal_1_phase = interp1(saddletime, n_equal_1_phase, params.times)

    # Next, get the toroidal magnetic field and compute n_equal_1_normalized
    btor = EastPhysicsMethods.get_btor(params)["btor"]
    n_equal_1_normalized = n_equal_1_mode / abs(btor)

    return {
        "n_equal_1_mode": n_equal_1_mode,
        "n_equal_1_phase": n_equal_1_phase,
        "n_equal_1_normalized": n_equal_1_normalized,
        "rmp_n_equal_1": rmp_n_equal_1,
        "rmp_n_equal_1_phase": rmp_n_equal_1_phase,
    }

get_p_ohm staticmethod ¤

get_p_ohm(params: PhysicsMethodParams)

Calculate the ohmic heating power from the loop voltage, inductive voltage, and plasma current.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the ohmic heating power (p_oh).

References
Source code in disruption_py/machine/east/physics.py
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
@staticmethod
@physics_method(columns=["p_oh"], tokamak=Tokamak.EAST)
def get_p_ohm(params: PhysicsMethodParams):
    r"""
    Calculate the ohmic heating power from the loop voltage, inductive voltage, and
    plasma current.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the ohmic heating power (`p_oh`).

    References
    -------
    - original source: [get_P_ohm.m](https://github.com/MIT-PSFC/disruption-py
    /blob/matlab/EAST/get_P_ohm.m)
    """
    # Get raw signals
    try:
        vloop, vloop_time = params.mds_conn.get_data_with_dims(
            r"\pcvloop", tree_name="pcs_east"
        )  # [V]
        li, li_time = params.mds_conn.get_data_with_dims(
            r"\efit_aeqdsk:li", tree_name="_efit_tree"
        )  # [H]
        # Fetch raw ip signal to calculate dip_dt and apply smoothing
        ip, ip_time = params.mds_conn.get_data_with_dims(
            r"\pcrl01", tree_name="pcs_east"
        )  # [A]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get necessary signals to compute p_ohm")
        return {"p_oh": [np.nan]}
    sign_ip = np.sign(sum(ip))
    dipdt = np.gradient(ip, ip_time)
    # TODO: switch to causal boxcar smoothing
    dipdt_smoothed = matlab_smooth(dipdt, 11)  # Use 11-point boxcar smoothing

    # Interpolate fetched signals to the requested timebase
    vloop = interp1(vloop_time, vloop, params.times, kind="linear", bounds_error=0)
    li = interp1(li_time, li, params.times, kind="linear", bounds_error=0)
    ip = interp1(ip_time, ip, params.times, kind="linear", bounds_error=0)
    dipdt_smoothed = interp1(
        ip_time, dipdt_smoothed, params.times, kind="linear", bounds_error=0
    )

    # Calculate p_ohm
    # TODO: replace 1.85 with fetched r0 signal
    inductance = 4e-7 * np.pi * 1.85 * li / 2  # For EAST use R0 = 1.85 m
    v_inductive = -inductance * dipdt_smoothed
    v_resistive = vloop - v_inductive
    (v_resistive_indices,) = np.where(v_resistive * sign_ip < 0)
    v_resistive[v_resistive_indices] = 0
    p_ohm = ip * v_resistive

    return {"p_oh": p_ohm}

get_pcs_parameters staticmethod ¤

get_pcs_parameters(params: PhysicsMethodParams)

Retrieve a subset of the real-time diagnostic signals that are used in the EAST plasma control system (PCS). These signals are either calculated by RT-EFIT (pre-2018) or P-EFIT (since 2018).

PARAMETER DESCRIPTION
params

The parameters containing the MDS connection and shot information.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing ip_error_rt, q95_rt, beta_p_rt, li_rt, and wmhd_rt.

References
Source code in disruption_py/machine/east/physics.py
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
@staticmethod
@physics_method(
    columns=["ip_error_rt", "q95_rt", "beta_p_rt", "li_rt", "wmhd_rt"],
    tokamak=Tokamak.EAST,
)
def get_pcs_parameters(params: PhysicsMethodParams):
    """
    Retrieve a subset of the real-time diagnostic signals that are used in the EAST
    plasma control system (PCS). These signals are either calculated by RT-EFIT (pre-2018)
    or P-EFIT (since 2018).

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the MDS connection and shot information.

    Returns
    -------
    dict
        A dictionary containing `ip_error_rt`, `q95_rt`, `beta_p_rt`, `li_rt`, and `wmhd_rt`.

    References
    -------
    - original source: [get_pcs.m](https://github.com/MIT-PSFC/disruption-py/
    blob/matlab/EAST/get_pcs.m)
    """
    output = {}

    # Get ip_error_rt, beta_p_rt, li_rt, and wmhd_rt
    signals = {
        "ip_error_rt": r"\lmeip",
        "beta_p_rt": r"\pfsbetap",
        "li_rt": r"\pfsli",
        "wmhd_rt": r"\pfswmhd",
    }
    for name, node in signals.items():
        try:
            signal, timearray = params.mds_conn.get_data_with_dims(
                node, tree_name="pcs_east"
            )
            signal = interp1(
                timearray, signal, params.times, kind="linear", bounds_error=0
            )
            output[name] = signal
        except mdsExceptions.MdsException:
            output[name] = [np.nan]

    # Get q95_rt
    try:
        q95_rt, q95_rt_time = params.mds_conn.get_data_with_dims(
            r"\q95", tree_name="pefitrt_east"
        )
        # Deal with bug
        q95_rt_time, unique_indices = np.unique(q95_rt_time, return_index=True)
        q95_rt = q95_rt[unique_indices]
        q95_rt = interp1(
            q95_rt_time, q95_rt, params.times, kind="linear", bounds_error=0
        )
        output["q95_rt"] = q95_rt
    except mdsExceptions.MdsException:
        output["q95_rt"] = [np.nan]

    return output

get_pcs_power staticmethod ¤

get_pcs_power(params: PhysicsMethodParams)

Retrieve and calculate real-time power signals that are available in the EAST plasma control system (PCS).

PARAMETER DESCRIPTION
params

The parameters containing the MDS connection and shot information.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the following keys:

  • p_rad_rt: radiated power.
  • p_lh_RT: lower hybrid heating power.
  • p_nbi_RT: neutral beam injection heating power.
References
Source code in disruption_py/machine/east/physics.py
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
@staticmethod
@physics_method(columns=["p_rad_rt", "p_lh_rt", "p_nbi_rt"], tokamak=Tokamak.EAST)
def get_pcs_power(params: PhysicsMethodParams):
    """
    Retrieve and calculate real-time power signals that are available
    in the EAST plasma control system (PCS).

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the MDS connection and shot information.

    Returns
    -------
    dict
        A dictionary containing the following keys:

        - `p_rad_rt`: radiated power.
        - `p_lh_RT`: lower hybrid heating power.
        - `p_nbi_RT`: neutral beam injection heating power.

    References
    -------
    - original source: [get_pcs.m](https://github.com/MIT-PSFC/disruption-py
    /blob/matlab/EAST/get_pcs.m)
    """
    # Get p_rad_rt
    try:
        p_rad_rt, timearray = params.mds_conn.get_data_with_dims(
            r"\pcprad", tree_name="pcs_east"
        )
        p_rad_rt = interp1(
            timearray, p_rad_rt, params.times, kind="linear", bounds_error=0
        )
    except mdsExceptions.MdsException:
        p_rad_rt = [np.nan]

    # TODO: Verify the outputs, then modify get_heating_power() to make it compatible with this
    # Get p_nbi_rt
    nbi_nodes = {
        "i_nbil_rt": r"\pcnbi1li",
        "v_nbil_rt": r"\pcnbi1lv",
        "i_nbir_rt": r"\pcnbi1ri",
        "v_nbir_rt": r"\pcnbi1rv",
    }
    try:
        nbi_signals = {}
        for name, node in nbi_nodes.items():
            signal, timearray = params.mds_conn.get_data_with_dims(
                node, tree_name="pefitrt_east"
            )
            signal = interp1(
                timearray, signal, params.times, kind="linear", bounds_error=0
            )
            nbi_signals[name] = signal
        p_nbi_rt = (
            nbi_signals["i_nbil_rt"] * nbi_signals["v_nbil_rt"]
            + nbi_signals["i_nbir_rt"] * nbi_signals["v_nbir_rt"]
        )
    except mdsExceptions.MdsException:
        p_nbi_rt = [np.nan]

    # Get p_lh_rt
    lh_nodes = {
        "p_lh_46_inj_rt": r"\pcplhi",
        "p_lh_46_ref_rt": r"\pcplhr",
        "p_lh_245_inj_rt": r"\pcplhi2",
        "p_lh_245_ref_rt": r"\pcplhr2",
    }
    try:
        lh_signals = {}
        for name, node in lh_nodes.items():
            signal, timearray = params.mds_conn.get_data_with_dims(
                node, tree_name="pefitrt_east"
            )
            signal = interp1(
                timearray, signal, params.times, kind="linear", bounds_error=0
            )
            lh_signals[name] = signal
        p_lh_rt = (
            lh_signals["p_lh_46_inj_rt"]
            - lh_signals["p_lh_46_ref_rt"]
            + lh_signals["p_lh_245_inj_rt"]
            - lh_signals["p_lh_245_ref_rt"]
        )
    except mdsExceptions.MdsException:
        p_lh_rt = [np.nan]

    # Q.P. Yuan:
    # There is no signals from ICRF or ECRH connected to PCS. And I checked the
    # signals for NBI, there is no effective value, just noise. We will make
    # the NBI signals available in this EAST campaign. The PLHI2 and PLHR2 for
    # 2.45G LHW system have signals but are not calibrated yet.

    # Get p_ohm
    # "Note, I'm not bothering to calculate a real time version" -- Whoever who wrote this
    # p_ohm = EastPhysicsMethods.get_p_ohm(params)['p_ohm']

    # Compute total power and radiation fractions
    # p_input_rt = p_ohm + p_lh_rt + p_icrf_rt + p_ecrh_rt + p_nbi_rt
    # rad_input_frac_rt = p_rad_rt / p_input_rt
    # rad_loss_frac_rt = p_rad_rt / (p_input_rt - dwmhd_dt)

    return {
        "p_rad_rt": p_rad_rt,
        "p_lh_rt": p_lh_rt,
        "p_nbi_rt": p_nbi_rt,
    }

get_pkappa_area staticmethod ¤

get_pkappa_area(params: PhysicsMethodParams)

Calculate the plasma's ellipticity (kappa) using the area and minor radius data from the P-EFIT tree. kappa_area is defined as:

\[ \kappa_{area} = \frac{A}{\pi a^2} \]

where \(A\) is the plasma cross-sectional area and \(a\) is the minor radius.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the pkappa_area signal.

References
Source code in disruption_py/machine/east/physics.py
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
@staticmethod
@physics_method(columns=["pkappa_area"], tokamak=Tokamak.EAST)
def get_pkappa_area(params: PhysicsMethodParams):
    r"""
    Calculate the plasma's ellipticity (*kappa*) using the area and minor
    radius data from the P-EFIT tree. `kappa_area` is defined as:

    $$
    \kappa_{area} = \frac{A}{\pi a^2}
    $$

    where $A$ is the plasma cross-sectional area and $a$ is the minor radius.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the `pkappa_area` signal.

    References
    -------
    - original source: [get_PEFIT_parameters.m](https://github.com/MIT-PSFC/
    disruption-py/blob/matlab/EAST/get_PEFIT_parameters.m)
    """
    # Get area and aminor from EastEfitMethods
    pefit_params = EastEfitMethods.get_pefit_parameters(params=params)
    area = pefit_params["parea"]
    aminor = pefit_params["paminor"]
    # Compute kappa_area
    with np.errstate(divide="ignore", invalid="ignore"):
        kappa_area = area / (np.pi * aminor**2)

    return {"pkappa_area": kappa_area}

get_power staticmethod ¤

get_power(params: PhysicsMethodParams)

This routine gets the auxiliary heating powers: electron cyclotron resonance heating (p_ecrh), neutral beam injection system (p_nbi) ion cyclotron (p_icrf), and lower hybrid (p_lh). If any of the auxiliary heating powers were not available during a shot, then it returns an array of zeros for them.

This routine also calculates the total input power, the radiated input fraction, and the radiated loss function. These parameters are defined as: $$ p_\text{input} = p_\text{ohmic} + p_\text{lh} + p_\text{icrf} + p_\text{ecrh} + p_\text{nbi} $$ $$ f_\text{rad input} = \frac{p_\text{rad}}{p_\text{input}} $$ $$ f_\text{rad loss} = \frac{p_\text{rad}}{p_\text{rad} + dW_\text{mhd}/dt} $$

Note that currently we assume that the MDSplus trees always have the corresponding tree nodes for each of the powers, even if a heating source/diagnostic was turned off during that shot. If the routine fails to find a tree or a tree node, then it assumes that tree is broken and returns the corresponding power as an array of nans. The routine will also skip calculating p_input or the radiated fractions if any of its inputs is an array of nans.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the auxiliary heating powers (p_ecrh, p_lh, p_icrf, p_nbi), the total input power (p_input), and the radiated fractions (rad_input_frac, rad_loss_frac).

References
Source code in disruption_py/machine/east/physics.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
@staticmethod
@physics_method(
    columns=[
        "p_ecrh",
        "p_lh",
        "p_icrf",
        "p_nbi",
        "rad_input_frac",
        "rad_loss_frac",
        "p_input",
    ],
    tokamak=Tokamak.EAST,
)
def get_power(params: PhysicsMethodParams):
    r"""
    This routine gets the auxiliary heating powers: electron cyclotron
    resonance heating (`p_ecrh`), neutral beam injection system (`p_nbi`)
    ion cyclotron (`p_icrf`), and lower hybrid (`p_lh`). If any of the auxiliary
    heating powers were not available during a shot, then it returns an array
    of zeros for them.

    This routine also calculates the total input power, the radiated input
    fraction, and the radiated loss function. These parameters are defined as:
    $$
    p_\text{input} = p_\text{ohmic} + p_\text{lh} + p_\text{icrf} + p_\text{ecrh}
    + p_\text{nbi}
    $$
    $$
    f_\text{rad input} = \frac{p_\text{rad}}{p_\text{input}}
    $$
    $$
    f_\text{rad loss} = \frac{p_\text{rad}}{p_\text{rad} + dW_\text{mhd}/dt}
    $$

    Note that currently we assume that the MDSplus trees always have the corresponding
    tree nodes for each of the powers, even if a heating source/diagnostic was turned
    off during that shot. If the routine fails to find a tree or a tree node,
    then it assumes that tree is broken and returns the corresponding power as an array
    of `nans`. The routine will also skip calculating `p_input` or the radiated fractions if
    any of its inputs is an array of `nans`.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the auxiliary heating powers (`p_ecrh`, `p_lh`, `p_icrf`,
        `p_nbi`), the total input power (`p_input`), and the radiated fractions
        (`rad_input_frac`, `rad_loss_frac`).

    References
    -------
    - original source: [get_power.m](https://github.com/MIT-PSFC/disruption-py/blob/
    matlab/EAST/get_power.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411), #[451](https:
    //github.com/MIT-PSFC/disruption-py/pull/451)
    """
    p_ecrh = [np.nan]
    p_lh = [np.nan]
    p_icrf = [np.nan]
    p_nbi = [np.nan]
    rad_input_frac = [np.nan]
    rad_loss_frac = [np.nan]
    p_input = [np.nan]

    def get_heating_power(nodes, tree):
        """
        Helper function to pack p_lh, p_icrf, and p_nbi computations
        """
        heating_power = np.zeros(params.times.shape)
        for node in nodes:
            power_node, time_node = params.mds_conn.get_data_with_dims(
                node, tree_name=tree
            )
            heating_power += interp1(
                time_node,
                power_node,
                params.times,
                kind="linear",
                fill_value=0,
            )
        return heating_power

    # Get lower hybrid power

    # Note: the timebase for the LH power signal does not extend over the full
    # time span of the discharge.  Therefore, when interpolating the LH power
    # signal onto the "timebase" array, the LH signal has to be extrapolated
    # with zero values.  This is an option in the 'interp1' routine.  If the
    # extrapolation is not done, then the 'interp1' routine will assign NaN
    # (Not-a-Number) values for times outside the LH timebase, and the NaN's
    # will propagate into p_input, rad_input_frac, and rad_loss_frac, which is
    # not desirable.

    # TODO: check if the extrapolation method actually works
    lh_nodes = [
        r"\plhi1*1e3",  # LHW 2.45 GHz data
        r"\plhi2*1e3",  # LHW 4.66 GHz data
    ]
    try:
        p_lh = get_heating_power(lh_nodes, "east_1")  # [W]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get LH heating power")
        p_lh = [np.nan]

    # Get ECRH power
    try:
        p_ecrh, ecrh_time = params.mds_conn.get_data_with_dims(
            r"\pecrh1i*1e3", tree_name="analysis"
        )  # [W], [s]
        (baseline_indices,) = np.where(ecrh_time < 0)
        if len(baseline_indices) > 0:
            p_ecrh_baseline = np.mean(p_ecrh[baseline_indices])
            p_ecrh -= p_ecrh_baseline
        p_ecrh = interp1(
            ecrh_time, p_ecrh, params.times, kind="linear", fill_value=0
        )
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get ECRH heating power")
        p_ecrh = [np.nan]

    # Get ICRF power
    # p_ICRF = (p_icrfii - p_icrfir) + (p_icrfbi - p_icrfbr)
    icrf_nodes = [
        r"\picrfii*1e3",
        r"\picrfir*-1e3",
        r"\picrfbi*1e3",
        r"\picrfbr*-1e3",
    ]
    try:
        p_icrf = get_heating_power(icrf_nodes, "icrf_east")  # [W]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get ICRF heating power")
        p_icrf = [np.nan]

    # Get NBI power
    nbi_nodes = [
        r"\pnbi1lsource*1e3",
        r"\pnbi1rsource*1e3",
        r"\pnbi2lsource*1e3",
        r"\pnbi2rsource*1e3",
    ]
    try:
        p_nbi = get_heating_power(nbi_nodes, "nbi_east")  # [W]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get NBI heating power")
        p_nbi = [np.nan]

    # Get ohmic power
    p_ohm = EastPhysicsMethods.get_p_ohm(params)["p_oh"]  # [W]

    # Get radiated power
    p_rad = EastPhysicsMethods.get_radiated_power(params)["p_rad"]  # [W]

    # Calculate p_input
    if ~(
        np.isnan(p_ohm).all()
        or np.isnan(p_lh).all()
        or np.isnan(p_icrf).all()
        or np.isnan(p_ecrh).all()
        or np.isnan(p_nbi).all()
    ):
        p_input = p_ohm + p_lh + p_icrf + p_ecrh + p_nbi
    else:
        p_input = [np.nan]

    # Get Wmhd, calculate dWmhd_dt, and calculate p_loss
    try:
        wmhd, efittime = params.mds_conn.get_data_with_dims(
            r"\efit_aeqdsk:wmhd", tree_name="_efit_tree"
        )  # [W], [s]
        dwmhd_dt = np.gradient(wmhd, efittime)
        dwmhd_dt = interp1(efittime, dwmhd_dt, params.times)
        if not np.isnan(p_input).all():
            p_loss = p_input - dwmhd_dt
        else:
            p_loss = [np.nan]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get proper signals to compute p_loss")
        p_loss = [np.nan]

    rad_input_frac = np.full(len(params.times), np.nan)
    rad_loss_frac = np.full(len(params.times), np.nan)
    if ~(np.isnan(p_rad).all() or np.isnan(p_input).all()):
        (valid_indices,) = np.where((p_input != 0) & ~np.isnan(p_input))
        rad_input_frac[valid_indices] = (
            p_rad[valid_indices] / p_input[valid_indices]
        )
    if ~(np.isnan(p_rad).all() or np.isnan(p_loss).all()):
        (valid_indices,) = np.where((p_loss != 0) & ~np.isnan(p_loss))
        rad_loss_frac[valid_indices] = p_rad[valid_indices] / p_loss[valid_indices]

    output = {
        "p_ecrh": p_ecrh,
        "p_lh": p_lh,
        "p_icrf": p_icrf,
        "p_nbi": p_nbi,
        "rad_input_frac": rad_input_frac,
        "rad_loss_frac": rad_loss_frac,
        "p_input": p_input,
    }
    return output

get_prad_peaking staticmethod ¤

get_prad_peaking(params: PhysicsMethodParams)

Calculates the peaking factor of the radiated power measured by the AXUV arrays. It is defined as the ratio of the average of the centralmost 6 channels to the average of all of the non-divertor- viewing channels (core-to-average).

PARAMETER DESCRIPTION
params

The parameters containing the MDS connection and shot information.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the peaking factor for the radiated power profile (prad_peaking).

References
Source code in disruption_py/machine/east/physics.py
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
@staticmethod
@physics_method(columns=["prad_peaking"], tokamak=Tokamak.EAST)
def get_prad_peaking(params: PhysicsMethodParams):
    """
    Calculates the peaking factor of the radiated power measured by
    the AXUV arrays. It is defined as the ratio of the average of the
    centralmost 6 channels to the average of all of the non-divertor-
    viewing channels (core-to-average).

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the MDS connection and shot information.

    Returns
    -------
    dict
        A dictionary containing the peaking factor for the radiated
        power profile (`prad_peaking`).

    References
    -------
    - original source: [get_prad_peaking.m](https://github.com/MIT-PSFC/disr
    uption-py/blob/matlab/EAST/get_prad_peaking.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/
    411), #[451](https://github.com/MIT-PSFC/disruption-py/pull/451)
    """
    xuv, xuvtime = EastPhysicsMethods._get_raw_axuv_data(params)

    # Get calibration factors
    calib_factors = EastUtilMethods.get_axuv_calib_factors()
    # Apply calibration factors
    for i in range(64):
        # Original script has Del_r(ichan) which should be a mistake
        xuv[:, i] *= (
            calib_factors["Fac1"][i % 16]
            * calib_factors["Fac2"][i // 16][i % 16]
            * calib_factors["Fac3"][i // 16]
            * calib_factors["Fac4"]
            * 2
            * np.pi
            * calib_factors["Maj_R"]
            * calib_factors["Del_r"][i]
            # * calib_factors["Fac5"]
        )
    # Correction for bad channels (from Duan Yanmin's program)
    # get_radiated_power does these 2 lines first and then apply geometric correction.
    xuv[:, 11] = 0.5 * (xuv[:, 10] + xuv[:, 12])
    # xuv[:, 35] = 0.5 * (xuv[:, 34] + xuv[:, 35])

    # Define the core chords to be #28 to #37 (centermost 10 chords), and
    # define the non-divertor chords to be #09 to #56.  (Chords #1-8 view the
    # lower divertor region, and chords #57-64 view the upper divertor region.)
    ch_core = np.zeros(64)
    ch_core[29:35] = 1 / 6
    ch_all = np.zeros(64)
    ch_all[8:56] = 1 / 48

    prad_peaking = np.full((len(xuvtime)), np.nan)
    # TODO: find better way to do this for loop
    for itime in range(len(xuvtime)):
        prad_core = np.dot(xuv[itime, :], ch_core)
        prad_all = np.dot(xuv[itime, :], ch_all)
        prad_peaking[itime] = prad_core / prad_all

    # Interpret to the requested timebase
    prad_peaking = interp1(xuvtime, prad_peaking, params.times)

    return {"prad_peaking": prad_peaking}

get_radiated_power staticmethod ¤

get_radiated_power(params: PhysicsMethodParams)

Calculate total radiated power in bulk plasma measured by the AXUV arrays.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the total radiated power (p_rad).

References
  • original source: Prad_bulk_xuv2014_2016.m (currently not available in the repository)
  • pull requests: #411
Source code in disruption_py/machine/east/physics.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
@staticmethod
@physics_method(columns=["p_rad"], tokamak=Tokamak.EAST)
def get_radiated_power(params: PhysicsMethodParams):
    """
    Calculate total radiated power in bulk plasma measured by
    the AXUV arrays.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the total radiated power (`p_rad`).

    References
    -------
    - original source: Prad_bulk_xuv2014_2016.m (currently not available in the repository)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/411)
    """
    # Get the raw AXUV data
    try:
        xuv, xuvtime = EastPhysicsMethods._get_raw_axuv_data(params)  # [W], [s]
    except mdsExceptions.MdsException:
        params.logger.warning("Failed to get raw AXUS data")
        return {"p_rad": [np.nan]}

    # Get calibration factors
    calib_factors = EastUtilMethods.get_axuv_calib_factors()
    # Apply calibration factors Fac1 to Fac4
    for i in range(64):
        xuv[:, i] *= (
            calib_factors["Fac1"][i % 16]
            * calib_factors["Fac2"][i // 16][i % 16]
            * calib_factors["Fac3"][i // 16]
            * calib_factors["Fac4"]
        )
    # Correction for bad channels (from Duan Yanmin's program)
    # TODO: why not [35] = ([34]+[36])/2 when [35] is the bad channel?
    xuv[:, 11] = 0.5 * (xuv[:, 10] + xuv[:, 12])
    xuv[:, 35] = 0.5 * (xuv[:, 34] + xuv[:, 35])
    # Apply geometric calibrations and Fac5
    for i in range(64):
        xuv[:, i] *= (
            2
            * np.pi
            * calib_factors["Maj_R"]
            * calib_factors["Del_r"][i]
            * calib_factors["Fac5"]
        )

    # Subtract divertor measurements and overlapping measurments
    core_indices = (
        list(range(7, 15))
        + list(range(20, 32))
        + list(range(32, 48))
        + list(range(53, 59))
    )
    p_rad = np.nansum(xuv[:, core_indices], axis=1)
    # Interpolate to requested time base
    p_rad = interp1(xuvtime, p_rad, params.times, kind="linear", fill_value=0)
    return {"p_rad": p_rad}

get_time_until_disrupt staticmethod ¤

get_time_until_disrupt(params: PhysicsMethodParams)

Calculate the time until disruption.

Currently, the disruption time is queried from the DISRUPTIONS table in the SQL database of each machine. These disruption times were calculated using Robert Granetz's routine.

PARAMETER DESCRIPTION
params

The parameters containing the disruption information and times.

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary with a single key time_until_disrupt.

References
Source code in disruption_py/machine/east/physics.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@staticmethod
@physics_method(columns=["time_until_disrupt"], tokamak=Tokamak.EAST)
def get_time_until_disrupt(params: PhysicsMethodParams):
    """
    Calculate the time until disruption.

    Currently, the disruption time is queried from the `DISRUPTIONS` table
    in the SQL database of each machine. These disruption times were calculated
    using Robert Granetz's routine.

    Parameters
    ----------
    params : PhysicsMethodParams
        The parameters containing the disruption information and times.

    Returns
    -------
    dict
        A dictionary with a single key `time_until_disrupt`.

    References
    -------
    - issues: #[223](https://github.com/MIT-PSFC/disruption-py/issues/223)
    """
    if params.disrupted:
        return {"time_until_disrupt": params.disruption_time - params.times}
    return {"time_until_disrupt": [np.nan]}

get_v_loop staticmethod ¤

get_v_loop(params: PhysicsMethodParams)

retrieve the loop voltage signal.

By default, this routine gets the loop voltage from \vp1_s from the east tree. This signal is derived by taking the time derivative of a flux loop near the inboard midplane. If \vp1_s isn't available, the method will fall back to reading \pcvloop from the pcs_east tree instead.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the loop voltage (v_loop).

References
Source code in disruption_py/machine/east/physics.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
@staticmethod
@physics_method(columns=["v_loop"], tokamak=Tokamak.EAST)
def get_v_loop(params: PhysicsMethodParams):
    r"""
    retrieve the loop voltage signal.

    By default, this routine gets the loop voltage from `\vp1_s` from the `east` tree.
    This signal is derived by taking the time derivative of a flux loop
    near the inboard midplane. If `\vp1_s` isn't available, the method will fall back
    to reading `\pcvloop` from the `pcs_east` tree instead.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the loop voltage (`v_loop`).

    References
    -------
    - original source: [get_v_loop.m](https://github.com/MIT-PSFC/disruption-py
    /blob/matlab/EAST/get_v_loop.m)
    - pull requests: #[411](https://github.com/MIT-PSFC/disruption-py/pull/
    411), #[451](https://github.com/MIT-PSFC/disruption-py/pull/451)
    """
    v_loop = [np.nan]

    # Get "\vp1_s" signal from the EAST tree.  (This signal is a sub-sampled
    # version of "vp1".)
    try:
        v_loop, v_loop_time = params.mds_conn.get_data_with_dims(
            r"\vp1_s", tree_name="east"
        )
    except mdsExceptions.MdsException:
        params.logger.verbose(
            r"v_loop: Failed to get \vp1_s data. Use \pcvloop from pcs_east instead."
        )
        v_loop, v_loop_time = params.mds_conn.get_data_with_dims(
            r"\pcvloop", tree_name="pcs_east"
        )  # [V]

    # Interpolate the signal onto the requested timebase
    v_loop = interp1(v_loop_time, v_loop, params.times)

    return {"v_loop": v_loop}

get_z_error staticmethod ¤

get_z_error(params: PhysicsMethodParams)

Retrieve the programmed and measured vertical positions of the plasma current centroid, then calculate the control error.

This routine uses two methods to compute z_error. The first and original method gets the z-centroid from \zcur which is calculated by EFIT. The second method uses the z-centroid from the \lmsz signal which is calculated by the plasma control system (PCS). For the lmsz-derived signals, this routine also returns their normalized version by dividing them by the plasma minor radius.

PARAMETER DESCRIPTION
params

Parameters containing MDS connection and shot information

TYPE: PhysicsMethodParams

RETURNS DESCRIPTION
dict

A dictionary containing the programmed vertical position (z_prog), the measured vertical positions (zcur and zcur_lmsz), the errors (z_error and z_error_lmsz), and the normalized lmsz-derived signals (zcur_lmsz_normalized and z_error_lmsz_normalized).

References
Source code in disruption_py/machine/east/physics.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
@staticmethod
@physics_method(
    columns=[
        "zcur",
        "z_prog",
        "z_error",
        "zcur_lmsz",
        "z_error_lmsz",
        "zcur_lmsz_normalized",
        "z_error_lmsz_normalized",
    ],
    tokamak=Tokamak.EAST,
)
def get_z_error(params: PhysicsMethodParams):
    r"""
    Retrieve the programmed and measured vertical positions of the plasma current
    centroid, then calculate the control error.

    This routine uses two methods to compute `z_error`. The first and original method
    gets the z-centroid from `\zcur` which is calculated by EFIT. The second method uses
    the z-centroid from the `\lmsz` signal which is calculated by the plasma control
    system (PCS). For the *lmsz*-derived signals, this routine also returns their normalized
    version by dividing them by the plasma minor radius.

    Parameters
    ----------
    params : PhysicsMethodParams
        Parameters containing MDS connection and shot information

    Returns
    -------
    dict
        A dictionary containing the programmed vertical position (`z_prog`), the measured
        vertical positions (`zcur` and `zcur_lmsz)`, the errors (`z_error` and
        `z_error_lmsz`), and the normalized *lmsz*-derived signals (`zcur_lmsz_normalized`
        and `z_error_lmsz_normalized`).

    References
    -------
    - original source: [get_Z_error.m](https://github.com/MIT-PSFC/disruption-py/
    blob/matlab/EAST/get_Z_error.m)
    """
    # Read in the calculated zcur from EFIT
    zcur, zcur_time = params.mds_conn.get_data_with_dims(
        r"\efit_aeqdsk:zcur", tree_name="_efit_tree"
    )  # [A], [s]
    # Deal with rare bug
    zcur_time, unique_indices = np.unique(zcur_time, return_index=True)
    zcur = zcur[unique_indices]

    # Read in aminor from EFIT
    # TODO: use \aminor or \aout? -- MATLAB: \aminor
    aminor = params.mds_conn.get_data(r"\aminor", tree_name="_efit_tree")  # [m]
    aminor = aminor[unique_indices]

    # Next, get the programmed/requested/target Z from PCS, and the
    # calculated Z-centroid from PCS
    z_prog, z_prog_time = params.mds_conn.get_data_with_dims(
        r"\lmtzref/100", tree_name="pcs_east"
    )  # [m], [s] (Node says 'm' but it's wrong)
    zcur_lmsz, lmsz_time = params.mds_conn.get_data_with_dims(
        r"\lmsz", tree_name="pcs_east"
    )  # [m], [s]

    # Interpolate all retrieved signals to the requested timebase
    zcur = interp1(zcur_time, zcur, params.times)
    aminor = interp1(zcur_time, aminor, params.times)
    z_prog = interp1(z_prog_time, z_prog, params.times)
    zcur_lmsz = interp1(lmsz_time, zcur_lmsz, params.times)

    # Calculate zcur_lmsz_normalized
    zcur_lmsz_normalized = zcur_lmsz / aminor

    # Calculate both versions of z_error and z_error_lmsz_normalized
    z_error = zcur - z_prog
    z_error_lmsz = zcur_lmsz - z_prog
    z_error_lmsz_normalized = z_error_lmsz / aminor

    return {
        "zcur": zcur,
        "z_prog": z_prog,
        "z_error": z_error,
        "zcur_lmsz": zcur_lmsz,
        "z_error_lmsz": z_error_lmsz,
        "zcur_lmsz_normalized": zcur_lmsz_normalized,
        "z_error_lmsz_normalized": z_error_lmsz_normalized,
    }