geo_utils.fpp Source File


Contents

Source Code


Source Code

#include "unused_dummy.inc"

!> A module to provide utilities used in geometry calculations.
!> Mainly to provide an abstract type used as a base for specific
!> geometry implementations.
module geo_utils
  use splines, only: spline, periodic_spline
  implicit none

  public :: abstract_geo_type, abstract_eqfile_geo_type, abstract_eqfile_cart_geo_type
  public :: geo_input_type, flux_surface_type, mod2pi, sort, find_lower_bound
  public :: bilinear_interpolate, mxh_max_moments

  integer, parameter :: EQFILE_LENGTH = 800

  integer, parameter :: geo_type_miller = 0
  integer, parameter :: geo_type_global = 1
  integer, parameter :: geo_type_generalized_elongation = 2
  integer, parameter :: geo_type_fourier_series = 3
  integer, parameter :: geo_type_miller_extended_harmonic = 4

  !> Maximum number of moments for the Miller Extended Harmonic equilbrium
  integer, parameter :: mxh_max_moments = 5

  !> Helper type to wrap up some flux surface specific variables for
  !> analytic equilibrium type.
  type :: flux_surface_type
     !> Flux surface major radius, \(R_{0N}\), input variable: `Rmaj`
     real :: Rmaj
     !> Location of reference magnetic field, \(R_{geoN}\), input variable: `R_geo`
     real :: R_geo
     !> Minor radius, \(r_{\psi N}\), input variable: `rhoc`
     real :: r
     !> Step size for radial derivatives, \(\Delta r_{\psi N}\), input variable: `delrho`
     real :: dr
     !> Minor radius of shaping surface, \(a_{\psi N}\), input variable: `aSurf`
     real :: aSurf
     !> Horizontal Shafranov shift, \(dR_{0N}/dr_{\psi N}\), input variable: `shift`, `Raxis`
     real :: sHorz
     !> Vertical Shafranov shift, \(dZ_{0N}/dr_{\psi N}\), input variable: `shiftVert`, `Zaxis`
     real :: sVert
     !> First shaping mode strength, \(\Delta_m\), input variable: `akappa`, `delta2`, `deltam`
     real :: delm
     !> Second shaping mode strength, \(\Delta_n\), input variable: `tri`, `delta3`, `deltan`
     real :: deln
     !> First shaping mode derivative, \(d\Delta_m/dr_{\psi N}\), input variable: `akappri`, `deltampri`
     real :: delmp
     !> Second shaping mode derivative, \(d\Delta_n/dr_{\psi N}\), input variable: `tripri`, `deltanpri`
     real :: delnp
     !> First shaping mode tilt angle, \(\theta_m\), input variable: `thetak`, `theta2`, `thetam`
     real :: thm
     !> Second shaping mode tilt angle, \(\theta_n\), input variable: `thetad`, `theta2`, `thetan`
     real :: thn
     !> Safety factor, \(q\), input variable: `qinp`
     real :: q
     !> Magnetic shear, \(\hat{s}\), input variable: `s_hat_input`
     real :: shat
     !> Number of points in theta, input variable: `ntheta`
     integer :: nt
     !> Geometry specification type, input variable: `geoType`
     integer :: geoType
     !> The first poloidal mode number, input variable: `mMode`
     integer :: mMode
     !> The second poloidal mode number, input variable: `nMode`
     integer :: nMode
     !> Number of moments for MXH equilibrium, input variable: `n_mxh`
     integer :: n_mxh
     !> Zeroth cosine moment for MXH equilibrium, input variable: `c0_mxh`
     real :: c0_mxh
     !> Cosine moments for MXH equilibrium, input variable: `c_mxh`
     real, dimension(mxh_max_moments) :: c_mxh
     !> Sine moments for MXH equilibrium, input variable: `s_mxh`
     real, dimension(mxh_max_moments) :: s_mxh
     !> Radial derivatives of cosine moments for MXH equilibrium, input variable: `dc_mxh_dr`
     real, dimension(mxh_max_moments) :: dc_mxh_dr
     !> Radial derivatives of sine moments for MXH equilibrium, input variable: `ds_mxh_dr`
     real, dimension(mxh_max_moments) :: ds_mxh_dr
  end type flux_surface_type

  interface flux_surface_type
    module procedure new_flux_surface_type
  end interface flux_surface_type

  !> A derived type used to hold the inputs to the geo initialisation routine.
  !> By encapsulating all possible inputs in this type we can avoid having to
  !> have multiple initialisation interfaces which all types have to define.
  type :: geo_input_type
     !> Filename to read in for equilibria based on reading a file
     character(len = EQFILE_LENGTH) :: eqfile = 'EQFILE_NOT_SET'
     !> Upsampling factor used in efit, dfit and gs2d cases
     integer :: big = -1
     !> File containing normalisation factors for dfit
     character(len = EQFILE_LENGTH) :: eqnormfile = ' EQNORMFILE_NOT_SET'
     !> Target theta grid used for ideq
     real, dimension(:), allocatable :: theta
     !> Used for analytic local equiliria
     type(flux_surface_type) :: surf
     !> Theta shift used in analytic local equiliria
     real :: thShift
  end type geo_input_type

  type, abstract :: abstract_geo_type
     !> Name of the specific implementation
     character(len=:), allocatable :: type_name
     !> Has this instance been initialised
     logical :: initialised = .false.
     !> Does this implementation have the full or half theta grid? If only half
     !> then we assume up-down symmetry and use this to set gradients etc.
     logical :: has_full_theta_range = .false.
     !> Typically the number of radial and theta grid points.
     integer :: nr, nt
     real :: psi_0, psi_a, B_T, beta_0, R_mag, Z_mag, aminor
     !> 2D map of co-ordinates to R, Z or B
     real, allocatable, dimension(:,:) :: R_psi, Z_psi, B_psi
     !> 2D maps of theta and eqpsi
     real, allocatable, dimension(:,:) :: eqth, eqpsi_2d
     !> Minor radius gradient in cylindrical coordinates \((\hat{e}_R,
     !> \hat{e}_Z, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dpcart
     !> Theta gradient in cylindrical coordinates \((\hat{e}_R, \hat{e}_Z, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dtcart
     !> B gradient in cylindrical coordinates \((\hat{e}_R, \hat{e}_Z, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dbcart
     !> Minor radius gradient in Bishop coordinates \((\hat{e}_{r_\psi}, \hat{e}_l, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dpbish
     !> Theta gradient in Bishop coordinates \((\hat{e}_{r_\psi}, \hat{e}_l, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dtbish
     !> B gradient in Bishop coordinates \((\hat{e}_{r_\psi}, \hat{e}_l, \hat{e}_\zeta)\)
     real, allocatable, dimension (:,:,:) :: dbbish
     !> Equilibrium psi (not normalised) and pressure grids
     real, allocatable, dimension(:) :: eqpsi, pressure
     !> Normalised psi, fp, q, beta, diameter and R_centre values on grid
     real, allocatable, dimension(:) :: psi_bar, fp, qsf, beta, diam, rc
   contains
     procedure :: initialise
     procedure(read_and_set_interface), deferred :: read_and_set
     procedure(noargs_interface), deferred :: finalise, finish_setup
     procedure(rtheta_interface), deferred :: rpos, zpos, psi, rfun
     procedure(rp_interface), deferred :: rcenter, diameter
     procedure(pbar_interface), deferred :: btori, dbtori, qfun, pfun, dpfun, betafun
     !> Calculate the derivative of rp w.r.t. R and Z and return
     !> the modulus sqrt(drp/dR ^ 2 + drp/dZ^2). I.e. return |grad rp|.
     !> Parameters are:
     !>   - rgrid: Submodule radial grid (r_circ for EFIT, rp otherwise)
     !>   - theta: grid of theta
     !>   - gradf(:,1): |grad psi|
     !>   - gradf(:,2): 0.0
     !>   - char: if char = 'R', return |grad pressure| instead
     !>   - char: if char = 'T', then return theta gradient in bishop form
     !>    - gradf(:,1): (dtheta/dZ d rp/dR - dtheta/dR drp/dZ)/ |grad rp|
     !>    - gradf(:,2): (dtheta/dR d rp/dR + dtheta/dZ drp/dZ)/ |grad rp|
     !>   - rp: value of rp on the flux surface where we want the grad
     !>   - nth: number of theta points
     !>   - ntgrid: lower index of theta array is -ntgrid
     procedure :: gradient
     procedure(eqitem_interface), deferred :: eqitem
     procedure :: calculate_gradients
     procedure :: eqdcart, eqdbish, invR, derm, rhofun => rhofun_null
     procedure :: alloc_common_arrays, dealloc_common_arrays
     procedure :: alloc_arrays, dealloc_arrays, hahm_burrell
     procedure :: alloc_special_arrays => alloc_special_arrays_null
     procedure :: dealloc_special_arrays => dealloc_special_arrays_null
  end type abstract_geo_type

  type, abstract, extends(abstract_geo_type) :: abstract_eqfile_geo_type
     character(len = EQFILE_LENGTH) :: filename
     type(spline) :: btori_spl, qsf_spl, pressure_spl, beta_spl, diam_spl, rcenter_spl
   contains
     procedure :: finalise => finalise_eqfile
     procedure :: finish_setup => finish_setup_eqfile
     procedure :: setup_special_splines => setup_special_splines_eqfile_null
     procedure :: delete_special_splines => delete_special_splines_eqfile_null
     procedure :: setup_splines => setup_splines_eqfile
     procedure :: delete_splines => delete_splines_eqfile
     procedure :: btori => btori_eqfile, dbtori => dbtori_eqfile
     procedure :: qfun => qfun_eqfile, betafun => betafun_eqfile
     procedure :: pfun => pfun_eqfile, dpfun => dpfun_eqfile
     procedure :: diameter => diameter_eqfile, rcenter => rcenter_eqfile
     procedure :: psi => psi_eqfile, rfun => psi_eqfile
     procedure :: rpos => rpos_eqfile, zpos => zpos_eqfile
     procedure :: eqitem => eqitem_eqfile
  end type abstract_eqfile_geo_type

  type, abstract, extends(abstract_eqfile_geo_type) :: abstract_eqfile_cart_geo_type
     integer :: nw, nh
     real, allocatable, dimension(:) :: thetab, r_bound, eq_R, eq_Z
     type(periodic_spline) :: rbound_spl
   contains
     procedure :: bound => bound_eqfile_cart, rfun, zbrent, shared_setup => shared_setup_cart
     procedure :: psi => psi_eqfile_cart, rpos => rpos_eqfile_cart, zpos => zpos_eqfile_cart
     procedure :: derm => derm_cart, eqitem => eqitem_cart, diameter => diameter_eqfile_cart
  end type abstract_eqfile_cart_geo_type

  ! Define the procedure interfaces.
  interface
     real function rtheta_interface(self, r, theta)
       import abstract_geo_type
       implicit none
       class(abstract_geo_type), intent(in) :: self
       real, intent(in) :: r, theta
     end function rtheta_interface

     real function rp_interface(self, rp)
       import abstract_geo_type
       implicit none       
       class(abstract_geo_type), intent(in) :: self
       real, intent(in) :: rp
     end function rp_interface

     real function pbar_interface(self, pbar)
       import abstract_geo_type
       implicit none       
       class(abstract_geo_type), intent(in) :: self
       real, intent(in) :: pbar
     end function pbar_interface

     real function eqitem_interface(self, r_in, theta_in, f, char) result(fstar)
       import abstract_geo_type
       implicit none       
       class(abstract_geo_type), intent(in) :: self
       real, intent(in) :: r_in, theta_in
       real, dimension(:, :), intent(in) :: f
       character(len=1), intent(in) :: char
     end function eqitem_interface

     subroutine read_and_set_interface(self, inputs)
       import abstract_geo_type, geo_input_type
       implicit none       
       class(abstract_geo_type), intent(in out) :: self
       type(geo_input_type), intent(in) :: inputs
     end subroutine read_and_set_interface

     subroutine noargs_interface(self)
       import abstract_geo_type
       implicit none       
       class(abstract_geo_type), intent(in out) :: self
     end subroutine noargs_interface
  end interface

contains

  subroutine initialise(self, inputs, psi_0_out, psi_a_out, rmaj, B_T0, avgrmid)
    class(abstract_geo_type), intent(in out) :: self
    type(geo_input_type), intent(in) :: inputs
    real, intent(out) :: psi_0_out, psi_a_out, rmaj, B_T0, avgrmid
    if (.not. self%initialised) then
       call self%read_and_set(inputs) ; call self%finish_setup
    end if
    psi_a_out = self%psi_a ; psi_0_out = self%psi_0
    rmaj = self%R_mag ; B_T0 = abs(self%B_T) ; avgrmid = self%aminor
    self%initialised = .true.
  end subroutine initialise

  pure subroutine alloc_arrays(self)
    class(abstract_geo_type), intent(in out) :: self
    call self%alloc_common_arrays ; call self%alloc_special_arrays
  end subroutine alloc_arrays

  pure subroutine dealloc_arrays(self)
    class(abstract_geo_type), intent(in out) :: self
    call self%dealloc_common_arrays ; call self%dealloc_special_arrays
  end subroutine dealloc_arrays

  pure subroutine alloc_special_arrays_null(self)
    class(abstract_geo_type), intent(in out) :: self
    UNUSED_DUMMY(self)
  end subroutine alloc_special_arrays_null

  pure subroutine dealloc_special_arrays_null(self)
    class(abstract_geo_type), intent(in out) :: self
    UNUSED_DUMMY(self)
  end subroutine dealloc_special_arrays_null

  pure subroutine alloc_common_arrays(self)
    class(abstract_geo_type), intent(in out) :: self
    integer :: nr, nt
    nr = self%nr ; nt = self%nt
    allocate(self%R_psi(nr, nt), self%Z_psi(nr, nt), self%B_psi(nr, nt))
    allocate(self%dpcart(nr, nt, 2), self%dbcart(nr, nt, 2), self%dbbish(nr, nt, 2))
    allocate(self%dtcart(nr, nt, 2), self%dpbish(nr, nt, 2), self%dtbish(nr, nt, 2))
    allocate(self%eqpsi(nr), self%pressure(nr), self%eqth(nr, nt), self%eqpsi_2d(nr, nt))
    allocate(self%psi_bar(nr), self%fp(nr), self%qsf(nr), self%beta(nr), self%rc(nr), self%diam(nr))
  end subroutine alloc_common_arrays

  pure subroutine dealloc_common_arrays(self)
    class(abstract_geo_type), intent(in out) :: self
    deallocate(self%R_psi, self%Z_psi, self%B_psi, self%eqth, self%eqpsi, self%pressure)
    deallocate(self%dpcart, self%dpbish, self%dtcart, self%dtbish, self%dbcart, self%dbbish)
    deallocate(self%eqpsi_2d, self%psi_bar, self%fp, self%qsf, self%beta, self%rc, self%diam)
  end subroutine dealloc_common_arrays

  !> Given theta, R, Z, B and psi on 2D grids calculate the index space
  !> derivatives in the two grid dimensions and use these to find gradients
  !> in cartesian and Bishop space.
  pure subroutine calculate_gradients(self)
    implicit none
    class(abstract_geo_type), intent(in out) :: self
    ! The following quantities represent changes between adjacent grid
    ! points. The first two dimensions are usually minor radius and theta,
    ! respectively. The last dimension is the difference along either
    ! the (minor) radial or theta directions, respectively
    !> Major radius differences on theta, minor radius grids
    real, allocatable, dimension (:,:,:) :: drm
    !> Vertical differences on theta, minor radius grids
    real, allocatable, dimension (:,:,:) :: dzm
    !> Temporary version used to hold minor radius(psi), theta or B index space derivatives
    real, allocatable, dimension (:,:,:) :: dtmp_m

    ! Need drm and dzm for all eqdcart calls
    allocate(drm(self%nr, self%nt, 2), dzm(self%nr, self%nt, 2))
    call self%derm(self%R_psi, drm, 'E')
    call self%derm(self%Z_psi, dzm, 'O')

    ! grad(psi) in cartesian and bishop form
    allocate(dtmp_m(self%nr, self%nt, 2))
    call self%derm(self%eqpsi_2d, dtmp_m, 'E')
    call self%eqdcart(dtmp_m, drm, dzm, self%dpcart)
    call self%eqdbish(self%dpcart, self%dpcart, self%dpbish)

    ! grad(B) in cartesian and bishop form
    call self%derm(self%B_psi, dtmp_m, 'E')
    call self%eqdcart(dtmp_m, drm, dzm, self%dbcart)
    call self%eqdbish(self%dbcart, self%dpcart, self%dbbish)

    ! grad(theta) in cartesian and bishop form
    call self%derm(self%eqth, dtmp_m, 'T')
    call self%eqdcart(dtmp_m, drm, dzm, self%dtcart)
    call self%eqdbish(self%dtcart, self%dpcart, self%dtbish)
    deallocate(dtmp_m, drm, dzm)
  end subroutine calculate_gradients

  !> Converts derivatives w.r.t. (psi_index,theta_index) to derivatives
  !> w.r.t. (R,Z). Note that `dfcart(1, :, :)` is not valid
  pure subroutine eqdcart(self, dfm, drm, dzm, dfcart)
    implicit none
    class(abstract_geo_type), intent(in) :: self
    !> dfm(:,:,1) is deriv w.r.t. psi_index (i.e. (df/dpsi)_theta * delta psi);
    !> dfm(:,:,2) is deriv w.r.t. theta_index
    real, dimension (:,:,:), intent(in)  :: dfm, drm, dzm
    !> dfcart(:,:,1) is deriv w.r.t. R;
    !> dfcart(:,:,2) is deriv w.r.t. Z
    real, dimension (:,:,:), intent(out) :: dfcart
    real, dimension (size(dfm, 1), size(dfm, 2)) :: denom
    denom = drm(:,:,1) * dzm(:,:,2) - drm(:,:,2) * dzm(:,:,1)
    dfcart(:,:,1) =   dfm(:,:,1) * dzm(:,:,2) - dzm(:,:,1) * dfm(:,:,2)
    dfcart(:,:,2) = - dfm(:,:,1) * drm(:,:,2) + drm(:,:,1) * dfm(:,:,2)
    ! Skips first point in first dimension because it's likely exactly on the
    ! magnetic axis, which means the Jacobian is zero.
    dfcart(2:, :, 1) = dfcart(2:, :, 1) / denom(2:, :)
    dfcart(2:, :, 2) = dfcart(2:, :, 2) / denom(2:, :)
    UNUSED_DUMMY(self)
  end subroutine eqdcart

  !> Convert gradients of a function f w.r.t. R,Z into bishop form.
  !> Note that `dbish(1, :, :)` is not valid
  pure subroutine eqdbish(self, dcart, dpcart, dbish)
    implicit none
    class(abstract_geo_type), intent(in) :: self
    !> dcart(:,:,1) is gradient of f w.r.t. R;
    !> dcart(:,:,2) is gradient of f w.r.t. Z
    real, dimension(:, :, :), intent (in) :: dcart, dpcart
    !> dbish(:,:,1) is set to (df/dR dpsi/dR + df/dZ dpsi/dZ)/|grad psi|;
    !> dbish(:,:,2) is set to (-df/dR dpsi/dZ + df/dZ dpsi/dR)/|grad psi|.
    !> Note that in the special case where f=psi:
    !> dbish(:,:,1) is |grad psi|; dbish(:,:,2) is 0
    real, dimension(:, :, :), intent(out) :: dbish
    real, dimension(size(dcart, 1) - 1, size(dcart, 2)) :: denom
    ! denom is |grad psi|
    denom = hypot(dpcart(2:,:,1), dpcart(2:,:,2))
    dbish(:,:,1) = dcart(:,:,1) * dpcart(:,:,1) + dcart(:,:,2) * dpcart(:,:,2)
    dbish(:,:,2) =-dcart(:,:,1) * dpcart(:,:,2) + dcart(:,:,2) * dpcart(:,:,1)
    dbish(2:, :, 1) = dbish(2:, :, 1) / denom
    dbish(2:, :, 2) = dbish(2:, :, 2) / denom
    UNUSED_DUMMY(self)
  end subroutine eqdbish

  subroutine gradient(self, rgrid, theta, grad, char, rp, nth_used, ntm, use_bishop)
    use splines, only: inter_d_cspl
    implicit none
    class(abstract_geo_type), intent(in) :: self
    integer, intent (in) :: nth_used, ntm
    character(1), intent (in) :: char
    real, dimension (-ntm:), intent (in)  :: rgrid, theta
    real, dimension (-ntm:,:), intent (out) :: grad
    real, intent(in) :: rp
    logical, intent(in) :: use_bishop
    real, dimension(:, :, :), allocatable :: darr
    real, dimension (1) :: p_eqpsi, dp_deqpsi
    integer :: i
    if (use_bishop) then
       select case(char)
       case('B')
          allocate(darr, source = self%dbbish)
       case('P', 'R')
          allocate(darr, source = self%dpbish)
       case('T')
          allocate(darr, source = self%dtbish)
       end select
    else
       select case(char)
       case('B')
          allocate(darr, source = self%dbcart)
       case('P', 'R')
          allocate(darr, source = self%dpcart)
       case('T')
          allocate(darr, source = self%dtcart)
       end select
    end if

    do i = -nth_used, nth_used
       grad(i, 1) = self%eqitem(rgrid(i), theta(i), darr(:,:,1), 'R')
       grad(i, 2) = self%eqitem(rgrid(i), theta(i), darr(:,:,2), 'Z')
    end do

    if(char == 'T' .and. .not. self%has_full_theta_range) then
       where (theta(-nth_used:nth_used) < 0.0)
          grad(-nth_used:nth_used, 1) = -grad(-nth_used:nth_used, 1)
          grad(-nth_used:nth_used, 2) = -grad(-nth_used:nth_used, 2)
       end where
    end if

    !     to get grad(pressure), multiply grad(psi) by dpressure/dpsi
    ! Could use self%dpfun if took pbar rather than rp?
    if (char == 'R') then !Only actually used if bishop == 0
       call inter_d_cspl(self%eqpsi, self%pressure, [rp], p_eqpsi, dp_deqpsi)
       do i = -nth_used, nth_used
          grad(i, 1:2) = grad(i, 1:2) * dp_deqpsi(1) * 0.5 * self%beta_0
       end do
    end if
  end subroutine gradient

  !> Calculate the derivative of f w.r.t to the radial
  !> and poloidal indexes (i.e. calculate the finite
  !> differences without dividing by
  !> delta psi and delta theta).
  !> - dfm(:,:,1) is the psi derivative
  !> - dfm(:,:,2) is the theta derivative
  !> - char specifies the periodicity in theta
  !>   - 'E', 'O' mean continuous at theta = +/- pi
  !>   - 'T' means a jump of 2 pi at theta = +/- pi
  pure subroutine derm(self, f, dfm, char)
    use constants, only: pi
    implicit none
    class(abstract_geo_type), intent(in) :: self
    real, dimension(:,:), intent(in) :: f
    real, dimension(:,:,:), intent(out) :: dfm
    character(1), intent(in) :: char
    integer :: i, j, nr, nt
    nr = self%nr ; nt = self%nt

    i = 1
    dfm(i,:,1) = -0.5 * (3 * f(i,:) - 4 * f(i+1,:) + f(i+2,:))

    i = nr
    dfm(i,:,1) = 0.5 * (3 * f(i,:) - 4 * f(i-1,:) + f(i-2,:))

    do i = 2, nr-1
       dfm(i,:,1) = 0.5 * (f(i+1,:) - f(i-1,:))
    end do

    do j = 2, nt-1
       dfm(:,j,2) = 0.5 * (f(:,j+1) - f(:,j-1))
    end do

    ! Handle periodic boundary
    if (self%has_full_theta_range) then
       ! Compute df/dtheta at -pi,+pi assuming
       !   (i) periodicity in theta,
       !  (ii) endpoints at -pi,+pi
       ! (iii) equispaced grid in theta, and
       !  (iv) 1 and nt correspond to -pi, +pi and are at the same place
       dfm(:,1,2) = 0.5 * (f(:,2) - f(:,nt-1))
       dfm(:,nt,2) = dfm(:, 1, 2)
       if (char == 'T') then
          dfm(:,1,2) = dfm(:,1,2) + pi
          dfm(:,nt,2) = dfm(:,nt,2) + pi
       end if
    else
       ! assume up-down symmetry for now:
       select case (char)
       case ('E')
          dfm(:,1,2) = 0.0
          dfm(:,nt,2) = 0.0
       case ('O')
          dfm(:,1,2) = f(:,2)
          dfm(:,nt,2) = -f(:,nt-1)
       case ('T')
          dfm(:,1,2) = f(:,2) !f(:,j+1) - f(:,j) -- assumes f(:,1) = 0
          dfm(:,nt,2) = pi - f(:,nt-1) !f(:, j) - f(:, j-1)  -- assumes f(:,nt) = pi
       end select
    end if
  end subroutine derm

  real function invR (self, r, theta)
    implicit none
    class(abstract_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    invR = 1. / self%rpos(r, theta)
  end function invR

  real function rhofun_null (self, pbar) result(rhofun) !Can't be pure as deq variant isnt
    implicit none
    class(abstract_geo_type), intent(in) :: self
    real, intent (in) :: pbar
    rhofun = 0.
    UNUSED_DUMMY(self); UNUSED_DUMMY(pbar)
  end function rhofun_null

  !> FIXME : Add documentation
  subroutine hahm_burrell(self, a)
    use file_utils, only: open_output_file, close_output_file
    use mp, only: proc0
    implicit none
    class(abstract_geo_type), intent(in) :: self
    real, intent(in) :: a
    integer :: i, fort24_unit, nr
    real :: gradpsi, mag_B, rho_eq, rp1, rp2, rho1, rho2, drhodpsiq
    real, dimension(self%nr) :: gamma_shear, pbar, dp, d2p, pres, bs_coll, s__hat, bs_sh
    if (.not. proc0) return
    nr = self%nr
    gamma_shear = 0.
    pbar = (self%eqpsi - self%eqpsi(1)) / (self%eqpsi(nr) - self%eqpsi(1))

    do i = 1, nr
       dp(i)  = self%dpfun(pbar(i))
       pres(i) = self%pfun(pbar(i))
    end do

    d2p(1) = 0.0 ; d2p(nr) = 0.0
    do i = 2, nr - 1
       d2p(i) = (dp(i+1) - dp(i-1)) / (self%eqpsi(i+1) - self%eqpsi(i-1))
    end do

    do i = 2, nr- 1
       rp1 = self%eqpsi(i+1) ; rp2 = self%eqpsi(i-1)
       rho1 = 0.5 * self%diameter(rp1) ; rho2 = 0.5 * self%diameter(rp2)
       drhodpsiq = (rho1 - rho2) / (rp1 - rp2)
       gradpsi = self%eqitem(self%eqpsi(i), 0., self%dpbish(:,:,1), 'R')

       mag_B = hypot(self%btori(pbar(i)), gradpsi) * self%invR(self%eqpsi(i), 0.)
       !For geq and ideq could be mag_B = geom%eqitem(geom%eqpsi(i), 0., geom%B_psi, 'R')

       gamma_shear(i) = 0.01 * gradpsi**2 * (d2p(i) / pres(i) - a * (dp(i) / pres(i))**2) &
            * (2 * pres(i) / self%beta_0)**((1 - a) / 2) * (-pres(i) / (dp(i) / drhodpsiq)) &
            / mag_B

       s__hat(i) = (self%qfun(pbar(i+1)) - self%qfun(pbar(i-1))) / (rho2 - rho1)
       s__hat(i) = s__hat(i) * 0.5 * (rho1 + rho2) / self%qfun(pbar(i))
    end do

    ! Assume rho_*0 = 0.01, nu_*0 = 1.0
    do i = 2, nr - 1
       bs_coll(i) = 0.01 * ((2 * pres(i) / self%beta_0)**(1 - a))**2.5 &
             * self%qfun(0.) * 0.5 * self%diameter(self%eqpsi(i)) &
            * (-dp(i) / pres(i)) * self%R_psi(1,1)**1.5 / (2 * pres(i) / self%beta_0)**a
       bs_sh(i) = (-pres(i) / (dp(i) / drhodpsiq))*s__hat(i) / (self%R_psi(1,1) &
            * self%qfun(pbar(i)) * sqrt(pres(i)))
    end do

    call open_output_file(fort24_unit,".fort.24")
    do i = 2, nr - 1
       rho_eq = 0.5 * self%diameter(self%eqpsi(i))
       write(fort24_unit,'(i5,11(1x,e16.9))') i, pbar(i), rho_eq, pres(i), gamma_shear(i), bs_coll(i), bs_sh(i)
    end do
    call close_output_file(fort24_unit)
  end subroutine hahm_burrell

  !> Calculates fstar which is f interpolated at the location (r,theta).
  !> Here r is the normalised poloidal flux coordinate rp (= psi_pN + psi_0N)
  !> and theta_in is theta. f is a grid of values of f as a function of psi_p,theta
  !> First we find the radial and theta indices bounding our requested point and we
  !> then interpolate within the bound rectangle.
  real function eqitem_eqfile(self, r_in, theta_in, f, char) result(fstar)
    use mp, only: mp_abort
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: r_in, theta_in
    real, dimension(:,:), intent(in) :: f
    character(1), intent(in) :: char
    integer :: istar, jstar
    real :: r, thet, eqpsi_sign, tps
    ! allow psi(r) to be a decreasing function
    eqpsi_sign = 1.
    if (self%eqpsi(2) < self%eqpsi(1)) eqpsi_sign = -1.
    associate (dim_1 => eqpsi_sign * self%eqpsi , mtheta => self%eqth(1, :)) !Non-contiguous copy - slow?

      !Copy input r to avoid any modification
      r = r_in ; thet = mod2pi(theta_in) ; tps = 1.

      if (.not. self%has_full_theta_range) then ! assume up-down symmetry
         if (char == 'Z' .and. abs(thet) >= 1.e-10) tps = sign(1.0, thet)
         thet = abs(thet)
      end if

      ! check for axis evaluation, no gradients available at axis, so do not get too close
      if (r < dim_1(2)) then
         write(*,*) r, theta_in, eqpsi_sign, dim_1(1), dim_1(2)
         call mp_abort('no evaluation too close to or inside axis allowed in eqitem', .true.)
      end if

      ! disallow evaluations on or outside the surface for now
      if (r >= dim_1(self%nr)) then
         write(*,*) r, theta_in, dim_1(self%nr), eqpsi_sign
         call mp_abort('No evaluation of eqitem allowed on or outside surface', .true.)
      end if

      istar = find_lower_bound(r, dim_1, 0)
      jstar = find_lower_bound(thet, mtheta, self%nt - 1)
      fstar = tps * bilinear_interpolate(f, dim_1, mtheta, istar, jstar, r, thet)
    end associate
  end function eqitem_eqfile

  subroutine setup_splines_eqfile(self)
    use splines, only: new_spline
    class(abstract_eqfile_geo_type), intent(in out) :: self
    self%diam_spl = new_spline(self%eqpsi, self%diam)
    self%rcenter_spl = new_spline(self%eqpsi, self%rc)
    self%btori_spl = new_spline(self%psi_bar, self%fp)
    self%qsf_spl = new_spline(self%psi_bar, self%qsf)
    self%pressure_spl = new_spline(self%psi_bar, self%pressure)
    self%beta_spl = new_spline(self%psi_bar, self%beta)
    call self%setup_special_splines
  end subroutine setup_splines_eqfile

  subroutine delete_splines_eqfile(self)
    use splines, only: delete_spline
    class(abstract_eqfile_geo_type), intent(in out) :: self
    call delete_spline(self%diam_spl)
    call delete_spline(self%rcenter_spl)
    call delete_spline(self%btori_spl)
    call delete_spline(self%qsf_spl)
    call delete_spline(self%pressure_spl)
    call delete_spline(self%beta_spl)
    call self%delete_special_splines
  end subroutine delete_splines_eqfile

  subroutine setup_special_splines_eqfile_null(self)
    class(abstract_eqfile_geo_type), intent(in out) :: self
    UNUSED_DUMMY(self)
  end subroutine setup_special_splines_eqfile_null

  subroutine delete_special_splines_eqfile_null(self)
    class(abstract_eqfile_geo_type), intent(in out) :: self
    UNUSED_DUMMY(self)
  end subroutine delete_special_splines_eqfile_null

  subroutine finalise_eqfile(self)
    class(abstract_eqfile_geo_type), intent(in out) :: self
    self%initialised = .false.
    call self%dealloc_arrays ; call self%delete_splines
  end subroutine finalise_eqfile

  subroutine finish_setup_eqfile(self)
    class(abstract_eqfile_geo_type), intent(in out) :: self
    call self%calculate_gradients ; call self%setup_splines
  end subroutine finish_setup_eqfile

  real function btori_eqfile (self, pbar) result(btori)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    btori = splint(pbar, self%btori_spl)
  end function btori_eqfile

  !> \(\frac{dI}{d\psi}\) at the given normalised \(\psi\)
  real function dbtori_eqfile (self, pbar) result(dbtori)
    use splines, only: dsplint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    dbtori = dsplint(pbar, self%btori_spl) / (self%psi_a - self%psi_0)
  end function dbtori_eqfile

  real function qfun_eqfile (self, pbar) result(qfun)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    qfun = splint(pbar, self%qsf_spl)
  end function qfun_eqfile

  real function pfun_eqfile (self, pbar) result(pfun)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    ! p_N would be B**2/mu_0 => p = beta/2 in our units
    pfun = 0.5 * self%beta_0 * splint(pbar, self%pressure_spl)
  end function pfun_eqfile

  real function dpfun_eqfile (self, pbar) result(dpfun)
    use splines, only: dsplint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    ! p_N would be B**2/mu_0 => p = beta/2 in our units
    dpfun = 0.5 * self%beta_0 * dsplint(pbar, self%pressure_spl) / (self%psi_a - self%psi_0)
  end function dpfun_eqfile

  !> Return the diameter of the flux surface at a given major radius
  real function diameter_eqfile (self, rp) result(diameter)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: rp
    diameter = splint(rp, self%diam_spl)
  end function diameter_eqfile

  !> Return the major radius of the centre of a given flux surface
  real function rcenter_eqfile (self, rp) result(rcenter)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: rp
    rcenter = splint(rp, self%rcenter_spl)
  end function rcenter_eqfile

  real function psi_eqfile (self, r, theta) result(psi) !Can't mark pure as eqfile_cart variant is not pure
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    if (r==0.) then
       psi = self%psi_0
    else if (r==1. .and. theta == 0.0) then
       psi = self%psi_a
    else
       psi = r
    end if
  end function psi_eqfile

  real function betafun_eqfile (self, pbar) result(betafun)
    use splines, only: splint
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent(in) :: pbar
    if (pbar == 0.) then
       betafun = self%beta_0
    else
       betafun = splint(pbar, self%beta_spl)
    end if
  end function betafun_eqfile

  real function rpos_eqfile (self, r, theta) result(rpos)
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    rpos = self%eqitem(r, theta, self%R_psi, 'R')
  end function rpos_eqfile

  real function zpos_eqfile (self, r, theta) result(zpos)
    implicit none
    class(abstract_eqfile_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    zpos = self%eqitem(r, theta, self%Z_psi, 'Z')
  end function zpos_eqfile

  !> Calculate the derivative of f w.r.t. R, Z
  !> - dfm(:,:,1) is deriv w.r.t. R
  !> - dfm(:,:,2) is deriv w.r.t. Z
  pure subroutine derm_cart(self, f, dfm, char)
    use constants, only: pi
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, dimension(:,:), intent(in) :: f
    real, dimension(:,:,:), intent(out) :: dfm
    character(1), intent(in) :: char
    integer :: i, j, nw, nh
    nw = self%nw ; nh = self%nh
    i = 1
    dfm(i,:,1) = -0.5 * (3 * f(i,:) - 4 * f(i+1,:) + f(i+2,:))

    i = nw
    dfm(i,:,1) = 0.5 * (3 * f(i,:) - 4 * f(i-1,:) + f(i-2,:))

    j = 1
    dfm(:,j,2) = -0.5 * (3 * f(:,j) - 4 * f(:,j+1) + f(:,j+2))

    j = nh
    dfm(:,j,2) = 0.5 * (3 * f(:,j)-4 * f(:,j-1) + f(:,j-2))

    do i = 2, nw-1
       dfm(i,:,1) = 0.5 * (f(i+1,:) - f(i-1,:))
    end do

    do j = 2, nh-1
       dfm(:,j,2) = 0.5 * (f(:, j+1) - f(:, j-1))
    end do

    if (char == 'T') then !Could a similar approach be ported to derm_eqfile?
       where(dfm(:, :, 2) > pi * 0.5)
          dfm(:, :, 2) = dfm(:, :, 2) - pi
       end where
    end if
  end subroutine derm_cart

  !> fstar is f(R,Z) interpolated at the values (r,thetain). The parameter
  !> r is the distance to the magnetic axis.
  real function eqitem_cart(self, r_in, theta_in, f, char) result(fstar)
    use mp, only: mp_abort
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: r_in, theta_in
    real, dimension(:, :), intent(in) :: f
    character(1), intent(in) :: char
    integer :: istar, jstar
    real :: r, thet, r_pos, z_pos
    UNUSED_DUMMY(char)
    associate(dim_1 => self%eq_R, dim_2 => self%eq_Z)
      !Copy input r to avoid any modification
      r = r_in ; thet = mod2pi(theta_in)
      r_pos = self%Rpos(r, thet) ; z_pos = self%Zpos(r, thet)

      ! ensure point is on R mesh
      if (r_pos >= dim_1(self%nw) .or. r_pos <= dim_1(1)) then
         write(*,*) r, theta_in, dim_1(self%nw), r_pos
         call mp_abort('No evaluation of eqitem allowed outside or on edge of R domain', .true.)
      end if

      ! ensure point is on Z mesh
      if (z_pos >= dim_2(self%nh) .or. z_pos <= dim_2(1)) then
         write(*,*) r, theta_in, dim_2(1), dim_2(self%nh), z_pos
         call mp_abort('No evaluation of eqitem allowed outside or on edge of Z domain', .true.)
      end if

      istar = find_lower_bound(r_pos, dim_1, 0)
      jstar = find_lower_bound(z_pos, dim_2, 0)
      fstar = bilinear_interpolate(f, dim_1, dim_2, istar, jstar, r_pos, z_pos)
    end associate
  end function eqitem_cart

  pure integer function find_lower_bound(val, array, default) result(bound)
    real, intent(in) :: val
    real, dimension(:), intent(in) :: array
    integer, intent(in) :: default
    integer :: limit, ind
    limit = size(array) ; bound = default
    do ind = 2, limit
       if (val < array(ind)) then
          bound = ind - 1 ; exit
       end if
    end do
  end function find_lower_bound

  !> use opposite area stencil to interpolate to point bound by [istar,istar+1]
  !> and [jstar, jstar + 1].
  pure real function bilinear_interpolate(f, dim_1, dim_2, istar, jstar, &
       d1, d2) result(fstar)
    real, dimension(:, :), intent(in) :: f
    real, dimension(:), intent(in) :: dim_1, dim_2
    integer, intent(in) :: istar, jstar
    real, intent(in) :: d1, d2
    real :: sr, st, dr, dt
    dr = d1 - dim_1(istar) ; sr = dim_1(istar + 1) - d1
    dt = d2 - dim_2(jstar) ; st = dim_2(jstar + 1) - d2
    fstar = f(istar    , jstar    ) * sr * st &
         +f(istar + 1, jstar    ) * dr * st &
         +f(istar    , jstar + 1) * sr * dt &
         +f(istar + 1, jstar + 1) * dr * dt
    fstar = fstar / (abs(dim_1(istar + 1) - dim_1(istar)) * &
         (dim_2(jstar + 1) - dim_2(jstar)))
  end function bilinear_interpolate

  !> Find the value of the major radius on the plasma boundary
  !> at given geometric theta.
  real function bound_eqfile_cart(self, theta) result(bound)
    use splines, only: periodic_splint
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent(in) :: theta
    bound = periodic_splint(theta, self%rbound_spl)
  end function bound_eqfile_cart

  pure real function rpos_eqfile_cart (self, r, theta) result(rpos)
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    rpos = self%R_mag + r * cos(theta)
  end function rpos_eqfile_cart

  pure real function zpos_eqfile_cart (self, r, theta) result(zpos)
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    zpos = self%Z_mag + r * sin(theta)
  end function zpos_eqfile_cart

  real function psi_eqfile_cart (self, r, theta) result(psi)
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: r, theta
    if (r==0.) then
       psi = self%psi_0
    else if (r==1. .and. theta == 0.0) then
       psi = self%psi_a
    else
       psi = self%eqitem(r, theta, self%eqpsi_2d, '?')
    end if
  end function psi_eqfile_cart

  real function diameter_eqfile_cart (self, rp) result(diameter)
    use constants, only: pi
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: rp
    diameter = self%rfun(rp, 0.) + self%rfun(rp, pi)
  end function diameter_eqfile_cart

  subroutine shared_setup_cart(self, nw_in, nh_in, spsi_bar, sefit_R, sefit_Z, f, p, q, &
       sefit_psi, nbbbs, rbbbs, zbbbs, bcentr, rleft, rwid, zoffset, zhei, big, calc_aminor)
    use splines, only: inter_cspl, fitp_surf2, fitp_surf1
    use constants, only: twopi
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in out) :: self
    integer, intent(in) :: nw_in, nh_in, nbbbs, big
    real, dimension(:), intent(in) :: spsi_bar, sefit_R, sefit_Z, f, p, q
    real, dimension(:), intent(in out) :: rbbbs, zbbbs
    real, intent(in) :: bcentr, rleft, rwid, zoffset, zhei
    real, dimension(:, :), intent(in) :: sefit_psi
    logical, intent(in) :: calc_aminor
    real, dimension(:), allocatable :: zp, temp, zx1, zxm, zy1, zyn, eq_R, eq_Z
    real :: zxy11, zxym1, zxy1n, zxymn, psi_N, f_N
    integer :: i, j, ierr
    self%nw = nw_in * big ; self%nr = self%nw
    self%nh = nh_in * big ; self%nt = self%nh
    call self%alloc_arrays()
    do i = 1, self%nw
       self%psi_bar(i) = float(i - 1) / float(self%nw - 1)
    end do
    call inter_cspl(spsi_bar, f, self%psi_bar, self%fp)
    call inter_cspl(spsi_bar, p, self%psi_bar, self%pressure)
    call inter_cspl(spsi_bar, q, self%psi_bar, self%qsf)

    allocate(eq_R(self%nw), eq_Z(self%nh))
    do i = 1, self%nw
       eq_R(i) = rleft + rwid * float(i - 1) / float(self%nw - 1)
    end do

    do j = 1, self%nh
       eq_Z(j) = zoffset + ((float(j - 1) / float(self%nh - 1) )) * zhei
    end do

    ! Setup 2D interpolation of psi(R, Z)
    allocate(zp(3 * nw_in * nh_in), temp(nw_in + 2 * nh_in))
    allocate(zx1(nh_in), zxm(nh_in), zy1(nw_in), zyn(nw_in))
    call fitp_surf1(nw_in, nh_in, sefit_R, sefit_Z, sefit_psi, &
         nw_in, zx1, zxm, zy1, zyn, zxy11, zxym1, zxy1n, zxymn, &
         255, zp, temp, 1., ierr)

    do j = 1, self%nh
       do i = 1, self%nw
          self%eqpsi_2d(i, j) = fitp_surf2(eq_R(i), eq_Z(j), nw_in, nh_in, &
               sefit_R, sefit_Z, sefit_psi, nw_in, zp, 1.)
       end do
    end do
    deallocate(zp, temp)

    allocate(self%thetab(nbbbs), self%r_bound(nbbbs))
    self%thetab = atan2((zbbbs - self%Z_mag), (rbbbs - self%R_mag))
    self%r_bound = hypot((rbbbs - self%R_mag), (zbbbs - self%Z_mag))
    call sort(self%thetab, self%r_bound, zbbbs, rbbbs)

    ! Allow for duplicated points near +- pi:
    if (self%thetab(1) == self%thetab(2)) then
       self%thetab(1) = self%thetab(1) + twopi
       call sort(self%thetab, self%r_bound, zbbbs, rbbbs)
    end if

    if (self%thetab(nbbbs-1) == self%thetab(nbbbs)) then
       self%thetab(nbbbs) = self%thetab(nbbbs) - twopi
       call sort(self%thetab, self%r_bound, zbbbs, rbbbs)
    end if

    ! It isn't likely that a duplicate point would exist near theta = 0,
    ! so I am not allowing this possibility for now.
    do i = 1, nbbbs-1
       if (self%thetab(i+1) == self%thetab(i)) then
          ! put in kluge for duplicate points, which happens near theta=0:
          self%thetab(i+1) = self%thetab(i+1) +  1.e-8
       end if
    end do

    if (calc_aminor) call a_minor(rbbbs, zbbbs, self%Z_mag, self%aminor)

    self%B_psi = 0.0 ; self%diam = 0.0 ; self%rc = 0.0
    self%r_bound = self%r_bound / self%aminor
    self%R_mag = self%R_mag / self%aminor
    self%Z_mag = self%Z_mag / self%aminor
    eq_R = eq_R / self%aminor ; eq_Z = eq_Z / self%aminor
    self%eq_R = eq_R ; self%eq_Z = eq_Z
    do j = 1, self%nh
       self%R_psi(:, j) = eq_R ; self%Z_psi(:, j) = eq_Z(j)
    end do

    self%B_T = abs(bcentr)
    psi_N = self%B_T * self%aminor**2
    self%psi_a = self%psi_a / psi_N
    self%psi_0 = self%psi_0 / psi_N
    self%eqpsi_2d = self%eqpsi_2d / psi_N
    f_N = self%B_T * self%aminor
    self%fp = self%fp / f_N
    ! MKS: beta = 2 mu_0 p / B**2
    self%beta = 4. * twopi * self%pressure * 1.e-7 / self%B_T**2
    self%beta_0 = self%beta(1)
    self%pressure = self%pressure / self%pressure(1)

    do j = 1, self%nh
       do i = 1, self%nw
          if (eq_Z(j) == self%Z_mag .and. eq_R(i) == self%R_mag) then
             self%eqth(i, j) = 0.  ! value should not matter
          else
             self%eqth(i, j) = atan2( (eq_Z(j) - self%Z_mag), (eq_R(i) - self%R_mag))
          end if
       end do
    end do
    self%eqpsi = self%psi_bar * (self%psi_a - self%psi_0) + self%psi_0
    self%has_full_theta_range = .true.
    if (.true.) then
       write (*,*) "Finished shared_setup... imported ",self%type_name," equilibrium"
       write (*,*) 'Some important quantities:'
       write (*,*) "aminor", self%aminor
       write (*,*) 'R_mag', self%R_mag
       write (*,*) 'B_T0', self%B_T
       write (*,*) 'beta', self%beta_0
    end if
  end subroutine shared_setup_cart

  !> FIXME : Add documentation | Designed for eeq/gs2deq only
  subroutine a_minor(r, z, Z_mag, a)
    use mp, only: mp_abort
    use splines, only: new_spline, splint, delete_spline, spline
    implicit none
    real, dimension(:), intent (in) :: r, z
    real, intent(in) :: Z_mag
    real, intent(out) :: a
    real :: r1, r2
    integer, parameter :: nz = 5, half_nz = int(nz/2.0), invalidPoint = -nz
    real, dimension(nz) :: rtmp, ztmp
    integer :: i, j, i1, n, k, index
    type (spline) :: spl
    !CMR, 28/10/08: add code to avoid duplicate points in 5 point spline
    !               to determine r2 on inboard mid-plane
    logical, parameter :: debug=.false.
    k = 0 ; n = size(r)
    if (debug) write(6,*) "aminor:"
    if (debug) write(6,fmt='("r=",10f8.4)') r
    if (debug) write(6,fmt='("z=",10f8.4)') z

    if (n < nz) call mp_abort('nbbbs < nz -- very strange.  Stopping. Look in geo_utils.fpp', .true.)

    j = 0
    do i = half_nz + 1, 1, -1
       j = j + 1
       ztmp(j) = z(i) ; rtmp(j) = r(i)
    end do

    if (r(1) == r(n) .and. z(1) == z(n)) then
       do i = n - 1, n - half_nz, -1
          j = j + 1
          ztmp(j) = z(i) ; rtmp(j) = r(i)
       end do
    else
       do i = n, n - half_nz + 1, -1
          j = j + 1
          ztmp(j) = z(i) ; rtmp(j) = r(i)
       end do
    end if

    if (debug) write(6,fmt='("rtmp=",5f8.4)') rtmp
    if (debug) write(6,fmt='("ztmp=",5f8.4)') ztmp
    if (debug) write(6,fmt='("Z_mag=",f8.4)') Z_mag

    spl = new_spline(ztmp, rtmp)
    r1 = splint(Z_mag, spl)
    call delete_spline(spl)

    ! find point near magnetic axis elevation on low field side
    ! Initialise i1 to some invalid value so we can check if an appropriate
    ! point is not found.
    i1 = invalidPoint

    ! Note there appears to be an assumption here about the order
    ! of z which may not be true
    do i = nz, n
       if (z(i) - Z_mag > 0.) then
          i1 = i - 1
          exit
       end if
    end do

    if (i1 == invalidPoint) then
       write(*, '("Could not find point near magnetic axis elevation on low field side in aminor of eeq.f90")')
       call mp_abort("Could not find point near magnetic axis elevation on low field side in aminor of eeq.f90")
    end if

    !CMR, 28/10/08: modify this code to avoid duplicate points in 5 point spline
    !               to determine r2 on inboard mid-plane
    index = i1 - half_nz + k
    rtmp(1) = r(index)
    ztmp(1) = z(index)

    do i = 2, nz
       index = i1 - half_nz + i - 1 + k
       rtmp(i) = r(index)
       ztmp(i) = z(index)
       ! If we find a duplicate point then we increase the
       ! duplicate point counter in k, used to offset index and
       ! store the next value instead Note, here we assume we can't
       ! get three duplicate values. A better approach may be to
       ! instead use a while loop where we keep going until we've
       ! found enough points.
       if ((rtmp(i)-rtmp(i-1))**2+(ztmp(i)-ztmp(i-1))**2 < 1.0e-7) then
         k = k + 1
         if (debug) write(6,fmt='("a_minor: duplicate pt, set k=",i2)') k
         rtmp(i) = r(index + 1) ; ztmp(i) = z(index + 1)
       end if
    end do

    if (debug) write(6,fmt='("a_minor: rtmp=",5f8.4)') rtmp
    if (debug) write(6,fmt='("a_minor: ztmp=",5f8.4)') ztmp

    spl = new_spline(ztmp, rtmp)
    r2 = splint(Z_mag, spl)
    call delete_spline(spl)

    a = (r2 - r1) / 2.
    if (debug) write(6,*) "a_minor: r1,r2,a=",r1,r2,a
  end subroutine a_minor

  !> Put `theta` into the range \([-\pi, \pi]\)
  elemental real function mod2pi (theta)
    use constants, only: pi, twopi
    implicit none
    real, intent(in) :: theta
    mod2pi = theta
    if (theta > pi .or. theta < -pi) then
       mod2pi = modulo(mod2pi + pi, twopi) - pi
    end if
  end function mod2pi

  !> FIXME : Add documentation
  !>
  !> @note Could we use the sorting module here?
  pure subroutine sort(a, b, c, d)
    real, dimension(:), intent(in out) :: a, b, c, d
    real :: tmp
    integer :: i, j, jmax
    jmax = size(a)
    do j = 1, jmax
       do i = 1, jmax - j
          if (a(i+1) < a(i)) then
             tmp = a(i); a(i) = a(i+1); a(i+1) = tmp
             tmp = b(i); b(i) = b(i+1); b(i+1) = tmp
             tmp = c(i); c(i) = c(i+1); c(i+1) = tmp
             tmp = d(i); d(i) = d(i+1); d(i+1) = tmp
          end if
       end do
    end do
  end subroutine sort

  !> Returns the distance to the magnetic axis as a function of rp
  !> (the normalised poloidal flux variable) and theta.
  real function rfun(self, r, theta)
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent(in) :: r, theta
    integer, parameter :: jmax = 5, kmax = 5
    real, parameter :: xerrsec = 1.e-9
    integer :: i, j, k, imax
    real :: fa, fb, bmult, thetroot, a, b, local_broot
    local_broot = self%bound(theta) ; thetroot = theta
    a = 0. ; b = local_broot ; bmult = 0.01
    fa = self%psi(a, thetroot) - r ; fb = self%psi(b, thetroot) - r
    ! need to bracket root, if we don't then try to search for a different b which does bracket
    if (fa * fb > 0.) then
       outer: do k = 1, kmax !Each k increases the distance we scan by (bmult)
          bmult = bmult * 2. ; imax = 10
          do j = 1, jmax !Each j increases the number of steps (imax)
             imax = imax * 2
             do i = 1, imax !Scan from local_broot to local_broot*(1 + bmult) in imax steps
                b = local_broot * (1 + bmult * float(i) / float(imax))
                fb = self%psi(b, thetroot) - r
                if (fa * fb <= 0.) exit outer !If a ; b brackets root skip ahead
             end do
             do i = 1, imax !Scan from local_broot to local_broot/(1 + bmult) in imax steps
                b = local_broot / (1 + bmult * float(i) / float(imax))
                fb = self%psi(b, thetroot) - r
                if (fa * fb <= 0.) exit outer  !If a ; b brackets root skip ahead
             end do
          end do
       end do outer !No promise that we have found a bracket here, but zbrent will report this.
    end if
    rfun = self%zbrent(a, b, r, thetroot, xerrsec)
  end function rfun

  !> FIXME : Add documentation
  real function zbrent(self, x1, x2, rootval, thetroot, tol)
    use mp, only: mp_abort
    implicit none
    class(abstract_eqfile_cart_geo_type), intent(in) :: self
    real, intent (in) :: x1, x2, rootval, thetroot, tol
    real, parameter :: eps = 3.e-8, eps1 = 2.e-8
    integer, parameter :: itmax = 100
    real :: a, b, c, fa, fb, fc, d, e, tol1, xm, q, p, r, s
    integer :: iter
    zbrent = 0
    a = x1 ; b = x2
    fa = self%psi(a, thetroot) - rootval ; fb = self%psi(b, thetroot) - rootval
    if (fb * fa > 0.) then !If we're not bracketing the root
       ! check to see if fa or fb is close to zero already:
       if (abs(fa) < eps1) then
          zbrent = a ; return
       else if (abs(fb) < eps1) then
          zbrent = b ; return
       else
          write(*,*) a, b, fa, fb
          call mp_abort('root must be bracketed for zbrent.', .true.)
       end if
    end if

    c = 0. ; fc = fb
    do iter = 1, itmax
       if (fb * fc > 0.) then ! If root not bracketed by b and c swap a and c
          c = a ; fc = fa
          d = b - a ; e = d
       end if
       if (abs(fc) < abs(fb)) then !If c closer than b cyclic shuffle a<-b<-c<-a
          a = b ; b = c ; c = a
          fa = fb ; fb = fc ; fc = fa
       end if
       tol1 = 2. * eps * abs(b) + 0.5 * tol
       xm = 0.5 * (c - b) ! Bisect c and b
       if (abs(xm) <= tol1 .or. fb == 0.) then
          zbrent = b ; return
       end if
       if (abs(e) >= tol1 .and. abs(fa) > abs(fb)) then !e might not be set by this point?
          s = fb / fa
          if (a == c) then
             p = 2. * xm * s
             q = 1. - s
          else
             q = fa / fc
             r = fb / fc
             p = s * (2. * xm * q * (q - r) - (b - a) * (r - 1.))
             q = (q - 1.) * (r - 1.) * (s - 1.)
          end if
          if (p > 0.) q = -q
          p = abs(p)
          if (2. * p  <  min(3. * xm * q - abs(tol1 * q), abs(e * q))) then
             e = d
             d = p / q
          else
             d = xm
             e = d
          end if
       else
          d = xm
          e = d
       end if
       a = b ; fa = fb
       if (abs(d)  >  tol1) then
          b = b + d
       else
          b = b + sign(tol1, xm)
       end if
       fb = self%psi(b, thetroot) - rootval
    end do
    call mp_abort('zbrent exceeding maximum iterations.',.true.)
  end function zbrent

  function new_flux_surface_type(geoType, Rmaj, R_geo, r, dr, aSurf, &
       sHorz, sVert, delm, deln, delmp, delnp, thm, thn, q, shat, nt, mMode, nMode, &
       n_mxh, c0_mxh, c_mxh, s_mxh, dc_mxh_dr, ds_mxh_dr) result(self)
    use optionals, only: get_option_with_default

    type(flux_surface_type) :: self
    integer, intent(in) :: geoType
    real, intent(in), optional :: Rmaj, R_geo, r, dr, aSurf, sHorz, sVert, delm, deln, delmp, delnp, thm, thn, q, shat, c0_mxh
    integer, intent(in), optional :: nt, mMode, nMode, n_mxh
    real, intent(in), optional, dimension(mxh_max_moments) :: c_mxh, dc_mxh_dr
    real, intent(in), optional, dimension(mxh_max_moments) :: s_mxh, ds_mxh_dr

    real, parameter :: invalid_value = -999.99
    integer, parameter :: invalid_int = -999

    self%geoType = geoType

    self%Rmaj = get_option_with_default(Rmaj, invalid_value)
    self%R_geo = get_option_with_default(R_geo, invalid_value)
    self%r = get_option_with_default(r, invalid_value)
    self%dr = get_option_with_default(dr, invalid_value)
    self%aSurf = get_option_with_default(aSurf, invalid_value)
    self%sHorz = get_option_with_default(sHorz, invalid_value)
    self%sVert = get_option_with_default(sVert, invalid_value)
    self%delm = get_option_with_default(delm, invalid_value)
    self%deln = get_option_with_default(deln, invalid_value)
    self%delmp = get_option_with_default(delmp, invalid_value)
    self%delnp = get_option_with_default(delnp, invalid_value)
    self%thm = get_option_with_default(thm, invalid_value)
    self%thn = get_option_with_default(thn, invalid_value)
    self%q = get_option_with_default(q, invalid_value)
    self%shat = get_option_with_default(shat, invalid_value)
    self%nt = get_option_with_default(nt, invalid_int)
    self%mMode = get_option_with_default(mMode, invalid_int)
    self%nMode = get_option_with_default(nMode, invalid_int)
    self%n_mxh = get_option_with_default(n_mxh, invalid_int)
    self%c0_mxh = get_option_with_default(c0_mxh, invalid_value)
    self%c_mxh = get_option_with_default(c_mxh, spread(invalid_value, 1, size(self%c_mxh)))
    self%s_mxh = get_option_with_default(s_mxh, spread(invalid_value, 1, size(self%s_mxh)))
    self%dc_mxh_dr = get_option_with_default(dc_mxh_dr, spread(invalid_value, 1, size(self%dc_mxh_dr)))
    self%ds_mxh_dr = get_option_with_default(ds_mxh_dr, spread(invalid_value, 1, size(self%ds_mxh_dr)))

  end function new_flux_surface_type
end module geo_utils