leq.f90 Source File


Contents

Source Code


Source Code

!> Analytic local equilibria via generalised forms of the Miller model
!>
!> Justin Ball's MIT Masters thesis and/or ["GS2 analytic geometry
!> specification"](https://bitbucket.org/gyrokinetics/wikifiles/raw/master/JRB/GS2GeoDoc.pdf)
!> provide a good overview of how this module works.
!>
!> There are four equilibrium models in this module:
!>
!> 1. generalised Miller
!> 2. global MHD
!> 3. generalised elongation
!> 4. Fourier
!>
!> These can be chosen via the `geoType` input parameter (`0-4`,
!> respectively)
!>
!> All of these models allow you to specify the amplitudes of two
!> shaping modes, along with their phases and radial derivatives. The
!> generalised elongation and Fourier models also allow you to specify
!> the exact mode numbers.
module leq
  use geo_utils, only: abstract_geo_type, flux_surface_type, geo_input_type, &
       geo_type_miller, geo_type_global, geo_type_generalized_elongation, geo_type_fourier_series, &
       geo_type_miller_extended_harmonic

  implicit none

  private

  public :: leq_type

  type, extends(abstract_geo_type) :: leq_type
     !> Used to translate definition of the poloidal angle such that the location
     !> of the maximium magnetic field is at pi (see section 3.2.3 of Ball MIT
     !> Masters thesis or section 2.4 of "GS2 analytic geometry specification")
     real :: thetaShift
     type(flux_surface_type) :: surf
   contains
     procedure :: finish_setup, finalise, read_and_set
     procedure :: rpos, zpos, rcenter, diameter, btori, dbtori, qfun
     procedure :: pfun, dpfun, betafun, psi, rfun, eqitem
     procedure :: geo_Miller, geo_global, geo_generalizedElongation, geo_FourierSeries
     procedure :: geo_MillerExtendedHarmonic
  end type leq_type

