gray/src/coreprofiles.f90
Michele Guerini Rocco 45ef9c5eae
src/coreprofiles: make psnbnd fully automatic
1. Fix the mismatch between the psnbnd in coreprofiles and gray_core.
   This happens whenever gray overrides the externally provided one
   (i.e. the density tail would become negative before psnbnd and is so
   rescaled to end exactly on the zero).

2. Make psnbnd no longer required by always computing it as in 1.
   It hasn't been removed, because gray_params.data is sacrosant,
   but it no longer has any effect.

3. Cleanup: mark public functions, restructure the global variables into
   three categories; add comments explaining the analytical profiles
   format, formulae and how the polynomial tail is computed.
2022-05-22 04:06:21 +02:00

598 lines
20 KiB
Fortran

! This modules handles the loading, interpolation and evaluation of the
! plasma profiles (density, temperature, effective charge)
!
! Two kinds of profiles are supported: analytical (suffix `_an` in the
! subroutine names) or numerical (suffix `_spline`). For the latter, the
! the data is interpolated using splines.
module coreprofiles
use const_and_precisions, only : wp_, zero, one
implicit none
! Parameters of the plasma profiles splines
type spline_parameters
integer :: ndata ! Number of data points
integer :: nknots ! Number of spline knots
! Density spline (ψ, knots, B-spline coefficients)
real(wp_), dimension(:), allocatable :: knots, coeffs
! Temperature and effective charge arrays (ψ, T(ψ), Zeff(ψ))
real(wp_), dimension(:), allocatable :: psi
real(wp_), dimension(:, :), allocatable :: temp, zeff
end type
! Parameters of the C² polynomial tail of the density spline
type density_tail
real(wp_) :: start ! ψ₀, start of the tail
real(wp_) :: end ! ψ₁, end of the end
real(wp_) :: value ! s(ψ₀), value at the start
real(wp_) :: deriv1 ! s'(ψ₀), first derivative at the start
real(wp_) :: deriv2 ! s"(ψ₀), second derivative at the start
end type
! Parameters of the analytical profiles model
type analytic_model
real(wp_) :: dens0 ! Density scaling factor
real(wp_) :: n1, n2 ! Density exponents
real(wp_) :: te0, te1 ! Temperature at ψ=0, ψ=1
real(wp_) :: t1, t2 ! Temperature exponents
real(wp_) :: zeff ! Effective charge
end type
! Global variable storing the state of the module
type(spline_parameters), save :: spline
type(density_tail), save :: tail
type(analytic_model), save :: model
private
public read_profiles, read_profiles_an ! Reading data files
public scale_profiles ! Applying rescaling
public density, temp, fzeff ! Accessing interpolated data
public set_profiles_spline, set_profiles_an ! Initialising internal state
public unset_profiles_spline ! Deinitialising internal state
contains
subroutine density(psin, dens, ddens)
! Computes the density its first derivative as a function of
! normalised poloidal flux.
!
! Note: density has units of 10¹⁹ m⁻³.
use gray_params, only : iprof
use dierckx, only : splev, splder
use logger, only : log_error
implicit none
! subroutine arguments
real(wp_), intent(in) :: psin ! normalised poloidal flux
real(wp_), intent(out) :: dens, ddens ! density and first derivative
! local variables
integer :: ier ! dierck error code
real(wp_) :: f(1) ! dierck output (must be an array)
real(wp_) :: wrkfd(spline%ndata+4) ! dierck working space array
character(256) :: msg ! for log messages formatting
! Initialise both to zero
dens = zero
ddens = zero
! Outside the tail end both density and its
! derivatives are identically zero
if (psin >= tail%end .or. psin < 0) return
if (iprof == 0) then
! Use the analytical model
!
! n(ψ) = dens0⋅(1 - ψ^aln1)^aln2
!
if (psin > 1) return
dens = model%dens0 * (1 - psin**model%n1)**model%n2
ddens = -model%dens0 * model%n1*model%n2 * psin**(model%n1 - 1) &
* (1 - psin**model%n1)**(model%n2 - 1)
else
! Use the numerical data
if (psin < tail%start) then
! Use the interpolating spline when in range
! Evaluate the spline
ier = 0
call splev(spline%knots, spline%nknots, spline%coeffs, &
3, [psin], f, 1, ier)
dens = f(1)
! Evaluate the spline 1st derivative
ier = 0
call splder(spline%knots, spline%nknots, spline%coeffs, &
3, 1, [psin], f, 1, wrkfd, ier)
ddens = f(1)
if (abs(dens) < 1.0e-10_wp_) dens = zero
else
! Use a C² polynomial extension outside (ψ > ψ₀)
! The tail consists of the product p(ψ)⋅h(t), where:
!
! - p(ψ) is the 2nd order Taylor polynomial of the spline,
! centered at ψ₀. See set_profiles_spline for details.
!
! - h(t) is a "smoothing" polynomial in the variable
! t = (ψ - ψ₀)/(ψ₁ - ψ₀), defined as:
!
! h(t) = (1 - t)³⋅(1 + 3t + 6t²)
!
! with the following properties:
!
! h(0) = 1 h'(0)=0 h"(0)=0
! h(1) = 0 h'(1)=0 h"(1)=0
block
real(wp_) :: dpsi, t, p, dp, h, dh
dpsi = psin - tail%start ! Δψ = (ψ - ψ₀)
! Taylor polynomial p(ψ) and its derivative
p = tail%value + dpsi*tail%deriv1 + dpsi**2*tail%deriv2/2
dp = tail%deriv1 + dpsi*tail%deriv2
! Smoothing polynomial h(t) and its derivative
t = dpsi/(tail%end - tail%start)
h = (1 - t)**3 * (1 + 3*t + 6*t**2)
dh = -30*(1 - t)**2 * t**2 / (tail%end - tail%start)
dens = p*h
ddens = dp*h + p*dh
end block
end if
if (dens < 0) then
write (msg, '("negative density:", 2(x,a,"=",g0.3))') &
'ne', dens, 'ψ', psin
call log_error(msg, mod='coreprofiles', proc='density')
end if
end if
end subroutine density
function temp(psin)
! Computes the temperature as a function of the
! normalised poloidal flux.
!
! Note: temperature has units of keV.
use gray_params, only : iprof
use utils, only : locate
use simplespline, only : spli
implicit none
! subroutine arguments
real(wp_), intent(in) :: psin
real(wp_) :: temp
! local variables
integer :: k
real(wp_) :: proft, dps
temp = zero
if (psin >= 1 .or. psin < 0) return
if (iprof == 0) then
! Use the analytical model
!
! T(ψ) = (te0 - te1)⋅(1 - ψ^t1)^t2 + te1
!
proft = (1 - psin**model%t1)**model%t2
temp = (model%te0 - model%te1)*proft + model%te1
else
! Use the interpolated numerical data
call locate(spline%psi, spline%ndata, psin, k)
k = max(1, min(k, spline%ndata - 1))
dps = psin - spline%psi(k)
temp = spli(spline%temp, spline%ndata, k, dps)
endif
end function temp
function fzeff(psin)
! Computes the effective charge Zeff as a
! function of the normalised poloidal flux.
use gray_params, only : iprof
use utils, only : locate
use simplespline, only : spli
implicit none
! subroutine arguments
real(wp_), intent(in) :: psin
real(wp_) :: fzeff
! local variables
integer :: k
real(wp_) :: dps
fzeff = one
if (psin >= 1 .or. psin < 0) return
if (iprof == 0) then
! Use the analytical model (just a constant)
fzeff = model%zeff
else
! Use the interpolated numerical data
call locate(spline%psi, spline%ndata, psin, k)
k = max(1, min(k, spline%ndata - 1))
dps = psin - spline%psi(k)
fzeff = spli(spline%zeff, spline%ndata, k, dps)
endif
end function fzeff
subroutine read_profiles(filenm, data, unit)
! Reads the radial plasma profiles from `file` and store them
! into `data`. If given, the file is opened in the `unit` number.
! Format notes:
! 1. The file is formatted as a table with the following columns:
! radial coordinate, temperature, density, effective charge.
! 2. The first line is a header specifying the number of rows.
use utils, only : get_free_unit
use gray_params, only : profiles_data
use logger, only : log_error
implicit none
! subroutine arguments
character(len=*), intent(in) :: filenm
type(profiles_data), intent(out) :: data
integer, optional, intent(in) :: unit
! local variables
integer :: u, i, nrows
integer :: err
! Free the arrays when already allocated
if (allocated(data%psrad)) deallocate(data%psrad)
if (allocated(data%terad)) deallocate(data%terad)
if (allocated(data%derad)) deallocate(data%derad)
if (allocated(data%zfc)) deallocate(data%zfc)
u = get_free_unit(unit)
! Read number of rows and allocate the arrays
open(file=filenm, status='old', action='read', unit=u, iostat=err)
if (err /= 0) then
call log_error('opening profiles file ('//trim(filenm)//') failed!', &
mod='coreprofiles', proc="read_profiles")
call exit(1)
end if
read(u, *) nrows
allocate(data%psrad(nrows), data%terad(nrows), &
data%derad(nrows), data%zfc(nrows))
! Read the table rows
do i=1,nrows
read(u, *) data%psrad(i), data%terad(i), data%derad(i), data%zfc(i)
end do
close(u)
! ??
data%psrad(1) = max(data%psrad(1), zero)
end subroutine read_profiles
subroutine read_profiles_an(filenm, data, unit)
! Reads the plasma profiles `data` in the analytical format
! from params%filenm.
! If given, the file is opened in the `unit` number.
!
! The file should be formatted as follows:
!
! 1 dens0 n1 n2
! 2 te0 te1 t1 t2
! 3 zeff
!
! See `density`, `temp`, `fzeff` subroutines for the meaning
! of the parameters (i.e. the formulae for n,T,Zeff).
use gray_params, only : profiles_data
use utils, only : get_free_unit
use logger, only : log_error
implicit none
! subroutine arguments
character(len=*), intent(in) :: filenm
type(profiles_data), intent(out) :: data
integer, optional, intent(in) :: unit
! local variables
integer :: u
integer :: err
u = get_free_unit(unit)
if (allocated(data%terad)) deallocate(data%terad)
if (allocated(data%derad)) deallocate(data%derad)
if (allocated(data%zfc)) deallocate(data%zfc)
allocate(data%terad(4), data%derad(3), data%zfc(1))
open(file=filenm, status='old', action='read', unit=u, iostat=err)
if (err /= 0) then
call log_error('opening profiles file ('//trim(filenm)//') failed!', &
mod='coreprofiles', proc='read_profiles_an')
call exit(1)
end if
read (u,*) data%derad(1:3) ! dens0, n1, n2
read (u,*) data%terad(1:4) ! te0, te1, t1, t2
read (u,*) data%zfc(1) ! zeff
close(u)
end subroutine read_profiles_an
subroutine scale_profiles(params, factb, data)
! Rescales the temperature and density profiles by `params%factte`
! and `params%factne`, respectively, according to the model
! specified by `params%iscal`.
! See the GRAY user manual for the explanation.
use gray_params, only : profiles_parameters, profiles_data
implicit none
! subroutine arguments
type(profiles_parameters), intent(in) :: params
real(wp_), intent(in) :: factb
type(profiles_data), intent(inout) :: data
! local variables
real(wp_) :: aat, aan, ffact
integer :: last_te, last_ne
if (params%iscal==0) then
aat = 2.0_wp_/3.0_wp_
aan = 4.0_wp_/3.0_wp_
else
aat = one
aan = one
end if
if (params%iscal==2) then
ffact = one
else
ffact = factb
end if
if (params%iprof==0) then
last_te = 2
last_ne = 1
else
last_te = size(data%terad)
last_ne = size(data%derad)
end if
data%terad(1:last_te) = data%terad(1:last_te) * ffact**aat * params%factte
data%derad(1:last_ne) = data%derad(1:last_ne) * ffact**aan * params%factne
end subroutine scale_profiles
subroutine set_profiles_spline(params, data, launch_pos)
! Computes splines for the plasma profiles data and stores them
! in their respective global variables, see the top of this file.
!
! When `launch_pos` (cartesian launch coordinates in cm) is present,
! the subroutine will also check that the wave launcher is strictly
! outside the reconstructed plasma density boundary.
use simplespline, only : difcs
use dierckx, only : curfit, splev, splder
use gray_params, only : profiles_parameters, profiles_data
use logger, only : log_debug, log_info, log_warning, log_error
implicit none
! subroutine arguments
type(profiles_parameters), intent(inout) :: params
type(profiles_data), intent(inout) :: data
real(wp_), optional, intent(in) :: launch_pos(3)
! curfit parameters
integer, parameter :: iopt = 0 ! smoothing spline mode
integer, parameter :: kspl = 3 ! order of spline (cubic)
! local variables
integer :: n, npest, ier
real(wp_) :: xb, xe, fp, ssplne_loc
! working space arrays for the dierckx functions
integer :: lwrkf
real(wp_), dimension(:), allocatable :: wf, wrkf
integer, dimension(:), allocatable :: iwrkf
! for log messages formatting
character(256) :: msg
n = size(data%psrad)
npest = n + 4
lwrkf = n*4 + npest*16
allocate(wrkf(lwrkf), iwrkf(npest), wf(n))
ssplne_loc=params%sspld
! If necessary, reallocate the spline arrays
if (.not. allocated(spline%psi)) then
allocate(spline%psi(n), spline%temp(n, 4), spline%zeff(n, 4))
else
if (size(spline%psi) < n) then
deallocate(spline%psi, spline%temp, spline%zeff)
allocate(spline%psi(n), spline%temp(n, 4), spline%zeff(n, 4))
end if
end if
if (.not. allocated(spline%coeffs)) then
allocate(spline%knots(npest), spline%coeffs(npest))
else
if (size(spline%coeffs) < npest) then
deallocate(spline%knots, spline%coeffs)
allocate(spline%knots(npest), spline%coeffs(npest))
end if
end if
! Spline interpolation of temperature and effective charge
call difcs(data%psrad, data%terad, n, iopt, spline%temp, ier)
call difcs(data%psrad, data%zfc, n, iopt, spline%zeff, ier)
spline%psi = data%psrad
spline%ndata = n
! Spline interpolation of density
xb = zero
xe = data%psrad(n)
wf(:) = one
call curfit(iopt, n, data%psrad, data%derad, wf, xb, xe, kspl, &
ssplne_loc, npest, spline%nknots, spline%knots, &
spline%coeffs, fp, wrkf, lwrkf, iwrkf, ier)
! if ier=-1 data are re-fitted using sspl=0
if (ier == -1) then
call log_warning('curfit failed with error -1: re-fitting with '// &
's=0', mod='coreprofiles', proc='density')
ssplne_loc = zero
call curfit(iopt, n, data%psrad, data%derad, wf, xb, xe, kspl, &
ssplne_loc, npest, spline%nknots, spline%knots, &
spline%coeffs, fp, wrkf, lwrkf, iwrkf, ier)
end if
! Computation of the polynomial tail parameters
!
! Note: The density is the only quantity that needs to be evaluated
! at the edge. The spline thus has to be extended to transition
! smoothly from the last profile point to 0 outside the plasma.
block
real(wp_), dimension(1) :: s0, s1, s2 ! spline, 1st, 2nd derivative
real(wp_), dimension(1) :: delta4 ! discriminant Δ/4 of q(x)
real(wp_), dimension(1) :: x0, x1 ! vertex of q(x), solution
! Compute the coefficients of a 2nd order Taylor polinomial to
! extend the spline beyond the last point:
!
! p(ψ) = s(ψ₀) + (ψ - ψ₀)s'(ψ₀) + ½(ψ - ψ₀)²s"(ψ₀)
!
! where s(ψ) is the spline and ψ₀ the last point.
!
call splev(spline%knots, spline%nknots, spline%coeffs, kspl, &
data%psrad(n:n), s0, 1, ier)
call splder(spline%knots, spline%nknots, spline%coeffs, kspl, 1, &
data%psrad(n:n), s1, 1, wrkf(1:spline%nknots), ier)
call splder(spline%knots, spline%nknots, spline%coeffs, kspl, 2, &
data%psrad(n:n), s2, 1, wrkf(1:spline%nknots), ier)
! Determine where to end the tail (to ensure the density remains
! positive) from the zeros of the Taylor polynomial p(ψ)
!
! Define x=(ψ - ψ₀), then p(ψ)=0 is rewritten as
!
! q(x) = x² + 2s'/s" x + 2s/s" = 0
!
! The discriminant is Δ/4 = (s'/s")² - 2(s/s") and
! the solutions are x = x₀ ± √(Δ/4), with x₀ = -s'/s".
!
x0 = -s1 / s2 ! vertex of parabola y=q(x)
delta4 = (s1 / s2)**2 - 2*s0/s2 ! Δ/4 of q(x)
if (delta4(1) > 0) then
! Pick the smallest positive zero (implying >ψ₀)
x1 = x0 + sign(sqrt(delta4), sqrt(delta4) - x0)
else
! There are no zeros, use the parabola vertex
x1 = x0
call log_debug('spline extension has no zeros', &
mod='coreprofiles', proc='set_profiles_spline')
end if
! Store the tail parameters
tail%start = data%psrad(n)
tail%end = tail%start + x1(1)
tail%value = s0(1)
tail%deriv1 = s1(1)
tail%deriv2 = s2(1)
end block
! Make sure the wave launcher does not fall inside the tail
! Note: if it does, the initial wave conditions become
! invalid as they are given assuming a vacuum (N=1)
if (present(launch_pos)) then
block
use equilibrium, only : equinum_psi
real(wp_) :: R, Z, psi
real(wp_), parameter :: cm = 1.0e-2_wp_
! Convert from cartesian to cylindrical coordinates
R = hypot(launch_pos(1), launch_pos(2)) * cm ! R = √(x²+y²)
z = launch_pos(3) * cm
! Get the poloidal flux at the launcher
! Note: this returns -1 when the data is not available
call equinum_psi(R, z, psi)
if (psi > tail%start .and. psi < tail%end) then
! Fall back to the midpoint of ψ₀ and the launcher ψ
tail%end = (tail%start + psi)/2
call log_warning('downscaled tail to not reach the wave launcher', &
mod='coreprofiles', proc='set_profiles_spline')
end if
if (psi > 0 .and. psi < tail%start) then
! This must be a user error, stop here
write (msg, '(a, a, g0.3, a, g0.3)') &
'wave launcher is inside the plasma! ', &
'launcher: ψ=', psi, ' boundary: ψ=', tail%end
call log_error(msg, mod='coreprofiles', proc='set_profiles_spline')
call exit(2)
end if
end block
end if
! Set the density boundary ψ
! Note: this is used to detect entrance in the plasma
params%psnbnd = tail%end
write (msg, '(a,g0.4)') 'density boundary: ψ=', tail%end
call log_info(msg, mod='coreprofiles', proc='set_profiles_spline')
deallocate(iwrkf, wrkf, wf)
end subroutine set_profiles_spline
subroutine unset_profiles_spline
! Unsets the splines global variables, see the top of this file.
implicit none
if (allocated(spline%psi)) deallocate(spline%psi)
if (allocated(spline%temp)) deallocate(spline%temp)
if (allocated(spline%zeff)) deallocate(spline%zeff)
if (allocated(spline%knots)) deallocate(spline%knots)
if (allocated(spline%coeffs)) deallocate(spline%coeffs)
end subroutine unset_profiles_spline
subroutine set_profiles_an(params, data)
! Stores the analytical profiles data in their respective
! global variables, see the top of this file.
use gray_params, only : profiles_parameters, profiles_data
implicit none
! subroutine arguments
type(profiles_parameters), intent(inout) :: params
type(profiles_data), intent(in) :: data
model%te0 = data%terad(1)
model%te1 = data%terad(2)
model%t1 = data%terad(3)
model%t2 = data%terad(4)
model%dens0 = data%derad(1)
model%n1 = data%derad(2)
model%n2 = data%derad(3)
model%zeff = data%zfc(1)
! Define the plasma boundary to be exactly ψ=1
! Note: this is used to detect entrance in the plasma
params%psnbnd = one
end subroutine set_profiles_an
end module coreprofiles