Example of auto_diff in run_star_extras#
The auto_diff
module simplifies using run_star_extras
hooks that
need to provide partial derivatives.
For example, the other_surface_PT
hook can be used to specify an alternate
surface boundary condition by providing the surface temperature and pressure.
This hook has the following call signature:
other_surface_PT(id, &
skip_partials, &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap, ierr)
In addition to lnT_surf
and lnP_surf
, we also need to provide four partial derivatives!
We could specify these manually.
For instance an Eddington atmosphere might have:
subroutine other_surface_PT(id, &
skip_partials, &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap, ierr)
use const_def, only: dp
integer, intent(in) :: id
logical, intent(in) :: skip_partials
real(dp), intent(out) :: &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap
integer, intent(out) :: ierr
real(dp) :: g
lnT_surf = log(pow(s%L(1) / (4d0 * pi * boltzsig * pow2(s%R(1))), 0.25d0))
dlnT_dL = lnT_surf / s%L(1)
dlnT_dlnR = -0.5d0
dlnT_dlnM = 0d0
dlnT_dlnkap = 0d0
g = s%M(1) / pow2(s%R(1))
lnP_surf = log(g / s%opacity(1))
dlnP_dL = 0d0
dlnP_dlnR = -2d0
dlnP_dlnM = 1d0
dlnP_dlnkap = -1d0
end subroutine other_surface_PT
This relies on us calculating the derivatives correctly by hand and hard-coding them.
For this simple case that isn’t too hard, but it is easy to imagine more complicated cases where doing it by hand is error-prone and tedious.
Instead, we could use auto_diff
:
subroutine other_surface_PT(id, &
skip_partials, &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap, ierr)
use const_def, only: dp
use auto_diff
integer, intent(in) :: id
logical, intent(in) :: skip_partials
real(dp), intent(out) :: &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap
integer, intent(out) :: ierr
type(auto_diff_real_4var_order1) :: L, lnR, lnM, lnkap, M, R, kap, g, lnT, lnP
real(dp) :: g
! Set up auto_diff base variables.
! Note that the ordering of which variable goes in which d1val slot doesn't matter.
! We just need to use the same ordering when reading the results out.
L = s%L(1)
L%d1val1 = 1d0
lnR = log(s%R(1))
lnR%d1val2 = 1d0
lnM = log(s%M(1))
lnM%d1val3 = 1d0
lnkap = log(s%opacity(1))
lnkap%d1val4 = 1d0
! Calculate lnT and lnP
M = exp(lnM)
R = exp(lnR)
kap = exp(lnkap)
lnT = log(pow(L / (4d0 * pi * boltzsig * pow2(R))))
g = M / pow2(R)
lnP = log(g / kap)
! Read results.
! Note that the order matches the order we used to set up the base variables.
lnT_surf = lnT%val
dlnT_dL = lnT%d1val1
dlnT_dlnR = lnT%d1val2
dlnT_dlnM = lnT%d1val3
dlnT_dlnkap = lnT%d1val4
lnP_surf = lnP%val
dlnP_dL = lnP%d1val1
dlnP_dlnR = lnP%d1val2
dlnP_dlnM = lnP%d1val3
dlnP_dlnkap = lnP%d1val4
end subroutine other_surface_PT
Note that auto_diff
does not support the operation real variable = auto_diff variable
.
If you attempt to set a real
equal to an auto_diff
variable you will get an error that this operation is not implemented.
That’s why we read the value of lnT
and lnP
above using lnT%val
and lnP%val
.
The reason this operation is not supported is that real
variables cannot store derivative information, so information would be lost in that conversion.
If this were done as an intermediate operation in a calculation (rather than during read out) the resulting derivatives that get read out at the end would be incorrect.
It is easy to make this conversion by mistake, and there is no valid use case for this operation other than at read out, so we choose to throw a compiler error rather than allow it.
Another common failure mode is not including use auto_diff
at the start of the hook.
That will result in a compiler error saying that the relevant auto_diff
types are not defined.
The above example with auto_diff
looks more complicated than assigning derivatives by hand, but that’s only because we used a simple example.
For instance the atmosphere model of Jermyn+2021 could be implemented as:
subroutine other_surface_PT(id, skip_partials, &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap, ierr)
use auto_diff
integer, intent(in) :: id
logical, intent(in) :: skip_partials
real(dp), intent(out) :: &
lnT_surf, dlnT_dL, dlnT_dlnR, dlnT_dlnM, dlnT_dlnkap, &
lnP_surf, dlnP_dL, dlnP_dlnR, dlnP_dlnM, dlnP_dlnkap
integer, intent(out) :: ierr
type (star_info), pointer :: s
real(dp) :: minT, blend_minT, Ledd_rdp
type(auto_diff_real_4var_order1) :: M, R, L, kappa
type(auto_diff_real_4var_order1) :: R_ph, R_bondi, R_acc, vesc2, Mdot, loss, super_eddington_factor, L_incident
type(auto_diff_real_4var_order1) :: H_disk, erf_arg, erf_f1, erf_f2, vortpar, R_AGN, R_Hill, tidepar
type(auto_diff_real_4var_order1) :: L_stream, L_shock, Ledd, T_surf, P_surf, P_rad, P_acc, P_outflow, P_simple
type(auto_diff_real_4var_order1) :: agn_Teff, simple_Teff, simple_lnT, simple_lnP, agn_lnT, agn_lnP
type(auto_diff_real_4var_order1) :: lnT, lnP
! Basic setup
ierr = 0
call star_ptr(id, s, ierr)
if (ierr /= 0) return
! Setup autodiff variables
M = s%m(1)
M%d1val1 = 1d0
R = s%r(1)
R%d1val2 = 1d0
L = s%L(1)
L%d1val3 = 1d0
kappa = const_kappa
kappa%d1val4 = 0d0
!kappa = s%opacity(1)
!kappa%d1val4 = 1d0
! Basic quantities
R_bondi = 2d0*standard_cgrav * M / pow2(const_csb) ! Bondi radius
R_acc = R_bondi
Mdot = pi * pow2(R_bondi) * const_rho * const_csb ! Mdot gain
R_ph = pow2(kappa * const_rho) * pow3(R_bondi) ! Photosphere radius
R_ph = R_ph + R
R_ph = min(R_ph, R_bondi)
! Adjust gain down due to radiation pressure
!Ledd_rdp = eval_Ledd(s, ierr)
!Ledd = (Ledd_rdp / M%val) * M ! Gives Ledd the correct derivatives, because Ledd_rdp scales like M.
Ledd = 4.0d0*pi*clight*s%cgrav(1)*M/kappa
if (s% x_logical_ctrl(7)) then
Ledd = Ledd * max(1e-2, (1d0 - pow2(s%job%extras_rpar(i_mu_J) / s%job%extras_rpar(i_J_crit))))
end if
if (s% x_logical_ctrl(1)) then
if (L < Ledd) then
Mdot = Mdot * (1d0 - L/Ledd)**2d0 ! Suggested by Alexander Dittmann
else
Mdot = 0d0
end if
else
Mdot = Mdot * (1d0 - tanh(abs(L / Ledd)))
end if
! Adjust gain down due to gap opening in the disk
if (s% x_logical_ctrl(2)) then
Mdot = Mdot * exp(-pow(M / Msun / cutoff_mass, 2d0/3d0)) ! Form suggested by Doug Lin
end if
if (s% x_logical_ctrl(3)) then
H_disk = pow(2d0, 0.5d0) * const_csb / Omega_AGN
erf_arg = -R_bondi / H_disk
erf_f1 = (-2d0/pow(pi, 0.5d0) )*pow( 1d0 - exp(-erf_arg*erf_arg), 0.5d0)
erf_f2 = pow(pi, 0.5d0)*0.5d0 + (3.1d1/2d2)*exp(-erf_arg*erf_arg) - (3.41d2/8d3)*exp(-2d0*erf_arg*erf_arg)
Mdot = 0.5d0 * Mdot * pow( pi, 0.5d0 ) * erf_f1 * erf_f2 / erf_arg
end if
if (s% x_logical_ctrl(4)) then
R_AGN = pow( standard_cgrav * Mass_AGN * Msun / (Omega_AGN * Omega_AGN) , 1d0/3d0 )
vortpar = (R_bondi * R_bondi * Omega_AGN)/(2d0 * const_csb * R_AGN) !avg vorticity parameter at R_bondi
vortpar = (2d0 / (pi * vortpar))*asinh( pow(2d0*vortpar, 1d0/3d0)) !Krumholz, McKee, Klein 2005 eqn. A7
Mdot = Mdot * pow(1d0 + pow(vortpar, -10d0), -0.1d0) !pick minimum factor
end if
if (s% x_logical_ctrl(5)) then
R_Hill = pow(standard_cgrav * s%m(1) / (3d0 * Omega_AGN*Omega_AGN), 1d0/3d0)
tidepar = pow(R_Hill / R_bondi, 2d0)
Mdot = Mdot * min(1d0, tidepar) !pick minimum factor
R_acc = min(R_Hill, R_Bondi)
! Use min(Bondi, Hill) for accretion rate. Similar to Rosenthal et al. 2020
end if
if (s% x_logical_ctrl(6)) then
H_disk = pow(2d0, 0.5d0) * const_csb / Omega_AGN
R_Hill = pow(standard_cgrav * s%m(1) / (3d0 * Omega_AGN*Omega_AGN), 1d0/3d0)
R_acc = min(R_Hill, R_Bondi)
tidepar = min(1d0, R_Hill/R_bondi)
tidepar = min(tidepar, H_disk/R_bondi)
tidepar = tidepar * min(1d0, R_Hill/R_bondi)
tidepar = tidepar * min(1d0, exp(2d0*(1d0-pow(R_Hill/H_disk, 2d0))))
!Dobbs-Dixon, Li, Lin 2007, approx eqn. 28
Mdot = Mdot * tidepar
! Adjust accretion rate to take into account the tidal barrier
end if
! Calculate super-eddington mass loss
vesc2 = 2d0*standard_cgrav*M / R
super_eddington_factor = 0.1d0
loss = super_eddington_mdot_autodiff(Ledd,M,L,R,super_eddington_factor)
! Set a minimum on Teff.
minT = const_Tb
! Various luminosities
L_shock = Mdot * M * standard_cgrav / R
L_stream = L + L_shock
L_incident = 4 * pi * pow2(R_ph) * boltz_sigma * pow4(minT)
L_stream = L_stream + L_incident
L_stream = max(L_stream, L_incident) ! Avoid NaN's due to bad near-surface grad(T)
! Evaluate simple photosphere
simple_Teff = pow(L_stream / (4d0 * pi * R**2 * boltz_sigma), 0.25d0)
simple_lnP = log(standard_cgrav * M / (R**2 * kappa))
P_simple = exp(simple_lnP)
simple_lnT = log(simple_Teff) ! First cell is tau=2/3 in this simple solution.
! Evaluate complicated photosphere
agn_Teff = pow(L_stream / (4 * pi * boltz_sigma * pow2(R_ph)), 0.25d0)
T_surf = agn_Teff * pow(R_ph / R, 5d0 / 8d0)
agn_lnT = log(T_surf)
! P_surf
P_acc = const_rho * pow2(const_csb) * pow(R_acc / R, 2.5d0)
P_rad = crad * pow4(T_surf) / 3d0
P_outflow = - loss * pow(vesc2, 0.5d0) / (4 * pi * pow2(R))
P_surf = P_acc + P_rad + P_outflow + P_simple
agn_lnP = log(P_surf)
! blend gets set in extras_start_step
lnT = agn_lnT
lnP = agn_lnP
! Format for output
lnT_surf = lnT%val
dlnT_dlnM = lnT%d1val1 * M%val
dlnT_dlnR = lnT%d1val2 * R%val
dlnT_dL = lnT%d1val3
dlnT_dlnkap = lnT%d1val4 * kappa%val
lnP_surf = lnP%val
dlnP_dlnM = lnP%d1val1 * M%val
dlnP_dlnR = lnP%d1val2 * R%val
dlnP_dL = lnP%d1val3
dlnP_dlnkap = lnP%d1val4 * kappa%val
end subroutine other_surface_PT
Propagating the derivatives through this by hand would be tedious and error-prone.
With auto_diff
the focus stays on the physics rather than differentiation.