!pip install matplotlib s3fs "xarray[io]"

Hide code cell output

Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: matplotlib in /home/runner/.local/lib/python3.12/site-packages (3.11.2)
Collecting s3fs
  Downloading s3fs-2026.9.0-py3-none-any.whl.metadata (1.2 kB)
Requirement already satisfied: xarray[io] in /home/runner/.local/lib/python3.12/site-packages (2026.9.0)
Requirement already satisfied: contourpy>=1.0.1 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (1.4.0)
Requirement already satisfied: cycler>=0.10 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.28.2 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (4.67.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (1.5.1)
Requirement already satisfied: numpy>=1.25 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (2.5.4)
Requirement already satisfied: packaging>=20.0 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (26.3)
Requirement already satisfied: pillow>=9 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (13.0.0)
Requirement already satisfied: pyparsing>=3 in /usr/lib/python3/dist-packages (from matplotlib) (3.1.1)
Requirement already satisfied: python-dateutil>=2.7 in /home/runner/.local/lib/python3.12/site-packages (from matplotlib) (3.0.0)
Collecting aiobotocore<4.0.0,>=2.19.0 (from s3fs)
  Downloading aiobotocore-3.10.0-py3-none-any.whl.metadata (32 kB)
Requirement already satisfied: fsspec<2026.9.1,>=2026.9.0 in /home/runner/.local/lib/python3.12/site-packages (from s3fs) (2026.9.0)
Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0 in /home/runner/.local/lib/python3.12/site-packages (from s3fs) (3.14.5)
Collecting aioitertools<1.0.0,>=0.5.1 (from aiobotocore<4.0.0,>=2.19.0->s3fs)
  Downloading aioitertools-0.13.0-py3-none-any.whl.metadata (3.3 kB)
Collecting botocore<1.43.118,>=1.43.115 (from aiobotocore<4.0.0,>=2.19.0->s3fs)
  Downloading botocore-1.43.117-py3-none-any.whl.metadata (5.6 kB)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /usr/lib/python3/dist-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs) (1.0.1)
