ballstab.f90 Source File


Contents

Source Code


Source Code

!> A small module used to calculate ideal ballooning stability.
!! Based on the original ball program in geo/ball.f90 of GS2
!! but tweaked to integrate closer to full GS2 runs etc.
module ballstab
  use abstract_config, only: abstract_config_type, CONFIG_MAX_NAME_LEN
  implicit none

  private

  !Routines
  public :: init_ballstab, finish_ballstab
  public :: run_stability_check, is_unstable
  public :: write_stability_ascii_1d, write_stability_ascii_2d

  !Vars
  public :: stability

  public :: ballstab_config_type
  public :: set_ballstab_config
  public :: get_ballstab_config
  
  logical :: make_salpha
  integer :: n_shat, n_beta
  real :: shat_min, shat_max, theta0
  real :: beta_mul, beta_div
  real, dimension(:), allocatable :: shat_arr, beta_arr, dbdrho_arr
  integer, dimension(:,:), allocatable :: stability
  real :: diff

  logical :: initialised = .false. 
  character(len=14) :: namelist_name="ballstab_knobs"

  !> Used to represent the input configuration of ballstab
  type, extends(abstract_config_type) :: ballstab_config_type
     ! namelist : ballstab_knobs
     ! indexed : false
     !> The minimum \(\beta^\prime\) in scans is
     !> `beta_prime_equilibrium/beta_div`
     real :: beta_div = 1.0
     !> The maximum \(\beta^\prime\) in scans is
     !> `beta_prime_equilibrium*beta_mul`
     real :: beta_mul = 1.0
     !> Controls decentring used in the numerical integration used for
     !> solving the ballooning equation. Recommended values are either
     !> 0 (default) or 1/3.
     real :: diff = 0.0
     !> Not currently used for anything.
     !>
     !> @todo Consider removing this variable
     logical :: make_salpha = .false.
     !> How many \(\beta^\prime\) values should be used in s-alpha
     !> type scans.
     integer :: n_beta = 1
     !> How many \(\hat{s}\) values should be used in s-alpha type
     !> scans.
     integer :: n_shat = 1
     !> The maximum value of \(\hat{s}\) to use in s-alpha type scans.
     !>
     !> @note This gets a smart default of the equilibrium value of \(\hat{s}\)
     real :: shat_max = 0.0
     !> The minimum value of \(\hat{s}\) to use in s-alpha type scans.
     !>
     !> @note This gets a smart default of the equilibrium value of \(\hat{s}\)
     real :: shat_min = 0.0
     !> The theta0 value to use in solving the ideal ballooning problem. We don't
     !> currently provide a means to study multiple theta0 in a single run. This
     !> may change in the future.
     real :: theta0 = 0.0
   contains
     procedure, public :: read => read_ballstab_config
     procedure, public :: write => write_ballstab_config
     procedure, public :: reset => reset_ballstab_config
     procedure, public :: broadcast => broadcast_ballstab_config
     procedure, public, nopass :: get_default_name => get_default_name_ballstab_config
     procedure, public, nopass :: get_default_requires_index => get_default_requires_index_ballstab_config
  end type ballstab_config_type

  type(ballstab_config_type) :: ballstab_config
  
