gs2_io.fpp Source File


Contents

Source Code


Source Code

#include "unused_dummy.inc"
!> A module for writing to netcdf.
module gs2_io

# ifdef NETCDF
  use netcdf_utils, only: kind_nf
# endif

  implicit none

  private

  public :: init_gs2_io, nc_eigenfunc, nc_final_fields, nc_final_epar
  public :: nc_final_moments, nc_final_an, nc_finish
  public :: nc_qflux, nc_vflux, nc_pflux, nc_pflux_tormom, nc_loop, nc_loop_moments
  public :: nc_loop_movie, nc_write_moments, nc_exchange, nc_bc
  public :: nc_loop_fullmom, nc_loop_dist, nc_loop_sym, nc_loop_corr, nc_loop_corr_extend
  public :: nc_loop_partsym_tormom, nc_sync, get_netcdf_file_id, get_netcdf_movie_file_id
  public :: define_dims, nc_norms, nc_species, nc_geo, save_input, nc_grids_mymovie, get_dim_length
  public :: nc_write_omega, nc_write_field, nc_write_heating, starts, nc_write_ql_metric
  public :: netcdf_write_complex
  public :: flux_dims, fluxk_dims, final_field_dims, sym_dims, omega_dims, mode_dims, mom_dims, mom_t_dims
  public :: species_dim, x_dim, kx_dim, y_dim, ky_dim, theta_dim, theta_extended_dim, time_dim
  public :: time_big_dim, time_movie_dim, complex_dim, vpa_dim, lambda_dim, egrid_dim, sign_dim
  public :: nheat_dim, dim_2, dim_3, dim_4, dim_5
  public :: kxt_shifted_dim, kxs_shifted_dim, kys_extended_dim

  !> Maximum length of dimension names. This is required to get around
  !> Fortran hating ragged arrays of characters
  integer, parameter :: max_dim_name_length = 9

  !> Name of species dimension
  character(max_dim_name_length), parameter :: species_dim = "species"
  !> Name of x dimension
  character(max_dim_name_length), parameter :: x_dim = "x"
  !> Name of kx dimension
  character(max_dim_name_length), parameter :: kx_dim = "kx"
  !> Name of y dimension
  character(max_dim_name_length), parameter :: y_dim = "y"
  !> Name of ky dimension
  character(max_dim_name_length), parameter :: ky_dim = "ky"
  !> Name of theta dimension
  character(max_dim_name_length), parameter :: theta_dim = "theta"
  !> Name of extended theta dimension
  character(max_dim_name_length), parameter :: theta_extended_dim = "theta_ext"
  !> Name of time dimension
  character(max_dim_name_length), parameter :: time_dim = "t"
  !> Name of "big time" dimension, used for some correlation functions
  character(max_dim_name_length), parameter :: time_big_dim = "tb"
  !> Name of time dimension for real space "movies"
  character(max_dim_name_length), parameter :: time_movie_dim = "tm"
  !> Name of complex dimension
  character(max_dim_name_length), parameter :: complex_dim = "ri"
  !> Name of vpa dimension
  character(max_dim_name_length), parameter :: vpa_dim = "vpa"
  !> Name of lambda dimension
  character(max_dim_name_length), parameter :: lambda_dim = "lambda"
  !> Name of egrid dimension
  character(max_dim_name_length), parameter :: egrid_dim = "egrid"
  !> Name of sign dimension
  character(max_dim_name_length), parameter :: sign_dim = "sign"
  !> Name of nheat dimension
  character(max_dim_name_length), parameter :: nheat_dim = "nheat"
  !> Name of generic dimension for arrays of length 2
  character(max_dim_name_length), parameter :: dim_2 = "2"
  !> Name of generic dimension for arrays of length 3
  character(max_dim_name_length), parameter :: dim_3 = "3"
  !> Name of generic dimension for arrays of length 4
  character(max_dim_name_length), parameter :: dim_4 = "4"
  !> Name of generic dimension for arrays of length 5
  character(max_dim_name_length), parameter :: dim_5 = "5"
  !> Name of shifted kx-target dimension
  character(max_dim_name_length), parameter :: kxt_shifted_dim = "kxt_shift"
  !> Name of shifted kx-target dimension
  character(max_dim_name_length), parameter :: kxs_shifted_dim = "kxs_shift"
  !> Name of shifted extended ky-source dimension
  character(max_dim_name_length), parameter :: kys_extended_dim = "kys_ext"

  !> Names of dimensions used by flux variables
  character(*), dimension(*), parameter :: flux_dims = [species_dim, time_dim]
  !> Names of dimensions used by flux-by-mode variables
  character(*), dimension(*), parameter :: fluxk_dims = [kx_dim, ky_dim, species_dim, time_dim]
  !> Names of dimensions used by snapshots of complex fields
  character(*), dimension(*), parameter :: final_field_dims = [complex_dim, theta_dim, kx_dim, ky_dim]
  !> Names of dimensions used by certain fluxes
  character(*), dimension(*), parameter :: sym_dims = [theta_dim, vpa_dim, species_dim, time_dim]
  !> Names of dimensions used by time-dependent complex fields
  character(*), dimension(*), parameter :: omega_dims = [complex_dim, kx_dim, ky_dim, time_dim]
  !> Names of dimensions used by time-dependent real fields
  character(*), dimension(*), parameter :: mode_dims = [kx_dim, ky_dim, time_dim]
  !> Names of dimensions used by moments
  character(*), dimension(*), parameter :: mom_dims = [complex_dim, kx_dim, ky_dim, species_dim, time_dim]
  !> Names of dimensions used by theta-dependent moments
  character(*), dimension(*), parameter :: mom_t_dims = [complex_dim, theta_dim, kx_dim, ky_dim, species_dim, time_dim]

# ifdef NETCDF
  integer (kind_nf) :: ncid
  integer (kind_nf) :: char10_dim

  ! added by EAB 03/05/04 for movies
  logical :: io_write_corr_extend
  integer :: ncid_movie = -1
  logical :: write_apar_t, write_phi_t, write_bpar_t ! Should the fields be written out every nwrite?

#else
  integer, parameter :: ncid = -1
  integer, parameter :: ncid_movie = -1
#endif
  !> Write a `complex` array to netcdf
  !>
  !> Converts the `complex` array to a `real` array with an extra dimension
  interface netcdf_write_complex
    module procedure write_complex_scalar
    module procedure write_complex_rank1, write_complex_rank2
    module procedure write_complex_rank3, write_complex_rank4
  end interface netcdf_write_complex

  !> Value to disable netCDF compression
  integer, parameter :: compression_off = 0
  !> Level of compression if enabled via
  !> `GK_NETCDF_DEFAULT_COMPRESSION_ON` macro. Must be a value between
  !> 1 (faster, less compression) and 9 (slower, more compression), or
  !> 0 (off)
  integer, parameter :: default_compression_level = 1

contains

  !> Provides read-only access to the netCDF file ID
  integer function get_netcdf_file_id()
    get_netcdf_file_id = ncid
  end function get_netcdf_file_id

  !> Provides read-only access to the netCDF movie file ID
  integer function get_netcdf_movie_file_id()
    get_netcdf_movie_file_id = ncid_movie
  end function get_netcdf_movie_file_id

  !> Return an array of 1-index offsets for writing a time-dependent
  !> field to netCDF:
  !>
  !>     starts(2, nout) == [1, nout]
  !>     starts(3, nout) == [1, 1, nout]
  function starts(rank, nout)
    !> Rank of the array being written
    integer, intent(in) :: rank
    !> Current time index
    integer, intent(in) :: nout
    integer, dimension(rank) :: starts
    starts(1:rank-1) = 1
    starts(rank) = nout
  end function starts

  !> Create and initialise the output netCDF files
  !>
  !> Create the main and movie (real-space fields) netCDF files,
  !> creating the dimensions and main variables
  subroutine init_gs2_io (append_old, make_movie, write_correlation_extend, &
       write_phi_over_time, write_apar_over_time, write_bpar_over_time, nout, &
       ob_midplane, header)
    use standard_header, only : standard_header_type
# ifdef NETCDF
    use mp, only: barrier, proc0
    use file_utils, only: run_name, error_unit
    use gs2_transforms, only: init_transforms
    use kt_grids, only: naky, ntheta0,nx,ny
    use theta_grid, only: ntgrid
    use le_grids, only: nlambda, negrid
    use species, only: nspec
    use constants, only: run_name_size
    use gs2_metadata, only: create_metadata
    use run_parameters, only: user_comments
    use neasyf, only: neasyf_open, neasyf_default_compression
# endif
    implicit none
    !> See [[gs2_diagnostics_knobs]] for details of these inputs
    logical, intent(in) :: append_old
    logical, intent(inout) :: write_correlation_extend
    logical, intent(in) :: make_movie
    logical, intent(in) :: write_phi_over_time, write_apar_over_time, write_bpar_over_time
    !> If true, write out moments at the outboard midplane only,
    !> otherwise write them out along the entire flux surface
    logical, intent(in), optional :: ob_midplane
    integer, intent(out) :: nout
    !> Header for files with build and run information
    type(standard_header_type), intent(in) :: header
# ifdef NETCDF
    logical :: accelerated
    character(run_name_size) :: filename
    logical :: previous_file_exists, appending

    !################
    !# Update internal flags

    write_phi_t = write_phi_over_time
    write_apar_t = write_apar_over_time
    write_bpar_t = write_bpar_over_time

    io_write_corr_extend = write_correlation_extend

    !----------------

#ifdef GK_NETCDF_DEFAULT_COMPRESSION_ON
    neasyf_default_compression = default_compression_level
#else
    neasyf_default_compression = compression_off