Requirement already satisfied: multidict<8.0.0,>=6.0.0 in /home/runner/.local/lib/python3.12/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs) (7.2.1)
Collecting python-dateutil>=2.7 (from matplotlib)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting wrapt<3.0.0,>=1.10.10 (from aiobotocore<4.0.0,>=2.19.0->s3fs)
  Downloading wrapt-2.5.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (7.6 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (2.7.1)
Requirement already satisfied: aiosignal>=1.4.0 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (1.4.0)
Requirement already satisfied: attrs>=17.3.0 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (26.1.0)
Requirement already satisfied: frozenlist>=1.1.1 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (1.8.0)
Requirement already satisfied: propcache>=0.2.0 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (0.5.4)
Requirement already satisfied: typing_extensions>=4.4 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (4.16.0)
Requirement already satisfied: yarl<2.0,>=1.25.1 in /home/runner/.local/lib/python3.12/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (1.25.1)
Requirement already satisfied: urllib3!=2.2.0,<3,>=1.25.4 in /usr/lib/python3/dist-packages (from botocore<1.43.118,>=1.43.115->aiobotocore<4.0.0,>=2.19.0->s3fs) (2.0.7)
Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)
Requirement already satisfied: idna>=2.0 in /usr/lib/python3/dist-packages (from yarl<2.0,>=1.25.1->aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs) (3.6)
Requirement already satisfied: pandas>=2.2 in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (3.1.0)
Requirement already satisfied: netCDF4>=1.6.0 in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (1.7.5.1.post1)
Requirement already satisfied: h5netcdf>=1.8.0 in /home/runner/.local/lib/python3.12/site-packages (from h5netcdf[h5py]>=1.8.0; extra == "io"->xarray[io]) (1.8.1)
Requirement already satisfied: pydap in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (3.5.11)
Requirement already satisfied: scipy>=1.15 in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (1.18.1)
Requirement already satisfied: zarr>=3.0 in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (3.4.1)
Requirement already satisfied: cftime in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (1.6.6.1)
Requirement already satisfied: pooch in /home/runner/.local/lib/python3.12/site-packages (from xarray[io]) (1.9.0)
Requirement already satisfied: h5py in /home/runner/.local/lib/python3.12/site-packages (from h5netcdf[h5py]>=1.8.0; extra == "io"->xarray[io]) (3.16.0)
Requirement already satisfied: certifi in /usr/lib/python3/dist-packages (from netCDF4>=1.6.0->xarray[io]) (2023.11.17)
Requirement already satisfied: donfig>=0.8 in /home/runner/.local/lib/python3.12/site-packages (from zarr>=3.0->xarray[io]) (0.8.1.post1)
Requirement already satisfied: google-crc32c>=1.5 in /home/runner/.local/lib/python3.12/site-packages (from zarr>=3.0->xarray[io]) (1.9.0)
Requirement already satisfied: msgspec>=0.19 in /home/runner/.local/lib/python3.12/site-packages (from zarr>=3.0->xarray[io]) (0.22.0)
Requirement already satisfied: numcodecs>=0.16 in /home/runner/.local/lib/python3.12/site-packages (from zarr>=3.0->xarray[io]) (0.17.1)
Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (from donfig>=0.8->zarr>=3.0->xarray[io]) (6.0.1)
Requirement already satisfied: platformdirs>=2.5.0 in /usr/local/lib/python3.12/dist-packages (from pooch->xarray[io]) (4.13.0)
Requirement already satisfied: requests>=2.19.0 in /usr/lib/python3/dist-packages (from pooch->xarray[io]) (2.31.0)
Requirement already satisfied: requests-cache in /home/runner/.local/lib/python3.12/site-packages (from pydap->xarray[io]) (1.3.3)
Requirement already satisfied: webob>=1.8.10 in /home/runner/.local/lib/python3.12/site-packages (from pydap->xarray[io]) (1.8.11)
Requirement already satisfied: beautifulsoup4 in /home/runner/.local/lib/python3.12/site-packages (from pydap->xarray[io]) (4.15.0)
Requirement already satisfied: lxml in /home/runner/.local/lib/python3.12/site-packages (from pydap->xarray[io]) (6.1.3)
Requirement already satisfied: Jinja2 in /usr/lib/python3/dist-packages (from pydap->xarray[io]) (3.1.2)
Requirement already satisfied: soupsieve>=1.6.1 in /home/runner/.local/lib/python3.12/site-packages (from beautifulsoup4->pydap->xarray[io]) (3.0.4)
Requirement already satisfied: cattrs>=22.2 in /home/runner/.local/lib/python3.12/site-packages (from requests-cache->pydap->xarray[io]) (26.2.1)
Requirement already satisfied: url-normalize>=2.0 in /home/runner/.local/lib/python3.12/site-packages (from requests-cache->pydap->xarray[io]) (3.0.1)
Downloading s3fs-2026.9.0-py3-none-any.whl (33 kB)
Downloading aiobotocore-3.10.0-py3-none-any.whl (102 kB)
Downloading aioitertools-0.13.0-py3-none-any.whl (24 kB)
Downloading botocore-1.43.117-py3-none-any.whl (16.1 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/16.1 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 16.0/16.1 MB 130.5 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.1/16.1 MB 80.1 MB/s  0:00:00
?25hDownloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading wrapt-2.5.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (250 kB)
Installing collected packages: wrapt, python-dateutil, aioitertools, botocore, aiobotocore, s3fs
?25l
  Attempting uninstall: python-dateutil
    Found existing installation: python-dateutil 3.0.0
    Uninstalling python-dateutil-3.0.0:
      Successfully uninstalled python-dateutil-3.0.0
?25h
Successfully installed aiobotocore-3.10.0 aioitertools-0.13.0 botocore-1.43.117 python-dateutil-2.9.0.post0 s3fs-2026.9.0 wrapt-2.5.1
import zarr
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from scipy.signal import stft

Level 2 Data#

In this notebook we demonstrate the variety of different data available in the FAIR MAST dataset. In this example we are using level 2 MAST data, which includes cropping, interpolation, calibration, etc. of each signal, as well as mapping each diagnostic group try and follow IMAS naming convetions. The level 2 data is well-indexed data and follows the FAIR principles. Shots are also filtered using the plasma current to remove shots which were only used for testing, comissioning, machine calibration etc.

First we need to connect to the remote S3 storage bucket to access the data. Each shot from MAST is stored as a seperate Zarr file.

Using fsspec and xarray we can remotely read data directly over the web. In the example below we also turn on local file caching, allowing us to avoid reading over the network multiple times.

shot_id = 30421

endpoint_url = 'https://s3.echo.stfc.ac.uk'
url = f's3://mast/level2/shots/{shot_id}.zarr'

# Get a handle to the remote file
store = zarr.storage.FsspecStore.from_url(
    url,
    storage_options=dict(
        protocol='simplecache',
        target_protocol="s3",
        cache_storage='.cache',
        target_options=dict(
            anon=True, endpoint_url=endpoint_url, asynchronous=True
        )
    )
)

Summary Profiles#

The summary group provides a collection of general physics quantities for an experiment.

Hide code cell source

def plot_1d_profiles(profiles: xr.Dataset):
    """Helper function for plotting 1D profiles"""
    names = list(profiles.data_vars)
    n = int(np.ceil(len(names) / 2))
    fig, axes = plt.subplots(n, 2, figsize=(10, 2*n))
    axes = axes.flatten()

    for i, name in enumerate(names):
        profiles[name].plot(x='time', ax=axes[i])
        axes[i].grid('on', alpha=0.5)
        axes[i].set_xlim(profiles.time.min(), profiles.time.max())

    # drop any unused trailing axes (e.g. when there is an odd number of profiles)
    for ax in axes[len(names):]:
        fig.delaxes(ax)

    plt.tight_layout()
profiles = xr.open_zarr(store, group='summary')

plot_1d_profiles(profiles)
profiles
<xarray.Dataset> Size: 49kB
Dimensions:              (time: 767)
Coordinates:
  * time                 (time) float64 6kB -0.1 -0.099 -0.098 ... 0.665 0.666
    shot_id              int64 8B ...
Data variables:
    greenwald_density    (time) float64 6kB ...
    ip                   (time) float64 6kB ...
    line_average_n_e     (time) float64 6kB ...
    neutron_rates_total  (time) float64 6kB ...
    power_nbi            (time) float64 6kB ...
    power_ohm            (time) float64 6kB ...
    power_radiated       (time) float64 6kB ...
Attributes:
    name:          summary
    description:   Summary of physics quantities from a simulation or an expe...
    imas:          summary
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:10:53Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/a76e56a6a01635e917f9a207e43235e214143fa30b5bb498604e6b39a7916701.png

Pulse Schedule#

profiles = xr.open_zarr(store, group='pulse_schedule')

fig, axes = plt.subplots(2, 1, figsize=(10, 5))
axes = axes.flatten()
profiles['i_plasma'].plot(x='time', ax=axes[0])
profiles['n_e_line'].plot(x='time', ax=axes[1])


for ax in axes:
    ax.grid('on', alpha=0.5)
plt.tight_layout()

profiles
<xarray.Dataset> Size: 490kB
Dimensions:   (time: 15305)
Coordinates:
  * time      (time) float64 122kB -0.1 -0.09995 -0.0999 ... 0.6652 0.6652
    shot_id   int64 8B ...
Data variables:
    i_plasma  (time) float64 122kB ...
    n_e_line  (time) float64 122kB ...
    z_ref     (time) float64 122kB ...
Attributes:
    name:          pulse_schedule
    description:   Description of Pulse Schedule, described by subsystems wav...
    imas:          pulse_schedule
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:10:51Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/b3863da6e102dbd5b647ac0a96a0032c33a6033bb5e83a7092b1a915e388c2d0.png

Magnetics#

Magnetic diagnostics for equilibrium identification and plasma shape control.

profiles = xr.open_zarr(store, group='magnetics')

fig, axes = plt.subplots(4, 3, figsize=(8, 10))
axes = axes.flatten()

profiles['b_field_pol_probe_ccbv_field'].plot.line(x='time', ax=axes[0], add_legend=False)
profiles['b_field_pol_probe_obv_field'].plot.line(x='time', ax=axes[1], add_legend=False)
profiles['b_field_pol_probe_obr_field'].plot.line(x='time', ax=axes[2], add_legend=False)


profiles['b_field_pol_probe_omv_voltage'].plot.line(x='time_mirnov', ax=axes[3], add_legend=False)
profiles['b_field_pol_probe_cc_field'].plot.line(x='time_mirnov', ax=axes[4], add_legend=False)
profiles['b_field_tor_probe_cc_field'].plot.line(x='time_mirnov', ax=axes[5], add_legend=False)

profiles['b_field_tor_probe_saddle_field'].plot.line(x='time_saddle', ax=axes[6], add_legend=False)
profiles['b_field_tor_probe_saddle_voltage'].plot.line(x='time_saddle', ax=axes[7], add_legend=False)
profiles['b_field_tor_probe_omaha_voltage'].plot.line(x='time_omaha', ax=axes[8], add_legend=False)

profiles['flux_loop_flux'].plot.line(x='time', ax=axes[9], add_legend=False)
profiles['ip'].plot.line(x='time', ax=axes[10], add_legend=False)

for ax in axes[:11]:
    ax.grid('on', alpha=0.5)

# remove the unused 12th axis in the 4x3 grid
fig.delaxes(axes[11])

plt.tight_layout()

profiles
<xarray.Dataset> Size: 109MB
Dimensions:                                      (
                                                  b_field_pol_probe_cc_channel: 5,
                                                  time_mirnov: 382601,
                                                  b_field_pol_probe_cc_geometry_channel: 40,
                                                  b_field_pol_probe_ccbv_channel: 40,
                                                  time: 3827,
                                                  ...
                                                  coordinate: 28,
                                                  b_field_tor_probe_saddle_m_geometry_channel: 12,
                                                  b_field_tor_probe_saddle_u_geometry_channel: 12,
                                                  b_field_tor_probe_saddle_voltage_channel: 12,
                                                  flux_loop_channel: 15,
                                                  flux_loop_geometry_channel: 46)
Coordinates: (12/26)
  * b_field_pol_probe_cc_channel                 (b_field_pol_probe_cc_channel) StringDType() 80B ...
  * time_mirnov                                  (time_mirnov) float64 3MB -0...
  * b_field_pol_probe_cc_geometry_channel        (b_field_pol_probe_cc_geometry_channel) StringDType() 640B ...
  * b_field_pol_probe_ccbv_channel               (b_field_pol_probe_ccbv_channel) StringDType() 640B ...
  * time                                         (time) float64 31kB -0.1 ......
  * b_field_pol_probe_ccbv_geometry_channel      (b_field_pol_probe_ccbv_geometry_channel) StringDType() 640B ...
    ...                                           ...
  * b_field_tor_probe_saddle_m_geometry_channel  (b_field_tor_probe_saddle_m_geometry_channel) StringDType() 192B ...
  * b_field_tor_probe_saddle_u_geometry_channel  (b_field_tor_probe_saddle_u_geometry_channel) StringDType() 192B ...
  * b_field_tor_probe_saddle_voltage_channel     (b_field_tor_probe_saddle_voltage_channel) StringDType() 192B ...
  * flux_loop_channel                            (flux_loop_channel) StringDType() 240B ...
  * flux_loop_geometry_channel                   (flux_loop_geometry_channel) StringDType() 736B ...
    shot_id                                      int64 8B 30421
Data variables: (12/46)
    b_field_pol_probe_cc_field                   (b_field_pol_probe_cc_channel, time_mirnov) float64 15MB ...
    b_field_pol_probe_cc_phi                     (b_field_pol_probe_cc_geometry_channel) float64 320B ...
    b_field_pol_probe_cc_r                       (b_field_pol_probe_cc_geometry_channel) float64 320B ...
    b_field_pol_probe_cc_z                       (b_field_pol_probe_cc_geometry_channel) float64 320B ...
    b_field_pol_probe_ccbv_field                 (b_field_pol_probe_ccbv_channel, time) float64 1MB ...
    b_field_pol_probe_ccbv_length                (b_field_pol_probe_ccbv_geometry_channel) float64 320B ...
    ...                                           ...
    b_field_tor_probe_saddle_u_z                 (b_field_tor_probe_saddle_u_geometry_channel, coordinate) float64 3kB ...
    b_field_tor_probe_saddle_voltage             (b_field_tor_probe_saddle_voltage_channel, time_saddle) float64 4MB ...
    flux_loop_flux                               (flux_loop_channel, time) float64 459kB ...
    flux_loop_r                                  (flux_loop_geometry_channel) float64 368B ...
    flux_loop_z                                  (flux_loop_geometry_channel) float64 368B ...
    ip                                           (time) float64 31kB -864.2 ....
Attributes:
    name:          magnetics
    description:   Magnetic diagnostics for equilibrium identification and pl...
    imas:          magnetics
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:15:15Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/5d4b457b00509c92589b9b53d2a35bc9d0c679490b98176467e39e8cf1125879.png

Looking at the spectrogram of one of the mirnov coils can show us information about the MHD modes. Here we see several mode instabilities occuring before the plasma is lost.

ds = profiles['b_field_pol_probe_omv_voltage'].isel(b_field_pol_probe_omv_channel=1)
# Parameters to limit the number of frequencies
nperseg = 2000  # Number of points per segment
nfft = 2000  # Number of FFT points

# Compute the Short-Time Fourier Transform (STFT)
sample_rate = 1/(ds.time_mirnov[1] - ds.time_mirnov[0])
f, t, Zxx = stft(ds, fs=int(sample_rate), nperseg=nperseg, nfft=nfft)

fig, ax = plt.subplots(figsize=(15, 5))
cax = ax.pcolormesh(t, f/1000, np.abs(Zxx), shading='nearest', cmap='jet', norm=LogNorm(vmin=1e-5))
ax.set_ylim(0, 50)
ax.set_title(f'Shot {shot_id}')
ax.set_ylabel('Frequency [Hz]')
ax.set_xlabel('Time [sec]')
plt.colorbar(cax, ax=ax)
plt.show()
_images/62726f69773e127373178c4eac0edcffb55d1b049588fb786cc46b86030c00c5.png

Spectrometer Visible#

Spectrometer in visible light range diagnostic

profiles = xr.open_zarr(store, group='spectrometer_visible')
profiles['filter_spectrometer_dalpha_voltage'].plot.line(x='time')
profiles['filter_spectrometer_bes_voltage'].isel(bes_channel=0).plot.line(x='time_bes')
profiles
<xarray.Dataset> Size: 358MB
Dimensions:                             (time: 33761, bes_channel: 32,
                                         time_bes: 1350401, dalpha_channel: 3)
Coordinates:
  * time                                (time) float64 270kB -0.01 ... 0.6652
  * bes_channel                         (bes_channel) StringDType() 512B 'cha...
  * time_bes                            (time_bes) float64 11MB -0.01 ... 0.6652
  * dalpha_channel                      (dalpha_channel) StringDType() 48B 'H...
    shot_id                             int64 8B 30421
Data variables:
    density_gradient                    (time) float64 270kB ...
    filter_spectrometer_bes_voltage     (bes_channel, time_bes) float64 346MB ...
    filter_spectrometer_dalpha_voltage  (dalpha_channel, time) float64 810kB ...
Attributes:
    name:          spectrometer_visible
    description:   Spectrometer in visible light range diagnostic
    imas:          spectrometer_visible
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:19:29Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/481f34e25f7ed8695afa9e93cf1dd48de3d2b962afa58dc3bde5aa634ea9db42.png

PF Active#

profiles = xr.open_zarr(store, group='pf_active')
fig, axes = plt.subplots(2, 1, figsize=(10, 6))

# coil_current includes the central solenoid (the 'SOL' channel), which
# carries the largest current; the legend identifies each coil channel.
profiles['coil_current'].plot.line(x='time', ax=axes[0])
profiles['coil_voltage'].plot.line(x='time', ax=axes[1])

plt.tight_layout()
profiles
<xarray.Dataset> Size: 2MB
Dimensions:                            (current_channel: 13, time: 15305,
                                        voltage_channel: 5,
                                        p2_inner_lower_coordinate_element: 12,
                                        p2_inner_upper_coordinate_element: 12,
                                        p2_outer_lower_coordinate_element: 8,
                                        p2_outer_upper_coordinate_element: 8,
                                        ...
                                        p4_upper_coordinate_element: 23,
                                        p5_lower_coordinate_element: 23,
                                        p5_upper_coordinate_element: 23,
                                        p6_lower_coordinate_element: 4,
                                        p6_upper_coordinate_element: 4,
                                        sol_coordinate_element: 656)
Coordinates: (12/17)
  * current_channel                    (current_channel) StringDType() 208B '...
  * time                               (time) float64 122kB -0.1 ... 0.6652
  * voltage_channel                    (voltage_channel) StringDType() 80B '/...
  * p2_inner_lower_coordinate_element  (p2_inner_lower_coordinate_element) StringDType() 192B ...
  * p2_inner_upper_coordinate_element  (p2_inner_upper_coordinate_element) StringDType() 192B ...
  * p2_outer_lower_coordinate_element  (p2_outer_lower_coordinate_element) StringDType() 128B ...
    ...                                 ...
  * p5_lower_coordinate_element        (p5_lower_coordinate_element) StringDType() 368B ...
  * p5_upper_coordinate_element        (p5_upper_coordinate_element) StringDType() 368B ...
  * p6_lower_coordinate_element        (p6_lower_coordinate_element) StringDType() 64B ...
  * p6_upper_coordinate_element        (p6_upper_coordinate_element) StringDType() 64B ...
  * sol_coordinate_element             (sol_coordinate_element) StringDType() 10kB ...
    shot_id                            int64 8B 30421
Data variables: (12/54)
    coil_current                       (current_channel, time) float64 2MB ...
    coil_voltage                       (voltage_channel, time) float64 612kB ...
    p2_inner_lower_height              (p2_inner_lower_coordinate_element) float32 48B ...
    p2_inner_lower_r                   (p2_inner_lower_coordinate_element) float32 48B ...
    p2_inner_lower_width               (p2_inner_lower_coordinate_element) float32 48B ...
    p2_inner_lower_z                   (p2_inner_lower_coordinate_element) float32 48B ...
    ...                                 ...
    p6_upper_width                     (p6_upper_coordinate_element) float32 16B ...
    p6_upper_z                         (p6_upper_coordinate_element) float32 16B ...
    sol_height                         (sol_coordinate_element) float32 3kB ...
    sol_r                              (sol_coordinate_element) float32 3kB ...
    sol_width                          (sol_coordinate_element) float32 3kB ...
    sol_z                              (sol_coordinate_element) float32 3kB ...
Attributes:
    name:          pf_active
    description:   
    imas:          pf_active
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:11:57Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/e6bdc8f209e3d7a421ce391aaf8e9ebd1e57263ec040c0f2acd6a5ff94e23976.png

Soft X-rays#

profiles = xr.open_zarr(store, group='soft_x_rays')
fig, axes = plt.subplots(3, 1)


profiles['horizontal_cam_lower'].plot.line(x='time', ax=axes[1], add_legend=False)
axes[1].set_ylim(0, 0.02)

profiles['horizontal_cam_upper'].plot.line(x='time', ax=axes[2], add_legend=False)
axes[2].set_ylim(0, 0.02)

if "tangential_cam" in profiles:
    profiles['tangential_cam'].plot.line(x='time', ax=axes[0], add_legend=False)
    axes[0].set_ylim(0, 0.2)

plt.tight_layout()
profiles
<xarray.Dataset> Size: 168MB
Dimensions:                                (horizontal_cam_lower_channel: 18,
                                            time: 382601,
                                            horizontal_cam_lower_geometry_channel: 18,
                                            horizontal_cam_upper_channel: 18,
                                            horizontal_cam_upper_geometry_channel: 18,
                                            inner_vertical_cam_geometry_channel: 12,
                                            outer_vertical_cam_geometry_channel: 12,
                                            tangential_cam_channel: 18,
                                            tangential_cam_geometry_channel: 18,
                                            third_horizontal_cam_geometry_channel: 18)
Coordinates:
  * horizontal_cam_lower_channel           (horizontal_cam_lower_channel) StringDType() 288B ...
  * time                                   (time) float64 3MB -0.1 ... 0.6652
  * horizontal_cam_lower_geometry_channel  (horizontal_cam_lower_geometry_channel) StringDType() 288B ...
  * horizontal_cam_upper_channel           (horizontal_cam_upper_channel) StringDType() 288B ...
  * horizontal_cam_upper_geometry_channel  (horizontal_cam_upper_geometry_channel) StringDType() 288B ...
  * inner_vertical_cam_geometry_channel    (inner_vertical_cam_geometry_channel) StringDType() 192B ...
  * outer_vertical_cam_geometry_channel    (outer_vertical_cam_geometry_channel) StringDType() 192B ...
  * tangential_cam_channel                 (tangential_cam_channel) StringDType() 288B ...
  * tangential_cam_geometry_channel        (tangential_cam_geometry_channel) StringDType() 288B ...
  * third_horizontal_cam_geometry_channel  (third_horizontal_cam_geometry_channel) StringDType() 288B ...
    shot_id                                int64 8B 30421
Data variables: (12/33)
    horizontal_cam_lower                   (horizontal_cam_lower_channel, time) float64 55MB ...
    horizontal_cam_lower_endpoint_r        (horizontal_cam_lower_geometry_channel) float64 144B ...
    horizontal_cam_lower_endpoint_z        (horizontal_cam_lower_geometry_channel) float64 144B ...
    horizontal_cam_lower_origin_r          (horizontal_cam_lower_geometry_channel) float64 144B ...
    horizontal_cam_lower_origin_z          (horizontal_cam_lower_geometry_channel) float64 144B ...
    horizontal_cam_lower_phi               (horizontal_cam_lower_geometry_channel) float64 144B ...
    ...                                     ...
    tangential_cam_phi                     (tangential_cam_geometry_channel) float64 144B ...
    third_horizontal_cam_endpoint_r        (third_horizontal_cam_geometry_channel) float64 144B ...
    third_horizontal_cam_endpoint_z        (third_horizontal_cam_geometry_channel) float64 144B ...
    third_horizontal_cam_origin_r          (third_horizontal_cam_geometry_channel) float64 144B ...
    third_horizontal_cam_origin_z          (third_horizontal_cam_geometry_channel) float64 144B ...
    third_horizontal_cam_phi               (third_horizontal_cam_geometry_channel) float64 144B ...
Attributes:
    name:          soft_x_rays
    description:   Soft X-rays tomography diagnostic
    imas:          soft_x_rays
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:21:02Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/039000e4cbd943e02405ef851f871782999835b6cc9c28279302b10f9f24ff21.png

Thomson Profiles#

Thomson scattering measurements in a tokamak provide information about the plasma’s electron temperature and density profiles. The diagnostic analyses the scattering of laser light off free electrons in the plasma from a number of radial channels.

Below we plot the following profiles measured by the Thomson diagnostic

  • \(T_e\) - Electron temperature

  • \(N_e\) - Electron density

  • \(P_e\) - Electron pressure

profiles = xr.open_zarr(store, group='thomson_scattering')
profiles

fig, axes = plt.subplots(3, 1)
axes = axes.flatten()
profiles.t_e.plot(x='time', y='major_radius', ax=axes[0])
profiles.n_e.plot(x='time', y='major_radius', ax=axes[1])
profiles.p_e.plot(x='time', y='major_radius', ax=axes[2])
plt.tight_layout()

profiles
<xarray.Dataset> Size: 819kB
Dimensions:       (major_radius: 120, time: 283)
Coordinates:
  * major_radius  (major_radius) float64 960B 0.3 0.31 0.32 ... 1.47 1.48 1.49
  * time          (time) float32 1kB 0.004166 0.008332 ... 0.6042 0.6042
    shot_id       int64 8B ...
Data variables:
    n_e           (major_radius, time) float64 272kB ...
    n_e_core      (time) float32 1kB ...
    p_e           (major_radius, time) float64 272kB ...
    t_e           (major_radius, time) float64 272kB ...
    t_e_core      (time) float32 1kB ...
Attributes:
    name:          thomson_scattering
    description:   Thomson scattering diagnostic
    imas:          thomson_scattering
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:19:31Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/6c6a3265b75b653fffcb4c7a8e0508284213f61abe51b9a498e480f208c47029.png
fig, axes = plt.subplots(2, 1)
profiles['t_e_core'].plot(x='time', ax=axes[0])
profiles['n_e_core'].plot(x='time', ax=axes[1])
for ax in axes:
    ax.grid('on', alpha=0.5)
plt.tight_layout()
profiles
<xarray.Dataset> Size: 819kB
Dimensions:       (major_radius: 120, time: 283)
Coordinates:
  * major_radius  (major_radius) float64 960B 0.3 0.31 0.32 ... 1.47 1.48 1.49
  * time          (time) float32 1kB 0.004166 0.008332 ... 0.6042 0.6042
    shot_id       int64 8B ...
Data variables:
    n_e           (major_radius, time) float64 272kB ...
    n_e_core      (time) float32 1kB ...
    p_e           (major_radius, time) float64 272kB ...
    t_e           (major_radius, time) float64 272kB ...
    t_e_core      (time) float32 1kB ...
Attributes:
    name:          thomson_scattering
    description:   Thomson scattering diagnostic
    imas:          thomson_scattering
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:19:31Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/d1326a16bb257abc420a0fa62c7e1388ea42a2672a36ea284ec2c8e390890dea.png

CXRS Profiles#

Charge Exchange Recombination Spectroscopy (CXRS) measurements provide information about ion temperature and plasma rotation. This diagnostic analyses the light emitted from charge exchange reactions between injected neutral beams and plasma ions.

Below we plot the following profiles measured by the CXRS diagnostic

  • \(T_i\) - Ion temperature

  • \(V_i\) - Ion velocity

profiles = xr.open_zarr(store, group='charge_exchange')

fig, axes = plt.subplots(2, 1)
for name, ax in [('t_i', axes[0]), ('v_i', axes[1])]:
    da = profiles[name]
    da.plot(x='time', y='major_radius', ax=ax, vmax=1000)
    # clip the axes to the region where the diagnostic actually has data
    valid = da.notnull()
    times = da['time'].where(valid.any('major_radius'), drop=True)
    radii = da['major_radius'].where(valid.any('time'), drop=True)
    ax.set_xlim(float(times.min()), float(times.max()))
    ax.set_ylim(float(radii.min()), float(radii.max()))

plt.tight_layout()
profiles
<xarray.Dataset> Size: 402kB
Dimensions:       (major_radius: 161, time: 155)
Coordinates:
  * major_radius  (major_radius) float64 1kB 0.0 0.01 0.02 ... 1.58 1.59 1.6
  * time          (time) float64 1kB -0.1 -0.095 -0.09 ... 0.66 0.665 0.67
    shot_id       int64 8B ...
Data variables:
    t_i           (major_radius, time) float64 200kB nan nan nan ... nan nan nan
    v_i           (major_radius, time) float64 200kB nan nan nan ... nan nan nan
Attributes:
    name:          charge_exchange
    description:   
    imas:          charge_exchange
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:19:31Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/dff18818c01d14640aad0ca757a36b9bb8d6bd2ca51810264ec1376a31a671ba.png

Equilibrium#

profiles = xr.open_zarr(store, group='equilibrium')

profile_1d = profiles[["beta_tor_normal", "wmhd", "li", "elongation", "triangularity_upper", "q95", "vloop_dynamic", "ip_rating", "lcfs_r", "lcfs_z"]]
plot_1d_profiles(profile_1d)

profiles
<xarray.Dataset> Size: 13MB
Dimensions:                          (time: 155, psi_norm: 65,
                                      n_b_field_pol_probe: 78, n_flux_loop: 46,
                                      z: 65, major_radius: 65,
                                      n_boundary_coords: 139,
                                      n_pf_current: 101, profile_r: 65,
                                      n_x_points_r: 2, n_x_points_z: 2)
Coordinates:
  * time                             (time) float64 1kB -0.1 -0.095 ... 0.67
  * psi_norm                         (psi_norm) float32 260B 0.0 0.01562 ... 1.0
  * n_b_field_pol_probe              (n_b_field_pol_probe) float32 312B 0.0 ....
  * n_flux_loop                      (n_flux_loop) float32 184B 0.0 1.0 ... 45.0
  * z                                (z) float32 260B -2.0 -1.938 ... 1.938 2.0
  * major_radius                     (major_radius) float64 520B 0.06 ... 1.98
  * n_boundary_coords                (n_boundary_coords) float32 556B 0.0 ......
  * n_pf_current                     (n_pf_current) float32 404B 0.0 ... 100.0
  * profile_r                        (profile_r) float32 260B 0.0 ... 1.0
  * n_x_points_r                     (n_x_points_r) StringDType() 32B 'XPOINT...
  * n_x_points_z                     (n_x_points_z) StringDType() 32B 'XPOINT...
    shot_id                          int64 8B ...
Data variables: (12/77)
    area                             (time) float64 1kB ...
    area_psi                         (psi_norm, time) float64 81kB ...
    b_field_pol_probe_ang            (n_b_field_pol_probe) float32 312B ...
    b_field_pol_probe_chi_squared    (time, n_b_field_pol_probe) float64 97kB ...
    b_field_pol_probe_len            (n_b_field_pol_probe) float32 312B ...
    b_field_pol_probe_measured       (time, n_b_field_pol_probe) float64 97kB ...
    ...                               ...
    vloop_static                     (time) float64 1kB ...
    volume                           (time) float64 1kB ...
    volume_psi                       (psi_norm, time) float64 81kB ...
    wmhd                             (time) float64 1kB ...
    x_point_r                        (n_x_points_r, time) float64 2kB ...
    x_point_z                        (n_x_points_z, time) float64 2kB ...
Attributes:
    name:          equilibrium
    description:   Description of a 2D, axi-symmetric, tokamak equilibrium; r...
    imas:          equilibrium
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:10:47Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/9245db644a086f9dd27ceaf8abc8ba49b170e606f793e84ec6a2b19fe9ead2b5.png
fig, axes = plt.subplots(1, 3, figsize=(10, 5))

profiles['j_phi'].isel(time=50).plot(ax=axes[0], x='major_radius')
profiles['psi'].isel(time=50).plot(ax=axes[1], x='major_radius')
profiles['q'].isel(time=50).plot(ax=axes[2])
plt.tight_layout()
_images/bae8f434bef74904ef269c50e5053cad953cf2753666fa512f6947bda757b4fd.png

EFIT constraint axes#

EFIT’s flux loop and magnetic probe constraints are stored on numbered axes, n_flux_loop (46 loops) and n_b_field_pol_probe (78 probes), in EFIT’s detector order. Each constraint variable carries the detector names for its axis in an attribute, n_flux_loop_order or n_b_field_pol_probe_order, stored as a JSON list.

Use it to select a detector by name:

import json

eq = xr.open_zarr(store, group='equilibrium')
loops = json.loads(eq.flux_loop_measured.attrs['n_flux_loop_order'])

eq.flux_loop_measured.isel(n_flux_loop=loops.index('P3U/1'))
<xarray.DataArray 'flux_loop_measured' (time: 155)> Size: 1kB
[155 values with dtype=float64]
Coordinates:
  * time         (time) float64 1kB -0.1 -0.095 -0.09 -0.085 ... 0.66 0.665 0.67
    n_flux_loop  float32 4B 14.0
    shot_id      int64 8B ...
Attributes:
    imas:               equilibrium.time_slice[:].constraints.flux_loop[:].me...
    description:        Measured flux loop value used as a constraint in the ...
    name:               flux_loop_measured
    uda_name:           EFM_SILOP_(X)
    n_flux_loop_order:  ["CC01", "CC02", "CC03", "CC04", "CC05", "CC06", "CC0...
    units:              Wb

Or put the names on the axis, so you can use .sel() throughout. The probes work the same way with n_b_field_pol_probe and n_b_field_pol_probe_order.

probes = json.loads(eq.b_field_pol_probe_measured.attrs['n_b_field_pol_probe_order'])
eq_named = eq.assign_coords(n_flux_loop=loops, n_b_field_pol_probe=probes)

eq_named.flux_loop_measured.sel(n_flux_loop='P3U/1').plot()
plt.show()
_images/f6e68d92d59d82e5598616823aacc8515458f313754ff0c6fbfe285225a83e45.png

Only some detectors were digitised: 15 of the 46 flux loops and 58 of the 78 probes. flux_loop_weight is zero for every loop on MAST, so use the measured values to find them. b_field_pol_probe_weight does show which probes the fit used.

# Non-zero at any time (max ignores the NaNs outside the fit window)
loop_digitised = abs(eq_named.flux_loop_measured).max('time') > 0
probe_digitised = abs(eq_named.b_field_pol_probe_measured).max('time') > 0
probe_used = abs(eq_named.b_field_pol_probe_weight).max('time') > 0

print(f'{int(loop_digitised.sum())} of {len(loops)} flux loops digitised')
print(f'{int(probe_digitised.sum())} of {len(probes)} probes digitised, {int(probe_used.sum())} used by the fit')
print('Digitised loops:', eq_named.n_flux_loop[loop_digitised].values.tolist())
15 of 46 flux loops digitised
58 of 78 probes digitised, 35 used by the fit
Digitised loops: ['CC03', 'CC04', 'CC05', 'CC07', 'CC09', 'P3U/1', 'P3U/4', 'P4U/4', 'P5U/1', 'P3L/4', 'P4L/1', 'P4L/4', 'P5L/1', 'P5L/4', 'P6L/1']

To compare EFIT’s constraints with the measurements in magnetics, match loops by name, not by index: magnetics.flux_loop_flux holds only the 15 digitised loops, in a different order, on a finer time grid.

mag = xr.open_zarr(store, group='magnetics')
names = [str(name) for name in mag.flux_loop_channel.values]

efit = eq_named.flux_loop_measured.sel(n_flux_loop=names)
measured = mag.flux_loop_flux.interp(time=eq.time)

fig, ax = plt.subplots()
measured.sel(flux_loop_channel='P3U/1').plot(ax=ax, label='magnetics')
efit.sel(n_flux_loop='P3U/1').plot(ax=ax, label='EFIT constraint', linestyle='--')
ax.legend()
plt.show()
_images/a4f80e82c2abd6e0795f2c421ff8a417d4ca37bf24b612d290950206442da3b5.png

Gas Injection#

profiles = xr.open_zarr(store, group='gas_injection')

plot_1d_profiles(profiles[["inboard_total", "outboard_total", "pressure", "total_injected"]])
plt.tight_layout()
profiles
<xarray.Dataset> Size: 5MB
Dimensions:               (time: 15305, target_valve_channel: 12,
                           valve_channel: 20)
Coordinates:
  * time                  (time) float64 122kB -0.1 -0.09995 ... 0.6652 0.6652
  * target_valve_channel  (target_valve_channel) StringDType() 192B 'g1' ... ...
  * valve_channel         (valve_channel) StringDType() 320B 'tc5a' ... 'ibflb'
    shot_id               int64 8B ...
Data variables:
    inboard_total         (time) float64 122kB ...
    outboard_total        (time) float64 122kB ...
    pressure              (time) float64 122kB ...
    total_injected        (time) float64 122kB ...
    valve_target_voltage  (target_valve_channel, time) float64 1MB ...
    valve_voltage         (valve_channel, time) float64 2MB ...
Attributes:
    name:          gas_injection
    description:   Gas injection by a system of pipes and valves.
    imas:          gas_injection
    license_name:  Creative Commons 4.0 BY-SA
    license_url:   https://creativecommons.org/licenses/by-sa/4.0/
    ingested_at:   2026-09-24T17:11:01Z
    commit_url:    https://github.com/ukaea/fair-mast-ingestion/tree/ed6ea4e2...
_images/dea14cca888c20e40424df8c26a02e7a0af8d2157a9f2fc1775a482ec5a13c80.png