contains

  !> Set module-level parameters and initialise the leq module
  subroutine read_and_set(self, inputs)
    use mp, only: mp_abort
    use constants, only: twopi, pi
    implicit none
    class(leq_type), intent(in out) :: self
    type(geo_input_type), intent(in) :: inputs
    real, dimension(3) :: dr
    integer :: i, j
    real :: r, t
    self%type_name = 'Local-Analytic'
    if (inputs%surf%geoType > 4 .or. inputs%surf%geoType < 0) &
       call mp_abort("ERROR: invalid analytic geometry specification", .true.)

    self%surf = inputs%surf ;  self%thetaShift = inputs%thShift
    self%beta_0 = 1.
    self%nr = 3 ; self%nt = self%surf%nt + 1
    call self%alloc_arrays()
    dr = [-self%surf%dr, 0., self%surf%dr]
    ! Why do we set pressure to -dr here?
    self%pressure = -dr
    self%eqpsi = 1 + dr

    do j = 1, self%nt
       t = (j - 1) * twopi / real(self%nt - 1) - pi
       self%eqth(:, j) = t
       self%eqpsi_2d(:, j) = self%eqpsi
       do i = 1, self%nr
          r = self%surf%r + dr(i)
          self%R_psi(i, j) = self%Rpos(r, t)
          self%Z_psi(i, j) = self%Zpos(r, t)
       end do
    end do

    self%psi_0 = 0 ; self%psi_a = 1.0
    self%B_T = -1.0 ; self%R_mag = self%surf%Rmaj ; self%Z_mag = 0.0
    self%aminor = 1.0
    self%has_full_theta_range = .true.
  end subroutine read_and_set

  !> Finalise leq module
  pure subroutine finalise(self)
    implicit none
    class(leq_type), intent(in out) :: self
    self%initialised = .false.
    call self%dealloc_arrays
  end subroutine finalise

  !> Finish the leq setup
  pure subroutine finish_setup(self)
    class(leq_type), intent(in out) :: self
    call self%calculate_gradients
  end subroutine finish_setup

  !> Calculates `fstar` which is `f` interpolated at the location `theta_in`
  pure real function eqitem(self, r_in, theta_in, f, char) result(fstar)
    use geo_utils, only: mod2pi, find_lower_bound, bilinear_interpolate
    implicit none
    class(leq_type), intent(in) :: self
    real, intent(in) :: r_in !Ignored
    !> Desired theta location
    real, intent(in) :: theta_in
    !> Array to interpolate
    real, dimension(:, :), intent(in) :: f
    !> FIXME: Unused
    character(1), intent(in) :: char
    !> Found r and theta indices
    integer :: jstar, istar
    real :: thet
    !> Internal theta grid
    real, dimension(self%nt) :: mtheta
    real, dimension(self%nr) :: dim_1
    dim_1 = [1, 2, 3] ; mtheta = self%eqth(1, :)
    thet = mod2pi(theta_in)
    istar = 2 !find_lower_bound(2.5, dim_1, 0)  !Make 2 < r < 3 here so istar == 2
    jstar = find_lower_bound(thet, mtheta, self%nt - 1)
    fstar = bilinear_interpolate(f, dim_1, mtheta, istar, jstar, dim_1(istar), thet)
  end function eqitem

  !> Return the major radius at \((r, \theta)\)
  pure real function Rpos (self, r, theta)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real :: Zpos

    ! Choose one of four different analytic geometry specifications
    ! (see section 2.1 of "GS2 analytic geometry specification") ! JRB
    select case (self%surf%geoType)
       case (geo_type_miller)
          call self%geo_Miller(r, theta, Rpos, Zpos)
       case (geo_type_global)
          call self%geo_global(r, theta, Rpos, Zpos)
       case (geo_type_generalized_elongation)
          call self%geo_generalizedElongation(r, theta, Rpos, Zpos)
       case (geo_type_fourier_series)
          call self%geo_FourierSeries(r, theta, Rpos, Zpos)
       case (geo_type_miller_extended_harmonic)
          call self%geo_MillerExtendedHarmonic(r, theta, Rpos, Zpos)
    end select
  end function Rpos

  !> Return the axial coordinate at \((r, \theta)\)
  pure real function Zpos (self, r, theta)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real :: Rpos

    ! Choose one of four different analytic geometry specifications
    ! (see section 2.1 of "GS2 analytic geometry specification") ! JRB
    select case (self%surf%geoType)
       case (geo_type_miller)
          call self%geo_Miller(r, theta, Rpos, Zpos)
       case (geo_type_global)
          call self%geo_global(r, theta, Rpos, Zpos)
       case (geo_type_generalized_elongation)
          call self%geo_generalizedElongation(r, theta, Rpos, Zpos)
       case (geo_type_fourier_series)
          call self%geo_FourierSeries(r, theta, Rpos, Zpos)
       case (geo_type_miller_extended_harmonic)
          call self%geo_MillerExtendedHarmonic(r, theta, Rpos, Zpos)
    end select
  end function Zpos

  !> Calculate the cylindrical \((R, Z)$$ at $$(r, \theta)\) for the
  !> generalised Miller model
  !>
  !> See section 3.2.1 of Ball MIT Masters thesis or section 2.1.1 of
  !> "GS2 analytic geometry specification"
  pure subroutine geo_Miller(self, r, theta, Rpos, Zpos)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent(in) :: r, theta
    real, intent(out) :: Rpos, Zpos
    real :: dr, thAdj
    real :: Rcirc, Rcircp, Relong, Relongp, RelongTilt, RelongTiltp, Rtri, Rtrip, RtriTilt, RtriTiltp, Rfinal, Rfinalp
    real :: Zcirc, Zcircp, Zelong, Zelongp, ZelongTilt, ZelongTiltp, Ztri, Ztrip, ZtriTilt, ZtriTiltp, Zfinal, Zfinalp

    dr = r - self%surf%r

    thAdj = theta + self%thetaShift

    Rcirc = self%surf%r*cos(self%surf%thn - self%surf%thm - thAdj)
    Zcirc = -self%surf%r*sin(self%surf%thn - self%surf%thm - thAdj)
    Rcircp = cos(self%surf%thn - self%surf%thm - thAdj)
    Zcircp = -sin(self%surf%thn - self%surf%thm - thAdj)

    Relong = Rcirc
    Zelong = -self%surf%r*(self%surf%delm - 1)*sin(self%surf%thn - self%surf%thm - thAdj) + Zcirc
    Relongp = Rcircp
    Zelongp = -((self%surf%delm - 1) + self%surf%r*self%surf%delmp)*sin(self%surf%thn - self%surf%thm - thAdj) + Zcircp

    RelongTilt = cos(self%surf%thn - self%surf%thm)*Relong - sin(self%surf%thn - self%surf%thm)*Zelong
    ZelongTilt = sin(self%surf%thn - self%surf%thm)*Relong + cos(self%surf%thn - self%surf%thm)*Zelong
    RelongTiltp = cos(self%surf%thn - self%surf%thm)*Relongp - sin(self%surf%thn - self%surf%thm)*Zelongp
    ZelongTiltp = sin(self%surf%thn - self%surf%thm)*Relongp + cos(self%surf%thn - self%surf%thm)*Zelongp

    Rtri = -self%surf%r*cos(thAdj) + self%surf%r*cos(sin(thAdj)*self%surf%deln + thAdj) + RelongTilt
    Ztri = ZelongTilt
    Rtrip = -cos(thAdj) + cos(sin(thAdj)*self%surf%deln + thAdj) &
            - self%surf%r*sin(sin(thAdj)*self%surf%deln + thAdj)*sin(thAdj)*self%surf%delnp + RelongTiltp
    Ztrip = ZelongTiltp

    RtriTilt = cos(self%surf%thn)*Rtri + sin(self%surf%thn)*Ztri
    ZtriTilt = -sin(self%surf%thn)*Rtri + cos(self%surf%thn)*Ztri
    RtriTiltp = cos(self%surf%thn)*Rtrip + sin(self%surf%thn)*Ztrip
    ZtriTiltp = -sin(self%surf%thn)*Rtrip + cos(self%surf%thn)*Ztrip

    Rfinal = RtriTilt + self%surf%Rmaj
    Zfinal = ZtriTilt
    Rfinalp = RtriTiltp + self%surf%sHorz
    Zfinalp = ZtriTiltp + self%surf%sVert

    Rpos = Rfinal + dr*Rfinalp
    Zpos = Zfinal + dr*Zfinalp

  end subroutine geo_Miller

  !> Calculate the cylindrical \((R, Z)\) at \((r, \theta)\) for the
  !> global MHD model
  !>
  !> See section 2.1.2 of "GS2 analytic geometry specification"
  pure subroutine geo_global(self, r, theta, Rpos, Zpos)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real, intent (out) :: Rpos, Zpos
    real :: rho0, thAdj, rCyl, Rc, Zc
    real :: Cm, Cn, psiN, xAng, yAng, rAng

    rho0 = r/self%surf%aSurf

    thAdj = theta + self%thetaShift

    Cm=(self%surf%delm**2-1)/(self%surf%delm**2+1)
    Cn=(self%surf%deln**2-1)/(self%surf%deln**3+1)

    psiN=rho0**2+Cm*rho0**2+Cn*rho0**3

    if (abs(Cn*cos(3*(thAdj+self%surf%thn)))<10e-6) then
      rCyl=self%surf%aSurf*sqrt(psiN/(1+Cm*cos(2*(thAdj+self%surf%thm))))
    else
      xAng=2*(1+Cm*cos(2*(thAdj+self%surf%thm)))**3-27*Cn**2*psiN*cos(3*(thAdj+self%surf%thn))**2
      yAng=3*Cn*sqrt(3*psiN)*cos(3*(thAdj+self%surf%thn))*sqrt(xAng+2*(1+Cm*cos(2*(thAdj+self%surf%thm)))**3)

      rAng=(1/3.0)*atan2(yAng,xAng)

      rCyl=self%surf%aSurf*(1+Cm*cos(2*(thAdj+self%surf%thm)))/(3*Cn*cos(3*(thAdj+self%surf%thn))) &
           *(cos(rAng)+sqrt(3.0)*sin(rAng)-1)
    end if

    Rc=self%surf%sHorz-(self%surf%sHorz-self%surf%Rmaj)*rho0**2
    Zc=self%surf%sVert-(self%surf%sVert)*rho0**2

    Rpos=Rc+rCyl*cos(thAdj)
    Zpos=Zc+rCyl*sin(thAdj)

  end subroutine geo_global

  !> Calculate the cylindrical \((R, Z)\) at \((r, \theta)\) for the
  !> generalised elongation model
  !>
  !> See section 5.1.2 of Ball Oxford PhD thesis or section 2.1.3 of
  !> "GS2 analytic geometry specification"
  pure subroutine geo_generalizedElongation(self, r, theta, Rpos, Zpos)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real, intent (out) :: Rpos, Zpos
    real :: dr, thAdj, rCyl, rCylp, Rfinal, Zfinal, Rfinalp, Zfinalp

    dr = r - self%surf%r

    thAdj = theta + self%thetaShift

    rCyl=self%surf%r * (1 &
        + (-1+self%surf%delm/sqrt(1+(self%surf%delm**2-1)*cos(self%surf%mMode*(thAdj+self%surf%thm)/2.0)**2)) &
        + (-1+self%surf%deln/sqrt(1+(self%surf%deln**2-1)*cos(self%surf%nMode*(thAdj+self%surf%thn)/2.0)**2)))
    rCylp=1 &
     +(-1+(1+(self%surf%delm**2-1)*cos(self%surf%mMode*(thAdj+self%surf%thm)/2.0)**2)**(-0.5)*(self%surf%delm+self%surf%r*self%surf%delmp &
      *(1-(self%surf%delm*cos(self%surf%mMode*(thAdj+self%surf%thm)/2.0))**2/(1+(self%surf%delm**2-1)*cos(self%surf%mMode*(thAdj+self%surf%thm)/2.0)**2)))) &
     +(-1+(1+(self%surf%deln**2-1)*cos(self%surf%nMode*(thAdj+self%surf%thn)/2)**2)**(-0.5)*(self%surf%deln+self%surf%r*self%surf%delnp &
      *(1-(self%surf%deln*cos(self%surf%nMode*(thAdj+self%surf%thn)/2.0))**2/(1+(self%surf%deln**2-1)*cos(self%surf%nMode*(thAdj+self%surf%thn)/2.0)**2))))

    Rfinal=rCyl*cos(thAdj)+self%surf%Rmaj
    Zfinal=rCyl*sin(thAdj)
    Rfinalp=rCylp*cos(thAdj)+self%surf%sHorz
    Zfinalp=rCylp*sin(thAdj)+self%surf%sVert

    Rpos=Rfinal+dr*Rfinalp
    Zpos=Zfinal+dr*Zfinalp

  end subroutine geo_generalizedElongation

  !> Calculate the cylindrical \((R, Z)\) at \((r, \theta)\) for the
  !> Fourier model
  !>
  !> See section 5.1.1 of Ball Oxford PhD thesis or section 2.1.4 of
  !> "GS2 analytic geometry specification"
  pure subroutine geo_FourierSeries(self, r, theta, Rpos, Zpos)
    implicit none
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real, intent (out) :: Rpos, Zpos
    real :: dr, thAdj, rCyl, rCylp, Rfinal, Zfinal, Rfinalp, Zfinalp

    dr = r - self%surf%r

    thAdj = theta + self%thetaShift

    rCyl=self%surf%r * (1+((self%surf%delm-1)/2) * (1-cos(self%surf%mMode*(thAdj+self%surf%thm))) + ((self%surf%deln-1)/2) * (1-cos(self%surf%nMode*(thAdj+self%surf%thn))))
    rCylp=1 + (((self%surf%delm-1)/2) + (self%surf%r*self%surf%delmp/2)) * (1-cos(self%surf%mMode*(thAdj+self%surf%thm))) + (((self%surf%deln-1)/2) + (self%surf%r*self%surf%delnp/2)) * (1-cos(self%surf%nMode*(thAdj+self%surf%thn)))

    Rfinal=rCyl*cos(thAdj)+self%surf%Rmaj
    Zfinal=rCyl*sin(thAdj)
    Rfinalp=rCylp*cos(thAdj)+self%surf%sHorz
    Zfinalp=rCylp*sin(thAdj)+self%surf%sVert

    Rpos=Rfinal+dr*Rfinalp
    Zpos=Zfinal+dr*Zfinalp

  end subroutine geo_FourierSeries

  !> Calculate the cylindrical \((R, Z)\) at \((r, \theta)\) for the
  !> Miller Extended Harmonic model
  !>
  !> See [PPCF 63 (2021) 012001 (5pp)](https://doi.org/10.1088/1361-6587/abc63b)
  pure subroutine geo_MillerExtendedHarmonic(self, r, theta, Rpos, Zpos)
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    real, intent (out) :: Rpos, Zpos

    integer :: n
    real :: theta_R, dthetaR_dr, dr, dZdr, dRdr

    theta_R = theta + self%surf%c0_mxh &
         + sum([(self%surf%c_mxh(n) * cos(n * theta) + self%surf%s_mxh(n) * sin(n * theta), &
                n = 1, self%surf%n_mxh)])

    dthetaR_dr = sum([(&
         (self%surf%dc_mxh_dr * cos(n * theta)) &
         + (self%surf%ds_mxh_dr * sin(n * theta)), &
         n = 1, self%surf%n_mxh)])

    dr = r - self%surf%r

    dRdr = self%surf%sHorz + cos(theta_R) - self%surf%r * sin(theta_R) * dthetaR_dr
    dZdr = self%surf%sVert + self%surf%delm * sin(theta) * (1 + self%surf%delmp)

    Rpos = self%surf%Rmaj + (self%surf%r * cos(theta_R)) + (dr * dRdr)
    Zpos = (self%surf%r * self%surf%delm * sin(theta)) + (dr * dZdr)

  end subroutine geo_MillerExtendedHarmonic

  !> FIXME : Add documentation
  pure real function psi (self, r, theta)
    class(leq_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 - self%surf%r
    end if
  end function psi

  !> FIXME : Add documentation | Should this be equivalent to psi as in others?
  pure real function rfun (self, r, theta)
    class(leq_type), intent(in) :: self
    real, intent (in) :: r, theta
    rfun = r
  end function rfun

  !> FIXME : Add documentation
  pure real function diameter (self, rp)
    class(leq_type), intent(in) :: self
    real, intent(in) :: rp
    diameter = 2. * rp
  end function diameter

  !> Return the major radius of the centre of the flux surface
  !> @Note rcenter for other geo types returns the major radius of the LCFS
  !> which is named R_geo in geometry. We do store R_geo here so could return that?
  pure real function rcenter(self, rp)
    class(leq_type), intent(in) :: self
    real, intent(in) :: rp
    ! If using the global geometry, Rmaj is the center of the shaped flux surface
    ! (i.e. the surface at rhoc=aSurf), so here we calculate the major radial
    ! location of the center of the flux surface of interest ! JRB
    rcenter = self%surf%Rmaj
    if (self%surf%geoType == geo_type_global) then
       rcenter = rcenter + (1 - (self%surf%r / self%surf%aSurf)**2) * self%surf%sHorz
    end if
  end function rcenter

  !> FIXME : Add documentation
  pure real function dbtori (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    dbtori = 0
  end function dbtori

  !> FIXME : Add documentation
  pure real function btori (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    btori = self%surf%R_geo
  end function btori

  !> FIXME : Add documentation
  pure real function qfun (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    qfun = self%surf%q
  end function qfun

  !> FIXME : Add documentation
  pure real function pfun (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    pfun = 0.5 * self%beta_0
  end function pfun

  !> FIXME : Add documentation
  pure real function dpfun (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    dpfun = 0
  end function dpfun

  !> FIXME : Add documentation
  pure real function betafun (self, pbar)
    class(leq_type), intent(in) :: self
    real, intent(in) :: pbar
    betafun = self%beta_0
  end function betafun
end module leq