contains
  
  !> Initialise grids, other modules etc.
  subroutine init_ballstab(ballstab_config_in)
    use theta_grid, only: init_theta_grid
    implicit none
    type(ballstab_config_type), intent(in), optional :: ballstab_config_in
    
    !Other modules
    call init_theta_grid

    !This module
    call real_init_ballstab(ballstab_config_in)
  end subroutine init_ballstab

  !> Finalise this module
  subroutine finish_ballstab
    implicit none
    !>Deallocate
    call dealloc_arrays

    initialised = .false.
    call ballstab_config%reset()
  end subroutine finish_ballstab

  !> Initialise this module
  subroutine real_init_ballstab(ballstab_config_in)
    use mp, only: proc0
    implicit none
    type(ballstab_config_type), intent(in), optional :: ballstab_config_in    

    if(initialised) return
    initialised = .true.

    !Get settings
    call read_parameters(ballstab_config_in)

    !Note only proc0 will be allowed to do any work
    if (.not.proc0) return
    
    !Verify parameters
    call verify_parameters

    !Create arrays
    call setup_arrays

  end subroutine real_init_ballstab

  !> Read parameters
  subroutine read_parameters(ballstab_config_in)
    use theta_grid, only: shat
    implicit none
    type(ballstab_config_type), intent(in), optional :: ballstab_config_in    
    logical :: exist

    if (present(ballstab_config_in)) ballstab_config = ballstab_config_in

    !Smart defaults
    if (.not.ballstab_config%is_initialised()) then    
       ballstab_config%shat_min = shat
       ballstab_config%shat_max = shat
    end if
    
    call ballstab_config%init(name = 'ballstab_knobs', requires_index = .false.)

    ! Copy out internal values into module level parameters
    beta_div = ballstab_config%beta_div
    beta_mul = ballstab_config%beta_mul
    diff = ballstab_config%diff
    make_salpha = ballstab_config%make_salpha
    n_beta = ballstab_config%n_beta
    n_shat = ballstab_config%n_shat
    shat_max = ballstab_config%shat_max
    shat_min = ballstab_config%shat_min
    theta0 = ballstab_config%theta0

    exist = ballstab_config%exist
  end subroutine read_parameters

  !> Check parameters are consistent etc.
  subroutine verify_parameters
    use geometry, only: bishop, local_eq
    use file_utils, only: error_unit
    implicit none
    integer :: ierr

    ierr = error_unit()

    !Disable scan in s_hat when incompatible bishop used
    if (n_shat > 1) then
       select case(bishop)
       case(2,3,4,5,8,9)
          !These are valid values
       case default
          if( (bishop /= 0) .and. local_eq) then
             !This is also ok
          else
             !This is not ok
             n_shat = 1
             write(ierr,'("ERROR : Cannot scan in shat for current combination of bishop and equilbrium type")')
          end if
       end select
    end if

    !Disable scan in beta_prime when incompatible bishop used
    if (n_beta > 1) then
       select case(bishop)
       case(2,3,4,5,6,7,8,9)
          !These are valid values
       case default
          !This is not ok
          n_beta = 1
          write(ierr,'("ERROR : Cannot scan in beta_prime for current combination of bishop and equilbrium type")')
       end select
    end if
  end subroutine verify_parameters

  !> Allocate+populate arrays
  subroutine setup_arrays
    implicit none
    integer :: is, ib
    real :: ds, db, b_min, b_max, bp
    
    allocate(shat_arr(n_shat))
    allocate(beta_arr(n_beta))
    allocate(dbdrho_arr(n_beta))
    dbdrho_arr = 0. !This will be populated later

    allocate(stability(n_shat,n_beta))
    stability = 0

    ds = 0
    if(n_shat.gt.1) ds = (shat_max-shat_min)/(n_shat-1)
    do is=1,n_shat
       shat_arr(is)=shat_min+ds*(is-1)
    enddo

    db = 0
    call get_beta_prime(bp)
    b_max = bp*beta_mul
    b_min = bp/beta_div
    if(n_beta.gt.1) then
       db = (b_max-b_min)/(n_beta-1)
    end if
    do ib=1,n_beta
       beta_arr(ib)=b_min+db*(ib-1)
    enddo
  end subroutine setup_arrays

  !> Deallocate arrays
  subroutine dealloc_arrays
    implicit none
    if(allocated(shat_arr)) deallocate(shat_arr)
    if(allocated(beta_arr)) deallocate(beta_arr)
    if(allocated(dbdrho_arr)) deallocate(dbdrho_arr)
    if(allocated(stability)) deallocate(stability)
  end subroutine dealloc_arrays

  !> Write a namelist based on current values
  subroutine wnml_ballstab(unit)
    implicit none
    integer, intent(in) :: unit

    !Header
    write(unit,'()')
    write(unit,'("&",A)') trim(adjustl(namelist_name))

    !Values
    write(unit,'(" make_salpha = ",L1)') make_salpha
    write(unit,'(" n_shat      = ",I0)') n_shat
    if(n_shat.gt.1) then
       write(unit,'(" shat_min    = ",F12.5)') shat_min
       write(unit,'(" shat_max    = ",F12.5)') shat_max
    endif
    write(unit,'(" n_beta      = ",I0)') n_beta

    !Footer
    write(unit,'("/")')
  end subroutine wnml_ballstab

  !> Check value consistency etc.
  subroutine check_ballstab(report_unit)
    implicit none
    integer, intent(in) :: report_unit
  end subroutine check_ballstab

  !> Gets the current value of shat
  subroutine get_shat(shat_out)
    use geometry, only: surf
    implicit none
    real, intent(out) :: shat_out
    shat_out = surf%shat
  end subroutine get_shat

  !> Sets the value of shat
  subroutine set_shat(shat_in)
    use geometry, only: surf
    implicit none
    real, intent(in) :: shat_in
    !Note for bishop = 6,7 or not in 2-9 and .not. local_eq cannot change shat
    !Should flag this somewhere (during init)
    surf%shat = shat_in
  end subroutine set_shat

  !> Gets the current value of beta_prime (or equivalent var)
  !! Really just gets the variable that we can use to control 
  !! the pressure gradient.
  subroutine get_beta_prime(bp_out)
    use geometry, only: bishop, p_prime_input, invLp_input, beta_prime_input, alpha_input, dp_mult
    implicit none
    real, intent(out) :: bp_out

    select case (bishop)
    case(2)
       bp_out = p_prime_input
    case(3)
       bp_out = invLp_input
    case(4)
       bp_out = beta_prime_input
    case(5)
       bp_out = alpha_input
    case(6)
       bp_out = beta_prime_input
    case(7)
       bp_out = dp_mult
    case(8)
       bp_out = beta_prime_input
    case(9)
       bp_out = beta_prime_input
    case default
       bp_out = 0.0
       print*,"ERROR: For bishop not in 2-9 cannot modify the pressure gradient used"
    end select
  end subroutine get_beta_prime

  !> Sets the current value of beta_prime (or equivalent var)
  !! Really just sets the variable that we can use to control 
  !! the pressure gradient.
  subroutine set_beta_prime(bp_in)
    use geometry, only: bishop, p_prime_input, invLp_input, beta_prime_input, alpha_input, dp_mult
    implicit none
    real, intent(in) :: bp_in

    select case (bishop)
    case(2)
       p_prime_input = bp_in
    case(3)
       invLp_input = bp_in
    case(4)
       beta_prime_input = bp_in
    case(5)
       alpha_input = bp_in
    case(6)
       beta_prime_input = bp_in
    case(7)
       dp_mult = bp_in
    case(8)
       beta_prime_input = bp_in
    case(9)
       beta_prime_input = bp_in
    case default
       print*,"ERROR: For bishop not in 2-9 cannot modify the pressure gradient used"
    end select
  end subroutine set_beta_prime

  !> Runs the stability check scan for the module level shat/beta array values
  subroutine run_stability_check
    use file_utils, only: open_output_file, close_output_file
    implicit none
    integer :: is, ib

    !Exit if not initialised
    if(.not.initialised) return

    !Check stability over range
    do ib = 1, n_beta
       do is = 1, n_shat
          call check_stability(shat_arr(is), beta_arr(ib), &
               dbdrho_arr(ib), stability(is, ib), theta0)
       end do
    end do
  end subroutine run_stability_check

  !> Routine to write out stability data to 1D ascii file
  subroutine write_stability_ascii_1d
    use file_utils, only: open_output_file, close_output_file
    implicit none
    integer :: is, ib, unit

    call open_output_file(unit,".ballstab_1d")
    write(unit,'(4(A12," "))') "shear", "beta_prime", "dbetadrho", "stability"
    do ib=1,n_beta
       do is=1,n_shat
          write(unit,'(3(F12.4," "),I12)') shat_arr(is), beta_arr(ib), dbdrho_arr(ib), stability(is,ib)
       enddo
    enddo
    call close_output_file(unit)
  end subroutine write_stability_ascii_1d

  !> Routine to write out stability data to 2D ascii file + 1d axis data
  subroutine write_stability_ascii_2d
    use file_utils, only: open_output_file, close_output_file
    implicit none
    integer :: is, ib, unit

    !/2D -- data
    call open_output_file(unit,".ballstab_2d")
    do ib=1,n_beta
       do is=1,n_shat
          write(unit,'(I0," ")',advance="no") stability(is,ib)
       enddo
       write(unit,'()') !Move to the next line
    enddo

    !/2D -- axis
    call open_output_file(unit,".ballstab_shat")
    !Commented lines here can be used to give row output
    do is=1,n_shat
       !write(unit,'(F9.2," ")',advance="no") shat_arr(is)
       write(unit,'(F9.2)') shat_arr(is)
    enddo
    !write(unit,'()')
    call close_output_file(unit)

    call open_output_file(unit,".ballstab_bp")
    do ib=1,n_beta
       write(unit,'(F12.4)') beta_arr(ib)
    enddo
    call close_output_file(unit)

    call open_output_file(unit,".ballstab_dbdr")
    do ib=1,n_beta
       write(unit,'(F12.4)') dbdrho_arr(ib)
    enddo
    call close_output_file(unit)
  end subroutine write_stability_ascii_2d

  !> Test if given shat/beta is unstable
  subroutine check_stability(shat_in, beta_prime_in, dbetadrho_out, &
       iunstable, theta0, restore)
    !Note iunstable>0 means unstable - Could use a logical but size may be interesting
    use theta_grid_params, only: ntheta, nperiod
    use geometry, only: eikcoefs, eikcoefs_output_type
    use optionals, only: get_option_with_default
    implicit none
    real, intent(in) :: shat_in, beta_prime_in
    real, intent(out) :: dbetadrho_out
    integer, intent(out) :: iunstable
    real, intent(in), optional :: theta0
    logical, intent(in), optional :: restore
    type(eikcoefs_output_type) :: eikcoefs_results
    real :: shat_bak, beta_prime_bak
    integer :: ntgrid
    logical :: local_restore
    
    local_restore = get_option_with_default(restore, .false.)

    !Store initial state if we're going to restore
    if (local_restore) then
       call get_shat(shat_bak)
       call get_beta_prime(beta_prime_bak)
    end if

    !Setup geometry
    !NOTE: As how we can influence beta_prime (and shat) varies with bishop
    !we actually end up setting different parameters in different cases. The
    !physical parameter dbetadrho should really be the relevant parameter.
    call set_beta_prime(beta_prime_in)
    call set_shat(shat_in)
    call eikcoefs(ntheta, nperiod, eikcoefs_results) !Recalculate geometry terms

    !Here we pass out the value of dbetadrho, this should be bishop value independent
    dbetadrho_out = eikcoefs_results%dbetadrho

    !Note we don't use theta_grid:ntgrid as in the case that
    !gridgen resizes the theta grid used by GS2 we don't end up
    !resizing any of the geometry module arrays (that we use directly here)
    !Really we would like to use everything from theta_grid directly but we'd
    !need some way to force theta_grid to update it's internal arrays when we
    !change parameters
    ntgrid=ubound(eikcoefs_results%theta,1)

    !Test stability
    iunstable = is_unstable(ntgrid, eikcoefs_results%theta, eikcoefs_results%bmag, &
         eikcoefs_results%dbetadrho, eikcoefs_results%gradpar, eikcoefs_results%gds2, &
         eikcoefs_results%gds21, eikcoefs_results%gds22, eikcoefs_results%cvdrift, &
         eikcoefs_results%cvdrift0, theta0)

    !Restore geometry if requested
    if (local_restore) then
       call set_beta_prime(beta_prime_bak)
       call set_shat(shat_bak)
       call eikcoefs(ntheta, nperiod, eikcoefs_results)
    end if
  end subroutine check_stability

  !> Test stability of current system for passed theta0 and geometry -- return integer
  !>
  !> Note is_unstable>0 means unstable - Could use a logical but size
  !> may be interesting.
  pure integer function is_unstable(ntgrid, theta, bmag, dbetadrho, gradpar, &
       gds2, gds21, gds22, cvdrift, cvdrift0, theta0_in) &
       result(is_unstable_out)
    use optionals, only: get_option_with_default
    implicit none
    integer, intent(in) :: ntgrid
    real, dimension(-ntgrid:ntgrid), intent(in) :: theta, bmag, gradpar
    real, intent(in) :: dbetadrho
    real, dimension(-ntgrid:ntgrid), intent(in) :: gds2, gds21, gds22, cvdrift, cvdrift0
    real, intent(in), optional :: theta0_in
    !<DD> NEED TO IMPROVE VARIABLE NAMES
    real, dimension(-ntgrid:ntgrid) :: g, c, psi, c1, c2, ch, g1, g2, gh
    real, dimension(-ntgrid:ntgrid-1) :: delthet
    integer :: ig
    real :: one_m_diff, psi_prime, theta0

    !Initialise
    theta0 = get_option_with_default(theta0_in, 0.0)
    is_unstable_out = 0
    one_m_diff = 1 - diff

    !Calculate geometry arrays
    delthet = theta(-ntgrid+1:) - theta(:ntgrid-1)

    g = (gds2 + 2 * theta0 * gds21 + theta0**2 * gds22) * gradpar / bmag
    c = -0.5 * dbetadrho * (cvdrift + theta0 * cvdrift0) / (bmag * gradpar)

    ! Get grid centre values -- note we don't set the value at -ntgrid
    do ig=-ntgrid+1, ntgrid
       ch(ig)=0.5*(c(ig)+c(ig-1))
       gh(ig)=0.5*(g(ig)+g(ig-1))
    enddo

    !Calculate coefficients -- note we don't set the value at -ntgrid or ntgrid
    do ig=-ntgrid+1,ntgrid-1
       c1(ig) = -delthet(ig)*(one_m_diff*c(ig)+0.5*diff*ch(ig+1)) &
            -delthet(ig-1)*(one_m_diff*c(ig)+0.5*diff*ch(ig)) &
            -delthet(ig-1)*0.5*diff*ch(ig)
       c1(ig)=0.5*c1(ig)
    enddo

    !Calculate coefficients -- note we don't set the value at -ntgrid
    do ig=-ntgrid+1,ntgrid
       c2(ig) = -0.25*diff*ch(ig)*delthet(ig-1)
       g1(ig) = gh(ig)/delthet(ig-1)
       g2(ig) = 1.0/(0.25*diff*ch(ig)*delthet(ig-1)+gh(ig)/delthet(ig-1))
    enddo

    !Set boundary values
    psi(-ntgrid)=0.
    psi(-ntgrid+1)=delthet(-ntgrid)
    psi_prime=psi(-ntgrid+1)/g2(-ntgrid+1)!-g1(-ntgrid+1)*psi(-ntgrid) !Second term zero by defn

    !Integrate along in theta
    do ig=-ntgrid+1,ntgrid-1
       psi_prime=psi_prime+c1(ig)*psi(ig)+c2(ig)*psi(ig-1)
       psi(ig+1)=(g1(ig+1)*psi(ig)+psi_prime)*g2(ig+1)
    enddo

    !Find how many times psi crosses axis || Actually how many times it hits zero
    !If we really do just want to count crossings then need to replace le with lt
    ! Could we replace this with
    ! is_unstable_out = count(psi(-ntgrid+1:ntgrid-1) * psi(-ntgrid+2:) <= 0)
    do ig=-ntgrid+1,ntgrid-1
       if(psi(ig)*psi(ig+1) .le. 0 ) is_unstable_out = is_unstable_out + 1
    enddo

    !Probably want to think about writing out psi if debug
  end function is_unstable

  !> Set the module level config type
  !> Will abort if the module has already been initialised to avoid
  !> inconsistencies.
  subroutine set_ballstab_config(ballstab_config_in)
    use mp, only: mp_abort
    type(ballstab_config_type), intent(in), optional :: ballstab_config_in
    if (initialised) then
       call mp_abort("Trying to set ballstab_config when already initialised.", to_screen = .true.)
    end if
    if (present(ballstab_config_in)) then
       ballstab_config = ballstab_config_in
    end if
  end subroutine set_ballstab_config

  !> Get the module level config instance
  function get_ballstab_config()
    type(ballstab_config_type) :: get_ballstab_config
    get_ballstab_config = ballstab_config
  end function get_ballstab_config

  !---------------------------------------
  ! Following is for the config_type
  !---------------------------------------
  
  !> Reads in the ballstab_knobs namelist and populates the member variables
  subroutine read_ballstab_config(self)
    use file_utils, only: input_unit_exist, get_indexed_namelist_unit
    use mp, only: proc0
    implicit none
    class(ballstab_config_type), intent(in out) :: self
    logical :: exist
    integer :: in_file

    ! Note: When this routine is in the module where these variables live
    ! we shadow the module level variables here. This is intentional to provide
    ! isolation and ensure we can move this routine to another module easily.    
    integer :: n_beta, n_shat
    logical :: make_salpha
    real :: beta_div, beta_mul, diff, shat_max, shat_min, theta0

    namelist /ballstab_knobs/ beta_div, beta_mul, diff, make_salpha, n_beta, n_shat, shat_max, shat_min, theta0

    ! Only proc0 reads from file
    if (.not. proc0) return

    ! First set local variables from current values
    beta_div = self%beta_div
    beta_mul = self%beta_mul
    diff = self%diff
    make_salpha = self%make_salpha
    n_beta = self%n_beta
    n_shat = self%n_shat
    shat_max = self%shat_max
    shat_min = self%shat_min
    theta0 = self%theta0

    ! Now read in the main namelist
    in_file = input_unit_exist(self%get_name(), exist)
    if (exist) read(in_file, nml = ballstab_knobs)

    ! Now copy from local variables into type members
    self%beta_div = beta_div
    self%beta_mul = beta_mul
    self%diff = diff
    self%make_salpha = make_salpha
    self%n_beta = n_beta
    self%n_shat = n_shat
    self%shat_max = shat_max
    self%shat_min = shat_min
    self%theta0 = theta0

    self%exist = exist
  end subroutine read_ballstab_config

  !> Writes out a namelist representing the current state of the config object
  subroutine write_ballstab_config(self, unit)
    implicit none
    class(ballstab_config_type), intent(in) :: self
    integer, intent(in) , optional:: unit
    integer :: unit_internal

    unit_internal = 6 ! @todo -- get stdout from file_utils
    if (present(unit)) then
       unit_internal = unit
    endif

    call self%write_namelist_header(unit_internal)
    call self%write_key_val("beta_div", self%beta_div, unit_internal)
    call self%write_key_val("beta_mul", self%beta_mul, unit_internal)
    call self%write_key_val("diff", self%diff, unit_internal)
    call self%write_key_val("make_salpha", self%make_salpha, unit_internal)
    call self%write_key_val("n_beta", self%n_beta, unit_internal)
    call self%write_key_val("n_shat", self%n_shat, unit_internal)
    call self%write_key_val("shat_max", self%shat_max, unit_internal)
    call self%write_key_val("shat_min", self%shat_min, unit_internal)
    call self%write_key_val("theta0", self%theta0, unit_internal)
    call self%write_namelist_footer(unit_internal)
  end subroutine write_ballstab_config

  !> Resets the config object to the initial empty state
  subroutine reset_ballstab_config(self)
    class(ballstab_config_type), intent(in out) :: self
    type(ballstab_config_type) :: empty
    select type (self)
    type is (ballstab_config_type)
       self = empty
    end select
  end subroutine reset_ballstab_config

  !> Broadcasts all config parameters so object is populated identically on
  !! all processors
  subroutine broadcast_ballstab_config(self)
    use mp, only: broadcast
    implicit none
    class(ballstab_config_type), intent(in out) :: self
    call broadcast(self%beta_div)
    call broadcast(self%beta_mul)
    call broadcast(self%diff)
    call broadcast(self%make_salpha)
    call broadcast(self%n_beta)
    call broadcast(self%n_shat)
    call broadcast(self%shat_max)
    call broadcast(self%shat_min)
    call broadcast(self%theta0)

    call broadcast(self%exist)
  end subroutine broadcast_ballstab_config

  !> Gets the default name for this namelist
  function get_default_name_ballstab_config()
    implicit none
    character(len = CONFIG_MAX_NAME_LEN) :: get_default_name_ballstab_config
    get_default_name_ballstab_config = "ballstab_knobs"
  end function get_default_name_ballstab_config

  !> Gets the default requires index for this namelist
  function get_default_requires_index_ballstab_config()
    implicit none
    logical :: get_default_requires_index_ballstab_config
    get_default_requires_index_ballstab_config = .false.
  end function get_default_requires_index_ballstab_config
end module ballstab