#endif

    !Create file
    filename = trim(trim(run_name)//'.out.nc')

    ! only proc0 opens the file:
    if (proc0) then
       ! See if the output file already exists, so we know how to open
       ! the file.  This would be simpler if netCDF had a "open if it
       ! exists, create if it doesn't" mode, but it doesn't so we have
       ! to do this instead
       inquire(file=trim(filename), exist=previous_file_exists)
       if (append_old .and. .not. previous_file_exists) then
         write(error_unit(), '(a, a, a)') "WARNING: 'append_old' requested, but '", trim(filename), &
              "' does not exist. Creating new file"
       end if
       appending = (previous_file_exists .and. append_old)

       if (appending) then
         ncid = neasyf_open(trim(filename), "rw")
       else
         ncid = neasyf_open(trim(filename), "w")
       end if

       call create_metadata(ncid, "GS2 Simulation Data", header, user_comments)
    end if

    !Create the movie file
    if (make_movie) then
      call init_transforms(ntgrid, naky, ntheta0, nlambda, negrid, nspec, nx, ny, accelerated)
       ! only proc0 opens the file:
       if (proc0) then
         ncid_movie = neasyf_open(trim(run_name)//'.movie.nc', "w")
         call create_metadata(ncid_movie, 'GS2 Simulation x,y,theta Data for Movies', header, user_comments)
       end if
    endif

    if (proc0) then
        !Setup main file by creating netCDF file
        call define_dims (ncid, io_write_corr_extend)
        call define_vars (write_correlation_extend, ob_midplane=ob_midplane)

        if (appending) then
            nout = get_dim_length(ncid, "t")
        else
            !Write constant data
            call nc_norms(ncid)
            call nc_species(ncid)
            call nc_geo(ncid)
            call nc_bc(ncid)
            call save_input(ncid)
            nout = 1
        end if

        if (make_movie) then
          call nc_grids_mymovie(ncid_movie)
        end if
    end if
# else
    UNUSED_DUMMY(append_old); UNUSED_DUMMY(make_movie); UNUSED_DUMMY(write_correlation_extend)
    UNUSED_DUMMY(write_phi_over_time); UNUSED_DUMMY(write_apar_over_time)
    UNUSED_DUMMY(write_bpar_over_time); UNUSED_DUMMY(nout); UNUSED_DUMMY(ob_midplane)
    UNUSED_DUMMY(header)
# endif
  end subroutine init_gs2_io

  !> Ensure the netCDF file contains all the dimensions and grids,
  !> creating them if necessary
  !>
  !> Dimension variable IDs are module level variables
  subroutine define_dims (file_id, create_extended_correlation)
# ifdef NETCDF
    use file_utils, only: num_input_lines
    use kt_grids, only: aky, akx, theta0, jtwist_out, ntheta0, naky, nx, ny
    use theta_grid, only: theta, ntgrid, nperiod, ntheta, nbset, bset
    use le_grids, only: al, energy, negrid, nlambda
    use species, only: nspec
    use mp, only: nproc
    use nonlinear_terms, only: nonlin
    use gs2_layouts, only: layout
    use neasyf, only: neasyf_dim, neasyf_write
    use netcdf_utils, only: ensure_netcdf_dim_exists
# endif
    implicit none
    !> NetCDF ID of the file
    integer, intent(in) :: file_id
    !> Create the dimensions for the extended correlation diagnostics
    logical, intent(in) :: create_extended_correlation
# ifdef NETCDF
    integer :: nmesh

    call neasyf_dim(file_id, trim(kx_dim), values=akx, &
         long_name="Wavenumber perpendicular to flux surface", units="1/rho_r")
    call neasyf_dim(file_id, trim(ky_dim), values=aky, &
         long_name="Wavenumber in direction of grad alpha", units="1/rho_r")
    call neasyf_dim(file_id, trim(theta_dim), values=theta, long_name="Poloidal angle", units="rad")
    call neasyf_dim(file_id, trim(lambda_dim), values=al, long_name="Energy/magnetic moment", units="1/(2 B_a)")
    call neasyf_dim(file_id, trim(species_dim), dim_size=nspec)

    ! Energy coordinate is a function of species, but the dimension must be 1D
    call neasyf_dim(file_id, trim(egrid_dim), dim_size=negrid, long_name="See 'energy' for energy grid values")
    call neasyf_write(file_id, "energy", energy, dim_names=[egrid_dim, species_dim], &
         long_name="Energy grid values")

    call neasyf_dim(file_id, trim(sign_dim), dim_size=2)
    call neasyf_dim(file_id, trim(time_dim), unlimited=.true., long_name="Time", units="L/vt")
    call neasyf_dim(file_id, trim(complex_dim), dim_size=2, &
         long_name="Complex components", units="(real, imaginary)")
    call neasyf_dim(file_id, trim(nheat_dim), dim_size=7)
    call neasyf_dim(file_id, trim(vpa_dim), dim_size=negrid*nlambda)

    ! Dimensions for various string variables
    call neasyf_dim(file_id, "char5", dim_size=5)
    call neasyf_dim(file_id, "char10", dim_size=10, dimid=char10_dim)
    call neasyf_dim(file_id, "char200", dim_size=200)

    ! netCDF interprets zero-sized dimensions as unlimited, which is not what we want
    call neasyf_dim(file_id, "nlines", dim_size=max(1, num_input_lines), long_name="Input file line number")

    ! Dimensions for non-physical matrices etc
    call neasyf_dim(file_id, trim(dim_2), dim_size=2, long_name="Generic dim")
    call neasyf_dim(file_id, trim(dim_3), dim_size=3, long_name="Generic dim")
    call neasyf_dim(file_id, trim(dim_4), dim_size=4, long_name="Generic dim")
    call neasyf_dim(file_id, trim(dim_5), dim_size=5, long_name="Generic dim")

    call neasyf_dim(file_id, "bset", values=bset, long_name="Bmag at bounce points", units="Bref")

    if (create_extended_correlation) then
       call neasyf_dim(file_id, trim(theta_extended_dim), dim_size=(2*ntgrid+1)*((ntheta0-1)/jtwist_out+1))
       call neasyf_dim(file_id, trim(time_big_dim), unlimited=.true.)
    end if

    call neasyf_write(file_id, "layout", layout, long_name="Decomposition layout for g_lo")
    call neasyf_write(file_id, "nproc", nproc, long_name="Number of processors")
    call neasyf_write(file_id, "ntheta_tot", 2*ntgrid+1, long_name="Total number of theta points")
    call neasyf_write(file_id, "nkx", ntheta0, long_name="Number of kx points")
    call neasyf_write(file_id, "nky", naky, long_name="Number of ky points")
    call neasyf_write(file_id, "nspecies", nspec, long_name="Number of species")
    call neasyf_write(file_id, "nenergy", negrid, long_name="Number of energy points")
    call neasyf_write(file_id, "nlambda", nlambda, long_name="Number of lambda points")

    call neasyf_write(file_id, "nperiod", nperiod, long_name="Number of 2 pi theta sections in parallel domain chunk")
    call neasyf_write(file_id, "ntheta", ntheta, long_name="Number of theta points per 2 pi section")
    call neasyf_write(file_id, "nbset", nbset, long_name="Number of bounce points")

    call neasyf_write(file_id, "theta0", theta0, dim_names=[kx_dim, ky_dim], &
         long_name="Theta_0, poloidal angle where mode radial wavenumber == 0", units="rad")

    if (nonlin) then
       nmesh = (2*ntgrid+1)*2*nlambda*negrid*nx*ny*nspec
    else
       nmesh = (2*ntgrid+1)*2*nlambda*negrid*ntheta0*naky*nspec
    end if

    call neasyf_write(file_id, "nmesh", nmesh, long_name="Total number of mesh points")
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(create_extended_correlation)
# endif
  end subroutine define_dims

  !> Write the physical normalisations to the output netCDF file
  !> Write the physical normalisations to the output netCDF file
  subroutine nc_norms(file_id)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use normalisations, only: norms
#endif
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id

#ifdef NETCDF
    call neasyf_write(file_id, "mref", norms%get_value("mref"), &
         long_name="Reference mass in atomic mass units ", units="a.m.u.")
    call neasyf_write(file_id, "zref", norms%get_value("zref"), &
         long_name="Reference charge", units="proton charge")
    call neasyf_write(file_id, "nref", norms%get_value("nref"), &
         long_name="The reference density ", units="m^-3")
    call neasyf_write(file_id, "tref", norms%get_value("tref"), &
         long_name="The reference temperature ", units="eV")
    call neasyf_write(file_id, "aref", norms%get_value("aref"), &
         long_name="The reference length ", units="m")
    call neasyf_write(file_id, "vref", norms%get_value("vref"), &
         long_name="The reference (thermal) velocity ", units="m/s")
    call neasyf_write(file_id, "bref", norms%get_value("bref"), &
         long_name="The reference magnetic field ", units="Tesla")
    call neasyf_write(file_id, "rhoref", norms%get_value("rhoref"), &
         long_name="The reference larmour radius ", units="m")
#else
    UNUSED_DUMMY(file_id)
#endif
  end subroutine nc_norms

  !> Puts the grid data into the movie file
  subroutine nc_grids_mymovie(file_id)
#ifdef NETCDF
    use constants, only: pi
    use kt_grids, only: akx, aky
    use gs2_layouts, only: yxf_lo
    use theta_grid, only: ntgrid, theta
    use neasyf, only: neasyf_dim, neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
# ifdef NETCDF
    real, dimension(:), allocatable :: x,y
    integer :: it, ik

    allocate(x(yxf_lo%nx),y(yxf_lo%ny))

    do it = 1, yxf_lo%nx
      x(it) = 2.0*pi/akx(2)*(-0.5+ real(it-1)/real(yxf_lo%nx))
    end do
    do ik = 1, yxf_lo%ny
      y(ik) = 2.0*pi/aky(2)*(-0.5+ real(ik-1)/real(yxf_lo%ny))
    end do

    call neasyf_write(file_id, "nx", yxf_lo%nx)
    call neasyf_write(file_id, "ny", yxf_lo%ny)
    call neasyf_write(file_id, "ntheta", 2*ntgrid+1)
    call neasyf_dim(file_id, x_dim, x, long_name="x / rho")
    call neasyf_dim(file_id, y_dim, y, long_name="y / rho")
    call neasyf_dim(file_id, trim(theta_dim), theta)
    call neasyf_dim(file_id, trim(time_movie_dim), unlimited=.true., long_name="Time", units="L/vt")
    deallocate(x,y)
# else
    UNUSED_DUMMY(file_id)
# endif
  end subroutine nc_grids_mymovie

  !> Close the output netCDF files
  subroutine nc_finish
# ifdef NETCDF
    use mp, only: proc0
    use neasyf, only: neasyf_close
    implicit none

    if (proc0) then
       call neasyf_close(ncid)
       if (ncid_movie >= 0) then
          call neasyf_close(ncid_movie)
        endif
    end if
# endif
  end subroutine nc_finish

  !> Save the input file in the NetCDF file
  subroutine save_input(file_id)
#ifdef NETCDF
    use file_utils, only: num_input_lines, get_input_unit
    use neasyf, only: neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
#ifdef NETCDF
    integer, parameter :: line_length = 200
    character(line_length), dimension(:), allocatable ::  input_file_array
    integer :: n, unit

    allocate(input_file_array(max(1, num_input_lines)))
    input_file_array(1) = ""

    call get_input_unit(unit)
    rewind (unit=unit)
    do n = 1, num_input_lines
      read (unit=unit, fmt="(a)") input_file_array(n)
    end do

    call neasyf_write(file_id, "input_file", input_file_array, &
         long_name="Input file", dim_names=["char200", "nlines "])
#else
    UNUSED_DUMMY(file_id)
#endif
  end subroutine save_input

  !> Create (but don't fill) the various diagnostics variables in the
  !> output netCDF file
# ifdef NETCDF
  subroutine define_vars (write_correlation_extend, ob_midplane)
    use file_utils, only: error_unit
    use netcdf, only : NF90_CHAR, nf90_inq_libvers
    use netcdf_utils, only: get_netcdf_dim_length
    use netcdf_utils, only: ensure_netcdf_att_exists, ensure_netcdf_var_exists
    use optionals, only: get_option_with_default
    implicit none
    logical, intent(inout) :: write_correlation_extend
    logical, optional, intent(in) :: ob_midplane
    character (20) :: datestamp, timestamp, timezone
    integer, dimension(5) :: dimsize
    real    :: total
    logical :: obmid
    integer :: code_id

    !Handle optional input
    obmid = get_option_with_default(ob_midplane, .false.)

    ! HJL This is a hack to stop the netcdf diagnostic output failing when the variable sizes are too big
    ! The netcdf limit is 2^31-4 bytes, however this limit still causes a problem so 178180992 is used
    ! This limit was found by running the code with different sizes, it is not exact but this is a hack
    ! and is not worth pursuing at the moment
    !<DD>Added protective if statement as theta_extended_dim is only defined if io_write_corr_extend
    if(io_write_corr_extend)then
       dimsize(1) = get_dim_length(ncid, complex_dim)
       dimsize(2) = get_dim_length(ncid, theta_extended_dim)
       dimsize(3) = get_dim_length(ncid, kx_dim)
       dimsize(4) = get_dim_length(ncid, ky_dim)
       dimsize(5) = get_dim_length(ncid, time_big_dim)
       total = product(real(dimsize(1:5)))
       if(total .gt. 178180992) then ! number of reals allowed in netcdf
          write(error_unit(), *) 'WARNING: phi_corr is larger than maximum netcdf size, removing.'
          write(error_unit(), *) 'Try increasing nwrite_mult to avoid this.'
          io_write_corr_extend = .false.
          write_correlation_extend = .false.
       endif
    endif
    ! HJL End of hack

    datestamp(:) = ' '
    timestamp(:) = ' '
    timezone(:) = ' '
    call date_and_time (datestamp, timestamp, timezone)

    !################################################
    !# START WITH BASIC ATTRIBUTES

    call ensure_netcdf_var_exists(ncid, "code_info", NF90_CHAR, char10_dim, code_id)
    call ensure_netcdf_att_exists(ncid, code_id, "long_name", "GS2")
    call ensure_netcdf_att_exists(ncid, code_id, "c1", 'Date: '//trim(datestamp))
    call ensure_netcdf_att_exists(ncid, code_id, "c2", &
         'Time: '//trim(timestamp)//' '//trim(timezone))
    call ensure_netcdf_att_exists(ncid, code_id, "c3", &
         'netCDF version '//trim(nf90_inq_libvers()))
    call ensure_netcdf_att_exists(ncid, code_id, "c4", &
         'Units are determined with respect to reference temperature (T_ref),')
    call ensure_netcdf_att_exists(ncid, code_id, "c5", &
         'reference charge (q_ref), reference mass (mass_ref),')
    call ensure_netcdf_att_exists(ncid, code_id, "c6", &
         'reference field (B_ref), and reference length (a_ref)')
    call ensure_netcdf_att_exists(ncid, code_id, "c7", &
         'from which one may construct rho_ref and vt_ref/a,')
    call ensure_netcdf_att_exists(ncid, code_id, "c8", &
         'which are the basic units of perpendicular length and time.')
    call ensure_netcdf_att_exists(ncid, code_id, "c9", &
         'Macroscopic lengths are normalized to the minor radius.')
    call ensure_netcdf_att_exists(ncid, code_id, "c10", &
         'The difference between rho (normalized minor radius) and rho (gyroradius)')
    call ensure_netcdf_att_exists(ncid, code_id, "c11", &
         'should be clear from the context in which they appear below.')
  end subroutine define_vars
#endif

  !> Write the normalised fields to netCDF
  subroutine nc_eigenfunc(file_id, phase)
#ifdef NETCDF
    use convert, only: c2r
    use run_parameters, only: has_phi, has_apar, has_bpar
    use fields_arrays, only: phi, apar, bpar
    use theta_grid, only: ntgrid
    use kt_grids, only: naky, ntheta0
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Normalising phase factor (nominally \(\phi\) at the outboard midplane)
    complex, dimension(:,:), intent (in) :: phase
# ifdef NETCDF

    complex, dimension(-ntgrid:ntgrid, ntheta0, naky) :: tmp
    integer :: ig

    call netcdf_write_complex(file_id, "phase", phase, [complex_dim, kx_dim, ky_dim], long_name="Normalising phase")

    if (has_phi) then
       do ig = -ntgrid, ntgrid
          tmp(ig,:,:) = phi(ig,:,:)/phase(:,:)
       end do
       call netcdf_write_complex(file_id, "phi_norm", tmp, final_field_dims)
    end if

    if (has_apar) then
       do ig = -ntgrid, ntgrid
          tmp(ig,:,:) = apar(ig,:,:)/phase(:,:)
       end do
       call netcdf_write_complex(file_id, "apar_norm", tmp, final_field_dims)
    end if

    if (has_bpar) then
       do ig = -ntgrid, ntgrid
          tmp(ig,:,:) = bpar(ig,:,:)/phase(:,:)
       end do
       call netcdf_write_complex(file_id, "bpar_norm", tmp, final_field_dims)
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(phase)
# endif
  end subroutine nc_eigenfunc

  !> Write various moments to netcdf
  subroutine nc_write_moments (file_id, nout, ntot, density, upar, tpar, tperp, qparflux, &
       pperpj1, qpperpj1, ob_midplane)
    use theta_grid, only: ntgrid
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Total perturbed density
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: ntot
    !> Non-adiabatic part of the perturbed density
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: density
    !> Parallel velocity
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: upar
    !> Parallel temperature
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: tpar
    !> Perpendicular temperature
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: tperp
    !> Parallel heat flux
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: qparflux
    !> Pressure part of the particle flux
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: pperpj1
    !> Pressure part of the heat flux
    complex, dimension (-ntgrid:,:,:,:), intent (in) :: qpperpj1
    !> Current output timestep
    integer, intent (in) :: nout
    !> If true, write out moments at the outboard midplane only,
    !> otherwise write them out along the entire flux surface
    logical, optional, intent(in) :: ob_midplane
#ifdef NETCDF
    if (present(ob_midplane)) then
       if (ob_midplane ) then
          call netcdf_write_complex(file_id, "ntot0", ntot(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Total perturbed density (theta=0) over time")
          call netcdf_write_complex(file_id, "density0", density(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Nonadiabatic piece of perturbed density (theta=0) over time")
          call netcdf_write_complex(file_id, "upar0", upar(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Upar (theta=0) over time")
          call netcdf_write_complex(file_id, "tpar0", tpar(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Tpar (theta=0) over time")
          call netcdf_write_complex(file_id, "tperp0", tperp(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Tperp (theta=0) over time")
          call netcdf_write_complex(file_id, "qparflux0", qparflux(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Qparflux (theta=0) over time")
          call netcdf_write_complex(file_id, "pperpj10", pperpj1(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Pperpj1 (theta=0) over time")
          call netcdf_write_complex(file_id, "qpperpj10", qpperpj1(0, :, :, :), mom_dims, start=starts(5, nout), &
               long_name="Qpperpj1 (theta=0) over time")
          return
       endif
    endif

    call netcdf_write_complex(file_id, 'ntot_t', ntot, mom_t_dims, start=starts(6, nout), &
          long_name="Total perturbed density over time")
    call netcdf_write_complex(file_id, 'density_t', density, mom_t_dims, start=starts(6, nout), &
          long_name="Nonadiabatic piece of perturbed density over time")
    call netcdf_write_complex(file_id, 'upar_t', upar, mom_t_dims, start=starts(6, nout), &
          long_name="Upar over time")
    call netcdf_write_complex(file_id, 'tpar_t', tpar, mom_t_dims, start=starts(6, nout), &
          long_name="Tpar over time")
    call netcdf_write_complex(file_id, 'tperp_t', tperp, mom_t_dims, start=starts(6, nout), &
          long_name="Tperp over time")
    call netcdf_write_complex(file_id, 'qparflux_t', qparflux, mom_t_dims, start=starts(6, nout), &
          long_name="Qparflux over time")
    call netcdf_write_complex(file_id, 'pperpj1_t', pperpj1, mom_t_dims, start=starts(6, nout), &
          long_name="Pperpj1 over time")
    call netcdf_write_complex(file_id, 'qpperpj1_t', qpperpj1, mom_t_dims, start=starts(6, nout), &
          long_name="Qpperpj1 over time")
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(ntot); UNUSED_DUMMY(density)
    UNUSED_DUMMY(upar); UNUSED_DUMMY(tpar); UNUSED_DUMMY(tperp); UNUSED_DUMMY(qparflux)
    UNUSED_DUMMY(pperpj1); UNUSED_DUMMY(qpperpj1); UNUSED_DUMMY(ob_midplane)
# endif
  end subroutine nc_write_moments

  !> Write the fields to netCDF, overwriting existing values
  subroutine nc_final_fields(file_id)
#ifdef NETCDF
    use fields_arrays, only: phinew, aparnew, bparnew
    use run_parameters, only: has_phi, has_apar, has_bpar
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
#ifdef NETCDF
    if (has_phi) then
       call netcdf_write_complex(file_id, "phi", phinew, final_field_dims, &
            long_name="Electrostatic Potential", units="T_ref/q_ref rho_ref/a_ref")
    end if

    if (has_apar) then
       call netcdf_write_complex(file_id, "apar", aparnew, final_field_dims, &
            long_name="Parallel Magnetic Potential")
    end if

    if (has_bpar) then
       call netcdf_write_complex(file_id, "bpar", bparnew, final_field_dims, &
            long_name="delta B Parallel")
    end if
# else
    UNUSED_DUMMY(file_id)
# endif
  end subroutine nc_final_fields

  !> Write \(E_\parallel\) to netCDF, overwriting existing values
  subroutine nc_final_epar (file_id, epar)
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    complex, dimension (:,:,:), intent (in) :: epar
# ifdef NETCDF
    character(*), dimension(*), parameter :: dim_names = [complex_dim, theta_dim, kx_dim, ky_dim]

    call netcdf_write_complex(file_id, "epar", epar, dim_names)
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(epar)
# endif
  end subroutine nc_final_epar

  !> Write the various low-order moments to the output netCDF file,
  !> overwriting existing values
  subroutine nc_final_moments (file_id, ntot, density, upar, tpar, tperp, qparflux, pperpj1, qpperpj1)
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    complex, dimension (:,:,:,:), intent (in) :: ntot, density, upar, tpar, tperp
    complex, dimension (:,:,:,:), intent (in) :: qparflux, pperpj1, qpperpj1
#ifdef NETCDF
    character(*), dimension(*), parameter :: dim_names = [complex_dim, theta_dim, kx_dim, ky_dim, species_dim]
    call netcdf_write_complex(file_id, 'ntot', ntot, dim_names)
    call netcdf_write_complex(file_id, 'density', density, dim_names)
    call netcdf_write_complex(file_id, 'upar', upar, dim_names)
    call netcdf_write_complex(file_id, 'tpar', tpar, dim_names)
    call netcdf_write_complex(file_id, 'tperp', tperp, dim_names)
    call netcdf_write_complex(file_id, 'qparflux', qparflux, dim_names)
    call netcdf_write_complex(file_id, 'pperpj1', pperpj1, dim_names)
    call netcdf_write_complex(file_id, 'qpperpj1', qpperpj1, dim_names)
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(ntot); UNUSED_DUMMY(density); UNUSED_DUMMY(upar)
    UNUSED_DUMMY(tpar); UNUSED_DUMMY(tperp); UNUSED_DUMMY(qparflux); UNUSED_DUMMY(pperpj1)
    UNUSED_DUMMY(qpperpj1)
#endif
  end subroutine nc_final_moments

  !> Write moments as: volume averaged, field line averaged, and evaulated at theta=0
  subroutine nc_loop_moments (file_id, nout, ntot2, ntot2_by_mode, ntot20, ntot20_by_mode, &
       phi00, ntot00, density00, upar00, tpar00, tperp00, tpar2_by_mode, tperp2_by_mode)
#ifdef NETCDF
    use neasyf, only: neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:), intent (in) :: ntot2, ntot20
    real, dimension (:,:,:), intent (in) :: ntot2_by_mode, ntot20_by_mode
    real, dimension (:,:,:), intent (in) :: tpar2_by_mode, tperp2_by_mode
    complex, dimension (:), intent (in) :: phi00
    complex, dimension (:,:), intent (in) :: ntot00, density00, upar00, tpar00, tperp00
#ifdef NETCDF
    character(*), dimension(*), parameter :: loop_phi_dim = [complex_dim, kx_dim, time_dim]
    character(*), dimension(*), parameter :: loop_mom_dim = [complex_dim, kx_dim, species_dim, time_dim]
    
    ! Volume averaged function of theta
    ! <DD>Should we only do this if .not.obmid?
    call neasyf_write (file_id, "ntot2", ntot2, dim_names=flux_dims, start=starts(2, nout))
    call neasyf_write (file_id, "ntot2_by_mode", ntot2_by_mode, dim_names=fluxk_dims, start=starts(4, nout))
    call neasyf_write (file_id, "tpar2_by_mode", tpar2_by_mode, dim_names=fluxk_dims, start=starts(4, nout))
    call neasyf_write (file_id, "tperp2_by_mode", tperp2_by_mode, dim_names=fluxk_dims, start=starts(4, nout))

    ! "Volume averaged" data from outboard midplane (really just evaluated at theta=0
    ! and then weighted with Fourier coeff factor (typically 0.5).
    ! <DD>Should we only do this if obmid?
    call neasyf_write (file_id, "ntot20", ntot20, dim_names=flux_dims, start=starts(2, nout), &
         long_name='Density**2 at theta=0')
    call neasyf_write (file_id, "ntot20_by_mode", ntot20_by_mode, dim_names=fluxk_dims, start=starts(4, nout))

    ! Fieldline averaged data, note this includes phi which is not a moment
    call netcdf_write_complex(file_id, "phi00", phi00, loop_phi_dim, start=starts(3, nout))
    call netcdf_write_complex(file_id, "ntot00", ntot00, loop_mom_dim, start=starts(4, nout))
    call netcdf_write_complex(file_id, "density00", density00, loop_mom_dim, start=starts(4, nout))
    call netcdf_write_complex(file_id, "upar00", upar00, loop_mom_dim, start=starts(4, nout))
    call netcdf_write_complex(file_id, "tpar00", tpar00, loop_mom_dim, start=starts(4, nout))
    call netcdf_write_complex(file_id, "tperp00", tperp00, loop_mom_dim, start=starts(4, nout))
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(ntot2)
    UNUSED_DUMMY(ntot2_by_mode); UNUSED_DUMMY(ntot20); UNUSED_DUMMY(ntot20_by_mode)
    UNUSED_DUMMY(phi00); UNUSED_DUMMY(ntot00); UNUSED_DUMMY(density00); UNUSED_DUMMY(upar00)
    UNUSED_DUMMY(tpar00); UNUSED_DUMMY(tperp00); UNUSED_DUMMY(tpar2_by_mode)
    UNUSED_DUMMY(tperp2_by_mode)
#endif
  end subroutine nc_loop_moments

  !> Write the right-hand sides of the field equations to netCDF,
  !> overwriting existing values
  subroutine nc_final_an (file_id, antot, antota, antotp)
# ifdef NETCDF
    use run_parameters, only: has_phi, has_apar, has_bpar
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    complex, dimension (:,:,:), intent(in) :: antot, antota, antotp
# ifdef NETCDF
    if (has_phi) then
       call netcdf_write_complex(file_id, "antot", antot, final_field_dims, &
            long_name="Velocity moment of g used in calculating phi")
    end if

    if (has_apar) then
       call netcdf_write_complex(file_id, "antota", antota, final_field_dims, &
            long_name="Velocity space moment of g used in calculating apar")
    end if

    if (has_bpar) then
       call netcdf_write_complex(file_id, "antotp", antotp, final_field_dims, &
            long_name="Velocity space moment of g used in calculating bpar")
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(antot); UNUSED_DUMMY(antota); UNUSED_DUMMY(antotp)
# endif
  end subroutine nc_final_an

  !> Write the energy exchange to netCDF
  subroutine nc_exchange (file_id, nout, exchange, exchange_avg)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    !> Energy exchange
    real, dimension (:,:,:), intent (in) :: exchange
    !> Volume-averaged energy exchange
    real, dimension (:), intent (in) :: exchange_avg
# ifdef NETCDF
    if (has_phi) then
       call neasyf_write (file_id, "es_exchange", exchange_avg, &
            dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write (file_id, "es_exchange_by_k", exchange, &
            dim_names=fluxk_dims, start=starts(4, nout))
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(exchange)
    UNUSED_DUMMY(exchange_avg)
# endif
  end subroutine nc_exchange

  !> Write various heat fluxes to netCDF
  subroutine nc_qflux (file_id, nout, qheat, qmheat, qbheat, &
       heat_par,  mheat_par,  bheat_par, &
       heat_perp, mheat_perp, bheat_perp, &
       heat_fluxes, mheat_fluxes, bheat_fluxes, x_qmflux, hflux_tot)
# ifdef NETCDF
    use run_parameters, only: has_phi, has_apar, has_bpar
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: qheat, qmheat, qbheat
    real, dimension (:), intent (in) :: heat_par, mheat_par, bheat_par
    real, dimension (:), intent (in) :: heat_perp, mheat_perp, bheat_perp
    real, dimension (:), intent (in) :: heat_fluxes, mheat_fluxes, bheat_fluxes
    real, dimension (:,:), intent (in) :: x_qmflux
    real, intent (in) :: hflux_tot
# ifdef NETCDF
    character(*), dimension(*), parameter :: fluxx_dim = [kx_dim, species_dim, time_dim]

    if (has_phi) then
       call neasyf_write(file_id, "es_heat_flux", heat_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_heat_par", heat_par, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_heat_perp", heat_perp, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_heat_by_k", qheat, dim_names=fluxk_dims, start=starts(4, nout))
    end if

    if (has_apar) then
       call neasyf_write(file_id, "apar_heat_flux", mheat_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "apar_heat_par", mheat_par, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "apar_heat_perp", mheat_perp, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "apar_heat_by_k", qmheat, dim_names=fluxk_dims, start=starts(4, nout))
       call neasyf_write(file_id, "apar_heat_by_x", x_qmflux, dim_names=fluxx_dim, start=starts(3, nout))
    end if

    if (has_bpar) then
       call neasyf_write(file_id, "bpar_heat_flux", bheat_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "bpar_heat_par", bheat_par, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "bpar_heat_perp", bheat_perp, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "bpar_heat_by_k", qbheat, dim_names=fluxk_dims, start=starts(4, nout))
    end if
    
    call neasyf_write(file_id, "hflux_tot", hflux_tot, dim_names=[time_dim], start=[nout])
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(qheat); UNUSED_DUMMY(qmheat)
    UNUSED_DUMMY(qbheat); UNUSED_DUMMY(heat_par); UNUSED_DUMMY(mheat_par)
    UNUSED_DUMMY(bheat_par); UNUSED_DUMMY(heat_perp); UNUSED_DUMMY(mheat_perp)
    UNUSED_DUMMY(bheat_perp); UNUSED_DUMMY(heat_fluxes); UNUSED_DUMMY(mheat_fluxes)
    UNUSED_DUMMY(bheat_fluxes); UNUSED_DUMMY(x_qmflux); UNUSED_DUMMY(hflux_tot)
# endif
  end subroutine nc_qflux

  !> FIXME : Add documentation  
  subroutine nc_pflux (file_id, nout, pflux, pmflux, pbflux, &
       part_fluxes, mpart_fluxes, bpart_fluxes, zflux_tot)
# ifdef NETCDF
    use run_parameters, only: has_phi, has_apar, has_bpar
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: pflux, pmflux, pbflux
    real, dimension(:), intent (in) :: part_fluxes, mpart_fluxes, bpart_fluxes
    real, intent (in) :: zflux_tot
# ifdef NETCDF
    if (has_phi) then
       call neasyf_write(file_id, "es_part_flux", part_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_part_by_k", pflux, dim_names=fluxk_dims, start=starts(4, nout))
    end if

    if (has_apar) then
       call neasyf_write(file_id, "apar_part_flux", mpart_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "apar_part_by_k", pmflux, dim_names=fluxk_dims, start=starts(4, nout))
    end if

    if (has_bpar) then
       call neasyf_write(file_id, "bpar_part_flux", bpart_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "bpar_part_by_k", pbflux, dim_names=fluxk_dims, start=starts(4, nout))
    end if
    
    call neasyf_write(file_id, "zflux_tot", zflux_tot, dim_names=[time_dim], start=[nout])
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(pflux); UNUSED_DUMMY(pmflux)
    UNUSED_DUMMY(pbflux); UNUSED_DUMMY(part_fluxes); UNUSED_DUMMY(mpart_fluxes)
    UNUSED_DUMMY(bpart_fluxes); UNUSED_DUMMY(zflux_tot)
# endif
  end subroutine nc_pflux

  !> Write toroidal angular momentum flux to netCDF
  subroutine nc_pflux_tormom (file_id, nout, pflux_tormom, part_tormom_fluxes)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: pflux_tormom
    real, dimension(:), intent (in) :: part_tormom_fluxes
# ifdef NETCDF
    if (has_phi) then
       call neasyf_write(file_id, "es_part_tormom_flux", part_tormom_fluxes, &
            dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_part_tormom_by_k", pflux_tormom, &
            dim_names=fluxk_dims, start=starts(4, nout))
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(pflux_tormom)
    UNUSED_DUMMY(part_tormom_fluxes)
# endif
  end subroutine nc_pflux_tormom

  !> FIXME : Add documentation  
  subroutine nc_vflux (file_id, nout, vflux, vmflux, vbflux, &
       mom_fluxes, mmom_fluxes, bmom_fluxes, vflux_tot, &
       vflux_par, vflux_perp, vflux0, vflux1)
# ifdef NETCDF
    use run_parameters, only: has_phi, has_apar, has_bpar
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: vflux, vmflux, vbflux
    real, dimension (:,:,:), intent (in) :: vflux_par, vflux_perp
    real, dimension (:,:,:), intent (in) :: vflux0, vflux1
    real, dimension(:), intent (in) :: mom_fluxes, mmom_fluxes, bmom_fluxes
    real, intent (in) :: vflux_tot
# ifdef NETCDF
    if (has_phi) then
       call neasyf_write(file_id, "es_mom_flux", mom_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "es_mom_by_k", vflux, dim_names=fluxk_dims, start=starts(4, nout))
       call neasyf_write(file_id, "es_parmom_by_k", vflux_par, dim_names=fluxk_dims, start=starts(4, nout))
       call neasyf_write(file_id, "es_perpmom_by_k", vflux_perp, dim_names=fluxk_dims, start=starts(4, nout))
       call neasyf_write(file_id, "es_mom0_by_k", vflux0, dim_names=fluxk_dims, start=starts(4, nout))
       call neasyf_write(file_id, "es_mom1_by_k", vflux1, dim_names=fluxk_dims, start=starts(4, nout))
    end if

    if (has_apar) then
       call neasyf_write(file_id, "apar_mom_flux", mmom_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "apar_mom_by_k", vmflux, dim_names=fluxk_dims, start=starts(4, nout))
    end if

    if (has_bpar) then
       call neasyf_write(file_id, "bpar_mom_flux", bmom_fluxes, dim_names=flux_dims, start=starts(2, nout))
       call neasyf_write(file_id, "bpar_mom_by_k", vbflux, dim_names=fluxk_dims, start=starts(4, nout))
    end if
    
    call neasyf_write(file_id, "vflux_tot", vflux_tot, dim_names=[time_dim], start=[nout])
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(vflux); UNUSED_DUMMY(vmflux)
    UNUSED_DUMMY(vbflux); UNUSED_DUMMY(mom_fluxes); UNUSED_DUMMY(mmom_fluxes)
    UNUSED_DUMMY(bmom_fluxes); UNUSED_DUMMY(vflux_tot); UNUSED_DUMMY(vflux_par)
    UNUSED_DUMMY(vflux_perp); UNUSED_DUMMY(vflux0); UNUSED_DUMMY(vflux1)
# endif
  end subroutine nc_vflux

  !> Output the poloidal distributions of the fluxes of particles, perpendicular
  !> momentum, parallel momentum, and energy ! JRB
  subroutine nc_loop_dist (file_id, nout, part_fluxes_dist, mom_fluxes_par_dist, mom_fluxes_perp_dist, heat_fluxes_dist, phi_dist)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
# endif
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:, :), intent (in) :: part_fluxes_dist, mom_fluxes_par_dist, mom_fluxes_perp_dist, heat_fluxes_dist
    real, dimension (:, :), intent (in) :: phi_dist
# ifdef NETCDF
    character(*), dimension(*), parameter :: flux_dist_dim = [theta_dim, species_dim, time_dim]
    character(*), dimension(*), parameter :: phi_dist_dim = [complex_dim, theta_dim, time_dim]

    if (has_phi) then
       call neasyf_write(file_id, "es_part_flux_dist", part_fluxes_dist, &
            dim_names=flux_dist_dim, start=starts(3, nout), &
            long_name= "Particle flux density, see Ball et al PPCF 58 045023 2016")
       call neasyf_write(file_id, "es_mom_flux_par_dist", mom_fluxes_par_dist, &
            dim_names=flux_dist_dim, start=starts(3, nout), &
            long_name= "Parallel momentum flux density, see Ball et al PPCF 58 045023 2016")
       call neasyf_write(file_id, "es_mom_flux_perp_dist", mom_fluxes_perp_dist, &
            dim_names=flux_dist_dim, start=starts(3, nout), &
            long_name= "Perpendicular momentum flux density, see Ball et al PPCF 58 045023 2016")
       call neasyf_write(file_id, "es_heat_flux_dist", heat_fluxes_dist, &
            dim_names=flux_dist_dim, start=starts(3, nout), &
            long_name= "Heat flux density, see Ball et al PPCF 58 045023 2016")
       call neasyf_write(file_id, "es_phi_dist", phi_dist, &
            dim_names=phi_dist_dim, start=starts(3, nout))
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(part_fluxes_dist)
    UNUSED_DUMMY(mom_fluxes_par_dist); UNUSED_DUMMY(mom_fluxes_perp_dist)
    UNUSED_DUMMY(heat_fluxes_dist); UNUSED_DUMMY(phi_dist)
# endif
  end subroutine nc_loop_dist
  
  !> Write the momentum flux as a function of \((v_\parallel, \theta, t)\) to netCDF
  subroutine nc_loop_sym (file_id, nout, vflx_sym)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
    implicit none
# endif
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: vflx_sym
# ifdef NETCDF
    
    if (has_phi) then
       call neasyf_write(file_id, "es_mom_sym", vflx_sym, &
            dim_names=sym_dims, start=starts(4, nout), &
            long_name="Momentum flux density as a function theta and vspace, used for looking at the effect of asymmetry, &
            & see Parra et al POP 18 062501 2011")
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(vflx_sym)
# endif
  end subroutine nc_loop_sym
  
  !> Write the particle flux contribution to toroidal momentum flux to netCDF
  subroutine nc_loop_partsym_tormom (file_id, nout, partflx_sym)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    real, dimension (:,:,:), intent (in) :: partflx_sym
# ifdef NETCDF
    if (has_phi) then
       call neasyf_write(file_id, "es_part_tormom_sym", partflx_sym, &
            dim_names=sym_dims, start=starts(4, nout), &
            long_name="Particle flux density as a function theta and vspace, used for looking at the effect of asymmetry, &
            & see Parra et al POP 18 062501 2011 and ask Jung-Pyo Lee")
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(partflx_sym)
# endif
  end subroutine nc_loop_partsym_tormom

  !> Write the correlation function over the physical domain to netCDF
  subroutine nc_loop_corr (file_id, nout, phi_2pi_corr)
# ifdef NETCDF
    use run_parameters, only: has_phi
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    !> The correlation function over the physical domain
    complex, dimension (:,:), intent (in) :: phi_2pi_corr
# ifdef NETCDF

    if (has_phi) then
       call netcdf_write_complex(file_id, "phi_corr_2pi", phi_2pi_corr, &
            dim_names=[complex_dim, theta_dim, ky_dim, time_dim], start=starts(4, nout), &
            long_name="2 point correlation function calculated from the electric potential")
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(phi_2pi_corr)
# endif
  end subroutine nc_loop_corr

  !> Write the correlation function over the extended domain to netCDF
  subroutine nc_loop_corr_extend (file_id, nout_big, time, phi_corr, phi2_extend)
# ifdef NETCDF
    use run_parameters, only: has_phi
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> The current output timestep for "large" diagnostics
    integer, intent (in) :: nout_big
    !> Current simulation time
    real, intent(in) :: time
    real, dimension (:,:,:), intent (in) :: phi2_extend
    complex, dimension (:,:,:), intent (in) :: phi_corr
# ifdef NETCDF
    character(*), dimension(*), parameter :: phi_corr_dims = &
         [complex_dim, theta_extended_dim, kx_dim, ky_dim, time_big_dim]
    character(*), dimension(*), parameter :: phi2_extend_dims = &
         [theta_extended_dim, kx_dim, ky_dim, time_big_dim]

    if (has_phi) then
      call neasyf_write(file_id, trim(time_big_dim), time, dim_names=[time_big_dim], start=[nout_big])

       call netcdf_write_complex(file_id, "phi_corr", phi_corr, &
            dim_names=phi_corr_dims, start=starts(5, nout_big), &
            long_name="2 point correlation function calculated from the electric potential, time averaged")
       call neasyf_write(file_id, "phi2_extend", phi2_extend, &
            dim_names=phi2_extend_dims, start=starts(4, nout_big))
    end if
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout_big); UNUSED_DUMMY(time); UNUSED_DUMMY(phi_corr)
    UNUSED_DUMMY(phi2_extend)
# endif
  end subroutine nc_loop_corr_extend

  !> Write various fields, heating, and frequency diagnostics to netCDF
  subroutine nc_loop (file_id, nout, time, &
       phi2, apar2, bpar2, igomega, &
       h, hk, omega, omegaavg, woutunits, tunits, phitot, ql_metric, &
       write_omega, write_heating, write_ql_metric)
    use gs2_heating, only: heating_diagnostics
# ifdef NETCDF
    use run_parameters, only: has_phi, has_apar, has_bpar, wstar_units
    use fields_arrays, only: phinew, aparnew, bparnew
    use parameter_scan_arrays, only: write_scan_parameter,current_scan_parameter_value
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    !> Current simulation time
    real, intent (in) :: time
    !> Volume average of fields squared
    real, intent(out) :: phi2, apar2, bpar2
    !> Theta-index to measure fields at
    integer, intent(in) :: igomega
    !> Normalisation factor for frequency
    real, dimension (:), intent (in) :: woutunits
    !> Normalisation factor for time
    real, dimension (:), intent (in) :: tunits
    !> Fields, frequency and time-averaged frequency
    complex, dimension(:,:), intent (in) :: omega, omegaavg
    !> FIXME: add documentation
    real, dimension(:,:), intent (in) :: phitot, ql_metric
    !> Heating diagnostics
    type(heating_diagnostics), intent (in) :: h
    !> Heating per-mode
    type(heating_diagnostics), dimension(:,:), intent (in) :: hk
    logical, intent(in) :: write_omega, write_heating, write_ql_metric
# ifdef NETCDF

    call neasyf_write(file_id, time_dim, time, dim_names=[time_dim], start=[nout])

    if (wstar_units) then
       call neasyf_write(file_id, "t_wstar", time * tunits, dim_names=[ky_dim, time_dim], &
            start=[1, nout], long_name="Time (wstar)", units="L/vt")
    end if

    if (has_phi) then
       call nc_write_field(file_id, nout, phinew, "phi", "Electrostatic potential", &
            igomega, write_phi_t, phi2)
    end if

    if (has_apar) then
       call nc_write_field(file_id, nout, aparnew, "apar", "Parallel magnetic potential", &
            igomega, write_apar_t, apar2)
    end if

    if (has_bpar) then
       call nc_write_field(file_id, nout, bparnew, "bpar", "Parallel magnetic field", &
            igomega, write_bpar_t, bpar2)
    end if

    if (write_heating) call nc_write_heating(file_id, nout, h, hk)

    if (write_omega) call nc_write_omega(file_id, nout, omega, omegaavg, woutunits)

    if (write_ql_metric) call nc_write_ql_metric(file_id, nout, ql_metric)

    if (write_scan_parameter) then
       call neasyf_write(file_id, &
            "current_scan_parameter_value", &
            current_scan_parameter_value, &
            dim_names=[time_dim], start=[nout], &
            long_name="The current value of the scan parameter")
     end if

    call neasyf_write(file_id, "phtot", phitot, dim_names=mode_dims, start=starts(3, nout), &
         long_name="Roughly the field line averaged summed field amplitudes")
# else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(time)
    UNUSED_DUMMY(phi2); UNUSED_DUMMY(apar2); UNUSED_DUMMY(bpar2); UNUSED_DUMMY(igomega)
    UNUSED_DUMMY(h); UNUSED_DUMMY(hk); UNUSED_DUMMY(omega); UNUSED_DUMMY(omegaavg)
    UNUSED_DUMMY(woutunits); UNUSED_DUMMY(tunits); UNUSED_DUMMY(phitot)
    UNUSED_DUMMY(ql_metric); UNUSED_DUMMY(write_omega); UNUSED_DUMMY(write_heating)
    UNUSED_DUMMY(write_ql_metric)
# endif
  end subroutine nc_loop

  !> Writing the (normalised) complex frequency to netCDF
  subroutine nc_write_omega(file_id, nout, omega, omega_average, woutunits)
#ifdef NETCDF
    use kt_grids, only: ntheta0, naky
#endif
    !> NetCDF ID of the file
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    !> Normalisation factor for frequency
    real, dimension (:), intent (in) :: woutunits
    !> Fields, frequency and time-averaged frequency
    complex, dimension(:,:), intent (in) :: omega, omega_average
#ifdef NETCDF
    complex, dimension (ntheta0, naky) :: tmp
    integer :: it

    do it = 1, ntheta0
      tmp(it, :) = omega(it, :) * woutunits
    end do
    call netcdf_write_complex(file_id, "omega", tmp, dim_names=omega_dims, start=starts(4, nout), &
         long_name="Complex frequency", units="aref/vtref")

    do it = 1, ntheta0
      tmp(it, :) = omega_average(it, :) * woutunits
    end do

    ! FIXME: duplicate names for old/new diagnostics
    call netcdf_write_complex(file_id, "omegaavg", tmp, dim_names=omega_dims, start=starts(4, nout), &
         long_name="Complex frequency averaged over navg timesteps", units="aref/vtref")
    call netcdf_write_complex(file_id, "omega_average", tmp, dim_names=omega_dims, start=starts(4, nout), &
         long_name="Complex frequency averaged over navg timesteps", units="aref/vtref")
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(omega)
    UNUSED_DUMMY(omega_average); UNUSED_DUMMY(woutunits)
#endif
  end subroutine nc_write_omega

  !> Writes the QL flux metric by mode number and summed over mode number to netcdf file
  subroutine nc_write_ql_metric(file_id, nout, ql_metric)
#ifdef NETCDF
    use neasyf, only: neasyf_write
#endif
    !> NetCDF ID of the file
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent (in) :: nout
    !> The QL flux metric as a function of kx and ky
    real, dimension(:,:), intent (in) :: ql_metric
#ifdef NETCDF
    character(*), dimension(*), parameter :: ql_dims = [kx_dim, ky_dim, time_dim]

    call neasyf_write(file_id, "ql_metric_by_mode", ql_metric, dim_names=ql_dims, &
         start=starts(3, nout), long_name="QL flux metric by mode number")
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(ql_metric)
#endif
  end subroutine nc_write_ql_metric

  !> Write a field ((/E, A_\Vert, B_\vert/)) to netCDF
  subroutine nc_write_field(file_id, nout, field, field_name, long_name, igomega, write_field_over_time, field2)
# ifdef NETCDF
    use kt_grids, only: naky, ntheta0
    use neasyf, only: neasyf_write
    use volume_averages, only: average_theta, average_kx, average_ky
# endif
    use theta_grid, only: ntgrid
    !> NetCDF ID of the file
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent(in) :: nout
    !> One of the fields (/E, A_\Vert, B_\vert/)
    complex, dimension(-ntgrid:, :, :), intent(in) :: field
    !> Name of the field
    character(*), intent(in) :: field_name
    !> Description of the field
    character(*), intent(in) :: long_name
    !> \(\theta\)-index to measure field at
    integer, intent(in) :: igomega
    !> Should entire field be written every timestep?
    logical, intent(in) :: write_field_over_time
    !> Volume-average of the field amplitude squared
    real, intent(out) :: field2
# ifdef NETCDF
    real, dimension(ntheta0, naky) :: field2_by_mode
    real, dimension(ntheta0) :: field2_by_kx
    real, dimension(naky) :: field2_by_ky

    character(*), dimension(*), parameter :: field_dims = [complex_dim, theta_dim, kx_dim, ky_dim, time_dim]
    character(*), dimension(*), parameter :: kx_dims = [kx_dim, time_dim]
    character(*), dimension(*), parameter :: ky_dims = [ky_dim, time_dim]

    ! Note that we're not using `get_vol_average` because we'll end up
    ! with a circular dependency
    call average_theta(field, field, field2_by_mode, distributed=.false.)

    if (write_field_over_time) then
      call netcdf_write_complex(file_id, field_name // "_t", field, &
           dim_names=field_dims, start=starts(5, nout), &
           long_name=long_name // " over time")
    end if

    call average_ky(field2_by_mode, field2_by_kx, distributed=.false.)
    call neasyf_write(file_id, field_name // "2_by_kx", field2_by_kx, dim_names=kx_dims, &
         start=starts(2, nout), long_name=long_name // " squared and averaged over theta and ky")

    call average_kx(field2_by_mode, field2_by_ky, distributed=.false.)
    call neasyf_write(file_id, field_name // "2_by_ky", field2_by_ky, dim_names=ky_dims, &
         start=starts(2, nout), long_name=long_name // " squared and averaged over theta and kx")

    field2 = sum(field2_by_kx)

    ! FIXME: duplicated variable, old/new diagnostics
    call netcdf_write_complex(file_id, field_name // "0", field(igomega, :, :), &
         dim_names=omega_dims, start=starts(4, nout), &
         long_name=long_name // " at ig=igomega")
    call netcdf_write_complex(file_id, field_name // "_igomega_by_mode", field(igomega, :, :), &
         dim_names=omega_dims, start=starts(4, nout), &
         long_name=long_name // " at ig=igomega")

    call neasyf_write(file_id, field_name // "2_by_mode", field2_by_mode, &
         dim_names=mode_dims, start=starts(3, nout), &
         long_name=long_name // " per Fourier mode")
    call neasyf_write(file_id, field_name // "2", field2, dim_names=[time_dim], start=[nout], &
         long_name=long_name // " volume average", units="(T_ref/q_ref rho_ref/a_ref)**2")

    call nc_final_fields(file_id)
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(field); UNUSED_DUMMY(field_name)
    UNUSED_DUMMY(long_name); UNUSED_DUMMY(igomega); UNUSED_DUMMY(write_field_over_time)
    UNUSED_DUMMY(field2)
#endif
  end subroutine nc_write_field

  !> Writing heating diagnostics to netCDF
  subroutine nc_write_heating(file_id, nout, h, hk)
    use gs2_heating, only: heating_diagnostics
#ifdef NETCDF
    use kt_grids, only: naky, ntheta0
    use species, only: nspec
    use gs2_heating, only: hk_repack
    use neasyf, only: neasyf_write
#endif
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent(in) :: nout
    !> Heating diagnostics
    type(heating_diagnostics), intent (in) :: h
    !> Heating per-mode
    type(heating_diagnostics), dimension(:,:), intent (in) :: hk
#ifdef NETCDF
    real, dimension (ntheta0, naky, nspec) :: tmps
    ! Note: Here parentheses around variables passed to neasyf_write are an intentional
    ! workaround to a gfortran bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105658
    call neasyf_write(file_id, "h_energy", (h%energy), dim_names=[time_dim], start=[nout], &
         long_name="Total free (turbulent) energy")
    call neasyf_write(file_id, "h_energy_dot", (h%energy_dot), dim_names=[time_dim], start=[nout], &
         long_name="Time derivative of total free energy")
    call neasyf_write(file_id, "h_antenna", (h%antenna), dim_names=[time_dim], start=[nout], &
         long_name="Energy injection from antenna")
    call neasyf_write(file_id, "h_eapar", (h%eapar), dim_names=[time_dim], start=[nout], &
         long_name="Free energy in perpendicular magnetic field")
    call neasyf_write(file_id, "h_ebpar", (h%ebpar), dim_names=[time_dim], start=[nout], &
         long_name="Free energy in parallel magnetic field")
    call neasyf_write(file_id, "h_delfs2", (h%delfs2), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Total free energy dfs^2")
    call neasyf_write(file_id, "h_hs2", (h%hs2), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Non-adiabatic free energy hs^2")
    call neasyf_write(file_id, "h_phis2", (h%phis2), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Adiabatic free energy phi_bar^2")
    call neasyf_write(file_id, "h_hypervisc", (h%hypervisc), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Heating from hyperviscosity")
    call neasyf_write(file_id, "h_hyperres", (h%hyperres), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Heating from hyperresitivity")
    call neasyf_write(file_id, "h_collisions", (h%collisions), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Heating from collisions -[h C(h) * T_0]")
    call neasyf_write(file_id, "h_gradients", (h%gradients), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Heating from gradient")
    call neasyf_write(file_id, "h_imp_colls", (h%imp_colls), dim_names=flux_dims, start=starts(2, nout), &
         long_name="[h_(i+1)*h_*]/2 * C[h_(i+1)] * T_0")
    call neasyf_write(file_id, "h_hypercoll", (h%hypercoll), dim_names=flux_dims, start=starts(2, nout))
    call neasyf_write(file_id, "h_heating", (h%heating), dim_names=flux_dims, start=starts(2, nout), &
         long_name="Total heating by species")

    call neasyf_write(file_id, "hk_energy", (hk%energy), dim_names=mode_dims, start=starts(3, nout))
    call neasyf_write(file_id, "hk_energy_dot", (hk%energy_dot), dim_names=mode_dims, start=starts(3, nout))
    call neasyf_write(file_id, "hk_antenna", (hk%antenna), dim_names=mode_dims, start=starts(3, nout))
    call neasyf_write(file_id, "hk_eapar", (hk%eapar), dim_names=mode_dims, start=starts(3, nout))
    call neasyf_write(file_id, "hk_ebpar", (hk%ebpar), dim_names=mode_dims, start=starts(3, nout))

    call hk_repack (hk, 1, tmps)
    call neasyf_write(file_id, "hk_hypervisc", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 2, tmps)
    call neasyf_write(file_id, "hk_hyperres", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 3, tmps)
    call neasyf_write(file_id, "hk_collisions", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 4, tmps)
    call neasyf_write(file_id, "hk_gradients", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 5, tmps)
    call neasyf_write(file_id, "hk_heating", tmps, dim_names=fluxk_dims, start=starts(4, nout), &
         long_name="Total heating by species and mode")
    call hk_repack (hk, 6, tmps)
    call neasyf_write(file_id, "hk_hypercoll", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 7, tmps)
    call neasyf_write(file_id, "hk_delfs2", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 8, tmps)
    call neasyf_write(file_id, "hk_hs2", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 9, tmps)
    call neasyf_write(file_id, "hk_phis2", tmps, dim_names=fluxk_dims, start=starts(4, nout))
    call hk_repack (hk, 10, tmps)
    call neasyf_write(file_id, "hk_imp_colls", tmps, dim_names=fluxk_dims, start=starts(4, nout))
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(h); UNUSED_DUMMY(hk)
#endif
  end subroutine nc_write_heating

  !> Outputs full not guiding center moments
  subroutine nc_loop_fullmom (file_id, nout, ntot0, density0, upar0, tpar0, tperp0)
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current timestep
    integer, intent (in) :: nout
    complex, dimension(:,:,:), intent(in) :: ntot0, density0, upar0, tpar0, tperp0
#ifdef NETCDF
    integer, dimension (5) :: startmom
    character(*), dimension(*), parameter :: mom_dim = [complex_dim, kx_dim, ky_dim, species_dim, time_dim]

    startmom = [1, 1, 1, 1, nout]

    call netcdf_write_complex(file_id, "ntot0", ntot0, mom_dim, start=startmom, &
         long_name="Total perturbed density (theta=0) over time")
    call netcdf_write_complex(file_id, "density0", density0, mom_dim, start=startmom, &
         long_name="Nonadiabatic piece of perturbed density (theta=0) over time")
    call netcdf_write_complex(file_id, "upar0", upar0, mom_dim, start=startmom, &
         long_name="Upar (theta=0) over time")
    call netcdf_write_complex(file_id, "tpar0", tpar0, mom_dim, start=startmom, &
         long_name="Tpar (theta=0) over time")
    call netcdf_write_complex(file_id, "tperp0", tperp0, mom_dim, start=startmom, &
         long_name="Tperp (theta=0) over time")
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(ntot0); UNUSED_DUMMY(density0)
    UNUSED_DUMMY(upar0); UNUSED_DUMMY(tpar0); UNUSED_DUMMY(tperp0)
#endif
  end subroutine nc_loop_fullmom

  !> Write \(\phi, A_\parallel, B_\parallel\) as functions of real space
  subroutine nc_loop_movie(file_id, nout_movie, time)
# ifdef NETCDF
    use fields_arrays, only: phinew, aparnew, bparnew
    use gs2_layouts, only: yxf_lo
    use gs2_transforms, only: transform2
    use run_parameters, only: has_phi, has_apar, has_bpar
    use theta_grid, only: ntgrid
    use neasyf, only: neasyf_write
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current movie timestep
    integer, intent(in) :: nout_movie
    !> Current simulation time
    real, intent (in) :: time
# ifdef NETCDF
    ! \(\phi, A_\parallel, B_\parallel\) as functions of real space
    real, dimension (:,:,:), allocatable:: yxphi, yxapar, yxbpar
    character(*), dimension(*), parameter :: xmode_dim = [x_dim, y_dim, theta_dim, time_movie_dim]

    call neasyf_write(file_id, trim(time_movie_dim), time, dim_names=[time_movie_dim], start=[nout_movie])

    if (has_phi) then
       allocate(yxphi(yxf_lo%nx, yxf_lo%ny, -ntgrid:ntgrid))
       call transform2 (phinew, yxphi, yxf_lo%ny, yxf_lo%nx)
       call neasyf_write(file_id, "phi_by_xmode", yxphi, &
            dim_names=xmode_dim, start=starts(4, nout_movie))
       deallocate(yxphi)
    end if

    if (has_apar) then
       allocate(yxapar(yxf_lo%nx, yxf_lo%ny, -ntgrid:ntgrid))
       call transform2 (aparnew, yxapar, yxf_lo%ny, yxf_lo%nx)
       call neasyf_write(file_id, "apar_by_xmode", yxapar, &
            dim_names=xmode_dim, start=starts(4, nout_movie))
       deallocate(yxapar)
    end if

    if (has_bpar) then
       allocate(yxbpar(yxf_lo%nx, yxf_lo%ny, -ntgrid:ntgrid))
       call transform2 (bparnew, yxbpar, yxf_lo%ny, yxf_lo%nx)
       call neasyf_write(file_id, "bpar_by_xmode", yxbpar, &
            dim_names=xmode_dim, start=starts(4, nout_movie))
       deallocate(yxbpar)
    end if
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout_movie); UNUSED_DUMMY(time)
# endif
  end subroutine nc_loop_movie

  !> Sync the files every nsync writes
  subroutine nc_sync(file_id, nout, movie_file_id, nout_movie, nsync_freq)
# ifdef NETCDF
    use netcdf, only: nf90_sync, NF90_NOERR
    use netcdf_utils, only: netcdf_error
# endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
    !> Current output timestep
    integer, intent(in) :: nout
    !> NetCDF ID of the movie file to write to
    integer, intent(in) :: movie_file_id
    !> Current output timestep for real-space fields
    integer, intent(in) :: nout_movie
    !> Timestep period at which to actually flush files
    integer, intent(in), optional :: nsync_freq
# ifdef NETCDF
    integer, parameter :: nsync_default = 10
    integer :: status, nsync

    nsync = nsync_default
    if (present(nsync_freq)) nsync = nsync_freq

    !Main file
    if(mod(nout,nsync) == 0) then
       status = nf90_sync (file_id)
       if (status /= NF90_NOERR) call netcdf_error (status)
    endif

    !Movie file
    if (movie_file_id >= 0) then
       if (mod(nout_movie, nsync) == 0) then
          status = nf90_sync(movie_file_id)
          if (status /= NF90_NOERR) call netcdf_error(status)
       endif
    endif
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(nout); UNUSED_DUMMY(movie_file_id)
    UNUSED_DUMMY(nout_movie);  UNUSED_DUMMY(nsync_freq)
# endif
  end subroutine nc_sync

  !> Write [[species:spec]] to output netCDF file
  subroutine nc_species(file_id)
#ifdef NETCDF
    use species, only: spec
    use neasyf, only: neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
#ifdef NETCDF
    ! Note: Here parentheses around variables passed to neasyf_write are an intentional
    ! workaround to a gfortran bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105658
    call neasyf_write(file_id, "charge", (spec%z), dim_names=["species"], &
         long_name="Charge", units="e")
    call neasyf_write(file_id, "mass", (spec%mass), dim_names=["species"], &
         long_name="Atomic mass", units="AMU")
    call neasyf_write(file_id, "dens", (spec%dens), dim_names=["species"], &
         long_name="Normalised density", units="nref")
    call neasyf_write(file_id, "temp", (spec%temp), dim_names=["species"], &
         long_name="Normalised temperature", units="Tref")
    call neasyf_write(file_id, "tprim", (spec%tprim), dim_names=["species"], &
         long_name="Normalised temperature gradient scale length -1/rho dT/drho", units="1/aref")
    call neasyf_write(file_id, "fprim", (spec%fprim), dim_names=["species"], &
         long_name="Normalised density gradient scale length -Lref/n dn/drho", units="1/aref")
    call neasyf_write(file_id, "uprim", (spec%uprim), dim_names=["species"], &
         long_name="Normalised parallel velocity gradient scale length -1/v_t du_par/drho", units="1/aref")
    call neasyf_write(file_id, "uprim2", (spec%uprim2), dim_names=["species"])
    call neasyf_write(file_id, "vnewk", (spec%vnewk), dim_names=["species"], &
         long_name="Collisionality", units="vtref/aref")

    ! FIXME: duplicated variable, old/new diagnostics
    call neasyf_write(file_id, "type_of_species", (spec%type), dim_names=["species"], &
         long_name="Species type: 1=ion, 2=electron, 3=trace, 4=hybrid_electron")
    call neasyf_write(file_id, "type", (spec%type), dim_names=["species"], &
         long_name="Species type: 1=ion, 2=electron, 3=trace, 4=hybrid_electron")

    call neasyf_write(file_id, "f0type", (spec%f0type), dim_names=["species"], &
         long_name="Species type: 1=Maxwellian, 2=Tabulated, 3=Analytic slowing down")
    call neasyf_write(file_id, "nu_h", (spec%nu_h), dim_names=["species"], &
         long_name="Hypervicsous collision factor")
    call neasyf_write(file_id, "bess_fac", (spec%bess_fac), dim_names=["species"], &
         long_name="Artificial factor scaling Bessel function arguments")
    ! Parameters used in slowing down calculation
    call neasyf_write(file_id, "vcrit", (spec%vcrit), dim_names=["species"])
    call neasyf_write(file_id, "vcprim", (spec%vcprim), dim_names=["species"])
    ! Parameters only used for certain initialisation options
    call neasyf_write(file_id, "dens0", (spec%dens0), dim_names=["species"])
    call neasyf_write(file_id, "u0", (spec%u0), dim_names=["species"])
    call neasyf_write(file_id, "tpar0", (spec%tpar0), dim_names=["species"])
    call neasyf_write(file_id, "tperp0", (spec%tperp0), dim_names=["species"])
#else
    UNUSED_DUMMY(file_id)
#endif
  end subroutine nc_species
  
  !> Write various geometric quantities to output netCDF file
  subroutine nc_geo(file_id)
#ifdef NETCDF
    use theta_grid, only: bmag, gradpar, gbdrift, gbdrift0, &
         cvdrift, cvdrift0, gds2, gds21, gds22, gds23, gds24, gds24_noq, grho, jacob, &
         shat, drhodpsi, eps_trapped, cdrift, cdrift0, qval, rplot, &
         zplot, aplot, rprime, zprime, aprime, kxfac, bpol !, surfarea
    use kt_grids, only: kperp2
    use run_parameters, only: beta
    use neasyf, only: neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
#ifdef NETCDF
    call neasyf_write(file_id, "bmag", bmag, dim_names=["theta"], &
         long_name="Magnitude of magnetic field", units="B_0")
    call neasyf_write(file_id, "bpol", bpol, dim_names=["theta"], &
         long_name="Poloidal magnetic field", units="B_0")
    call neasyf_write(file_id, "kxfac", kxfac, long_name="kxfac factor used in NL term")
    call neasyf_write(file_id, "gradpar", gradpar, dim_names=["theta"], &
         long_name="Parallel derivative multiplier")
    call neasyf_write(file_id, "gbdrift", gbdrift, dim_names=["theta"], &
         long_name="Magnetic gradient drift")
    call neasyf_write(file_id, "gbdrift0", gbdrift0, dim_names=["theta"])
    call neasyf_write(file_id, "cvdrift", cvdrift, dim_names=["theta"])
    call neasyf_write(file_id, "cvdrift0", cvdrift0, dim_names=["theta"])
    call neasyf_write(file_id, "gds2", gds2, dim_names=["theta"])
    call neasyf_write(file_id, "gds21", gds21, dim_names=["theta"])
    call neasyf_write(file_id, "gds22", gds22, dim_names=["theta"])
    call neasyf_write(file_id, "gds23", gds23, dim_names=["theta"])
    call neasyf_write(file_id, "gds24", gds24, dim_names=["theta"])
    call neasyf_write(file_id, "gds24_noq", gds24_noq, dim_names=["theta"])
    call neasyf_write(file_id, "grho", grho, dim_names=["theta"])
    call neasyf_write(file_id, "jacob", jacob, dim_names=["theta"])
    call neasyf_write(file_id, "shat", shat, &
         long_name="(rho/q) dq/drho")
    call neasyf_write(file_id, "drhodpsi", drhodpsi, &
         long_name="drho/dPsi")
    call neasyf_write(file_id, "eps_trapped", eps_trapped, &
         long_name="Trapped fraction 1 - sqrt(bmin/bmax)")
    call neasyf_write(file_id, "cdrift", cdrift, dim_names=["theta"])
    call neasyf_write(file_id, "cdrift0", cdrift0, dim_names=["theta"])
    call neasyf_write(file_id, "qval", qval, &
         long_name="Local safety factor")
    call neasyf_write(file_id, "beta", beta, &
         long_name="Reference beta", units="2.mu0.nref.Tref/B_a**2")
    call neasyf_write(file_id, "rplot", Rplot, dim_names=["theta"], &
         long_name="The major radius at the centre of the flux tube", units="a")
    call neasyf_write(file_id, "zplot", Zplot, dim_names=["theta"], &
         long_name="The height above the midplane at the centre of the flux tube", units="a")
    call neasyf_write(file_id, "aplot", aplot, dim_names=["theta"], &
         long_name="The toroidal angle at the centre of the flux tube", units="rad")
    call neasyf_write(file_id, "rprime", Rprime, dim_names=["theta"], &
         long_name="d/dx of the major radius at the centre of the flux tube", units="a/rho_r")
    call neasyf_write(file_id, "zprime", Zprime, dim_names=["theta"], &
         long_name="d/dx of the height above the midplane at the centre of the flux tube", units="a/rho_r")
    call neasyf_write(file_id, "aprime", aprime, dim_names=["theta"], &
         long_name="d/dx of the toroidal angle at the centre of the flux tube", units="rad/rho_r")

    ! Wavenumber related quantities -- not strictly geometrical information.
    call neasyf_write(file_id, "kperp2", kperp2, dim_names=[theta_dim, kx_dim, ky_dim], &
         long_name="The perpendicular wavenumber squared", units="1/rho_r^2")
    ! FIXME: check if we need surfarea?
#else
    UNUSED_DUMMY(file_id)
#endif
  end subroutine nc_geo

  !> Write various quantities related to BC to output netCDF file
  subroutine nc_bc(file_id)
#ifdef NETCDF
    use dist_fn, only: supercell_labels
    use neasyf, only: neasyf_write
#endif
    implicit none
    !> NetCDF ID of the file to write to
    integer, intent(in) :: file_id
#ifdef NETCDF
    call neasyf_write(file_id, "supercell_labels", supercell_labels, dim_names=[kx_dim, ky_dim], &
         long_name="Labels which unique supercell each wavenumber pair belongs to", units="N/A")
#else
    UNUSED_DUMMY(file_id)
#endif
  end subroutine nc_bc

  !> Wrapper around `nf90_inquire_dimension`. Aborts if any errors are
  !> detected.
  function get_dim_length(file_id, name) result(length)
#ifdef NETCDF
    use netcdf, only: nf90_inq_dimid, nf90_inquire_dimension, NF90_MAX_NAME
    use neasyf, only: neasyf_error
#endif
    integer :: length
    !> NetCDF ID of the file
    integer, intent(in) :: file_id
    !> Name of the dimension
    character(len=*), intent(in) :: name
#ifdef NETCDF
    integer :: dim_id
    ! Temporary variable for the dimension name
    character(len=NF90_MAX_NAME) :: dim_name
    ! NetCDF error status
    integer :: status

    status = nf90_inq_dimid(file_id, trim(name), dim_id)
    call neasyf_error(status, file_id)

    status = nf90_inquire_dimension(file_id, dim_id, dim_name, length)
    call neasyf_error(status, file_id, dimid=dim_id, dim=name)
#else
    UNUSED_DUMMY(file_id); UNUSED_DUMMY(name)
    length = 0
#endif
  end function get_dim_length

  subroutine write_complex_scalar(parent_id, name, values, dim_names, units, long_name, start)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use convert, only: c2r
#endif
    !> Name of the variable
    character(len=*), intent(in) :: name
    !> NetCDF ID of the parent group/file
    integer, intent(in) :: parent_id
    !> Value of the integer to write
    complex, intent(in) :: values
    !> Array of dimension names, the first of which should be `"ri"`
    character(len=*), dimension(:), intent(in) :: dim_names
    !> Units of coordinate
    character(len=*), optional, intent(in) :: units
    !> Long descriptive name
    character(len=*), optional, intent(in) :: long_name
    !> Offset (one-indexed) for writing time-varying arrays. Must include the complex dimension as the first index
    integer, dimension(:), optional, intent(in) :: start
#ifdef NETCDF
    call neasyf_write(parent_id, name, [real(values), aimag(values)], &
         dim_names=dim_names, units=units, long_name=long_name, start=start)
#else
    UNUSED_DUMMY(parent_id); UNUSED_DUMMY(name); UNUSED_DUMMY(values)
    UNUSED_DUMMY(units); UNUSED_DUMMY(long_name); UNUSED_DUMMY(start)
    ! Bug in gcc gives a supurious warning for UNUSED_DUMMY on character(len=*) arrays
    if (.false.) write(*,*) dim_names
#endif
  end subroutine write_complex_scalar

  subroutine write_complex_rank1(parent_id, name, values, dim_names, units, long_name, start)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use convert, only: c2r
#endif
    !> Name of the variable
    character(len=*), intent(in) :: name
    !> NetCDF ID of the parent group/file
    integer, intent(in) :: parent_id
    !> Value of the integer to write
    complex, dimension(:), intent(in) :: values
    !> Array of dimension names, the first of which should be `"ri"`
    character(len=*), dimension(:), intent(in) :: dim_names
    !> Units of coordinate
    character(len=*), optional, intent(in) :: units
    !> Long descriptive name
    character(len=*), optional, intent(in) :: long_name
    !> Offset (one-indexed) for writing time-varying arrays. Must include the complex dimension as the first index 
    integer, dimension(:), optional, intent(in) :: start
#ifdef NETCDF
    real, dimension(2, size(values, 1)) :: real_values
    call c2r(values, real_values)
    call neasyf_write(parent_id, name, real_values, dim_names=dim_names, units=units, long_name=long_name, start=start)
#else
    UNUSED_DUMMY(parent_id); UNUSED_DUMMY(name); UNUSED_DUMMY(values)
    UNUSED_DUMMY(units); UNUSED_DUMMY(long_name); UNUSED_DUMMY(start)
    ! Bug in gcc gives a supurious warning for UNUSED_DUMMY on character(len=*) arrays
    if (.false.) write(*,*) dim_names
#endif
  end subroutine write_complex_rank1

  subroutine write_complex_rank2(parent_id, name, values, dim_names, units, long_name, start)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use convert, only: c2r
#endif
    !> Name of the variable
    character(len=*), intent(in) :: name
    !> NetCDF ID of the parent group/file
    integer, intent(in) :: parent_id
    !> Value of the integer to write
    complex, dimension(:, :), intent(in) :: values
    !> Array of dimension names
    character(len=*), dimension(:), intent(in) :: dim_names
    !> Units of coordinate
    character(len=*), optional, intent(in) :: units
    !> Long descriptive name
    character(len=*), optional, intent(in) :: long_name
    integer, dimension(:), optional, intent(in) :: start
#ifdef NETCDF
    real, dimension(2, size(values, 1), size(values, 2)) :: real_values
    call c2r(values, real_values)
    call neasyf_write(parent_id, name, real_values, dim_names=dim_names, units=units, long_name=long_name, start=start)
#else
    UNUSED_DUMMY(parent_id); UNUSED_DUMMY(name); UNUSED_DUMMY(values)
    UNUSED_DUMMY(units); UNUSED_DUMMY(long_name); UNUSED_DUMMY(start)
    ! Bug in gcc gives a supurious warning for UNUSED_DUMMY on character(len=*) arrays
    if (.false.) write(*,*) dim_names
#endif
  end subroutine write_complex_rank2

  subroutine write_complex_rank3(parent_id, name, values, dim_names, units, long_name, start)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use convert, only: c2r
#endif
    !> Name of the variable
    character(len=*), intent(in) :: name
    !> NetCDF ID of the parent group/file
    integer, intent(in) :: parent_id
    !> Value of the integer to write
    complex, dimension(:, :, :), intent(in) :: values
    !> Array of dimension names
    character(len=*), dimension(:), intent(in) :: dim_names
    !> Units of coordinate
    character(len=*), optional, intent(in) :: units
    !> Long descriptive name
    character(len=*), optional, intent(in) :: long_name
    integer, dimension(:), optional, intent(in) :: start
#ifdef NETCDF
    real, dimension(2, size(values, 1), size(values, 2), size(values, 3)) :: real_values
    call c2r(values, real_values)
    call neasyf_write(parent_id, name, real_values, dim_names=dim_names, units=units, long_name=long_name, start=start)
#else
    UNUSED_DUMMY(parent_id); UNUSED_DUMMY(name); UNUSED_DUMMY(values)
    UNUSED_DUMMY(units); UNUSED_DUMMY(long_name); UNUSED_DUMMY(start)
    ! Bug in gcc gives a supurious warning for UNUSED_DUMMY on character(len=*) arrays
    if (.false.) write(*,*) dim_names
#endif
  end subroutine write_complex_rank3

  subroutine write_complex_rank4(parent_id, name, values, dim_names, units, long_name, start)
#ifdef NETCDF
    use neasyf, only: neasyf_write
    use convert, only: c2r
#endif
    !> Name of the variable
    character(len=*), intent(in) :: name
    !> NetCDF ID of the parent group/file
    integer, intent(in) :: parent_id
    !> Value of the integer to write
    complex, dimension(:, :, :, :), intent(in) :: values
    !> Array of dimension names
    character(len=*), dimension(:), intent(in) :: dim_names
    !> Units of coordinate
    character(len=*), optional, intent(in) :: units
    !> Long descriptive name
    character(len=*), optional, intent(in) :: long_name
    integer, dimension(:), optional, intent(in) :: start
#ifdef NETCDF
    real, dimension(2, size(values, 1), size(values, 2), size(values, 3), size(values, 4)) :: real_values
    call c2r(values, real_values)
    call neasyf_write(parent_id, name, real_values, dim_names=dim_names, units=units, long_name=long_name, start=start)
#else
    UNUSED_DUMMY(parent_id); UNUSED_DUMMY(name); UNUSED_DUMMY(values)
    UNUSED_DUMMY(units); UNUSED_DUMMY(long_name); UNUSED_DUMMY(start)
    ! Bug in gcc gives a supurious warning for UNUSED_DUMMY on character(len=*) arrays
    if (.false.) write(*,*) dim_names
#endif
  end subroutine write_complex_rank4
end module gs2_io