LCOV - code coverage report
Current view: top level - binary/private - binary_disk.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.5 % 141 136
Test Date: 2025-10-14 06:41:40 Functions: 85.7 % 7 6

            Line data    Source code
       1              : ! ***********************************************************************
       2              : !
       3              : !   Copyright (C) 2025  Philip Mocz, Mathieu Renzo & The MESA Team
       4              : !
       5              : !   This program is free software: you can redistribute it and/or modify
       6              : !   it under the terms of the GNU Lesser General Public License
       7              : !   as published by the Free Software Foundation,
       8              : !   either version 3 of the License, or (at your option) any later version.
       9              : !
      10              : !   This program is distributed in the hope that it will be useful,
      11              : !   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              : !   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
      13              : !   See the GNU Lesser General Public License for more details.
      14              : !
      15              : !   You should have received a copy of the GNU Lesser General Public License
      16              : !   along with this program. If not, see <https://www.gnu.org/licenses/>.
      17              : !
      18              : ! ***********************************************************************
      19              : 
      20              : module binary_disk
      21              : 
      22              :    use const_def, only: dp, pi, pi2, one_third, two_thirds, standard_cgrav, Msun, Rsun, secyer, crad, boltzm, clight, mp
      23              :    use star_lib
      24              :    use star_def
      25              :    use math_lib
      26              :    use binary_def
      27              : 
      28              :    implicit none
      29              : 
      30              :    ! more numerical constants
      31              :    real(dp), parameter :: one_fourth = 1.0_dp / 4.0_dp
      32              :    real(dp), parameter :: seven_fourths = 7.0_dp / 4.0_dp
      33              :    real(dp), parameter :: one_eigth = 1.0_dp / 8.0_dp
      34              :    real(dp), parameter :: three_eigths = 3.0_dp / 8.0_dp
      35              :    real(dp), parameter :: one_ninth = 1.0_dp / 9.0_dp
      36              :    real(dp), parameter :: four_twentyseventh = 4.0_dp / 27.0_dp
      37              : 
      38              :    ! This module includes functions for mass transfer and L2 mass loss for binary systems with a thin disk
      39              :    ! TODO: switch Kramers opacity module to real one in eval_L2_mass_loss_fraction()
      40              : 
      41              : contains
      42              : 
      43            3 :    subroutine eval_L2_mass_loss_fraction(donor_mass, accretor_mass, mass_transfer_rate, orbital_separation, &
      44              :                                          disk_alpha, disk_mu, &
      45              :                                          fL2, ierr)
      46              :       ! Calculate the (outer) L2 mass-loss fraction
      47              :       ! according to Lu et al. (2023, MNRAS 519, 1409) "On rapid binary mass transfer -I. Physical model"
      48              :       ! https://ui.adsabs.harvard.edu/abs/2023MNRAS.519.1409L/abstract
      49              :       real(dp), intent(in) :: donor_mass         ! [M_sun]
      50              :       real(dp), intent(in) :: accretor_mass      ! [M_sun]
      51              :       real(dp), intent(in) :: mass_transfer_rate ! [M_sun/yr]
      52              :       real(dp), intent(in) :: orbital_separation ! [R_sun]
      53              :       real(dp), intent(in) :: disk_alpha         ! disk alpha viscosity parameter (dimensionless)
      54              :       real(dp), intent(in) :: disk_mu            ! disk mean molecular weight (dimensionless)
      55              :       real(dp), intent(out) :: fL2               ! L2 mass-loss fraction (dimensionless)
      56              :       integer, intent(out) :: ierr
      57              : 
      58              :       real(dp), parameter :: eps_small = 1d-12  ! a very small number
      59              :       real(dp), parameter :: tol = 1d-8  ! fractional tolerance for bisection method
      60            6 :       real(dp) :: M2, M1dot, a, q, log_q
      61            6 :       real(dp) :: xL1, xL2, mu
      62            3 :       real(dp) :: Rd_over_a, PhiL1_dimless, PhiL2_dimless, PhiRd_dimless
      63            3 :       real(dp) :: GM2, Rd, Phi_units, PhiL1, PhiL2, PhiRd, omega_K
      64              :       real(dp) :: c1, c2, c3, c4
      65              : 
      66              :       integer :: i
      67              :       integer, parameter :: n_the = 50  ! number of grid points for disk thickness search
      68              :       real(dp), parameter :: the_grid_min = 0.1_dp
      69              :       real(dp), parameter :: the_grid_max = 1.0_dp
      70          153 :       real(dp) :: the_grid(n_the)  ! grid for disk thickness
      71          153 :       real(dp) :: T_arr(n_the)
      72              :       real(dp), parameter :: T_floor = 3.0d3  ! [K] the minimum value for disk temperature solution
      73            6 :       real(dp) :: T, T_max, the, the_left, the_right, the_min, the_max, separation_factor, dlogthe
      74            3 :       real(dp) :: T_left, T_right, f1, f1_left, f2, f2_left, f2_right, f, f_left
      75          303 :       real(dp) :: logT_arr(n_the), logthe_grid(n_the)
      76              : 
      77            3 :       ierr = 0
      78              : 
      79              :       ! key parameters
      80            3 :       M2 = accretor_mass * Msun
      81            3 :       M1dot = mass_transfer_rate * Msun/secyer
      82            3 :       a = orbital_separation * Rsun
      83            3 :       q = accretor_mass / donor_mass  ! mass ratio M2/M1
      84              : 
      85            3 :       log_q = log10(q)
      86              : 
      87              :       ! positions of Lagrangian points (based on analytic fits)
      88            3 :       xL1 = -0.0355_dp * log_q**2 + 0.251_dp * abs(log_q) + 0.500_dp  ! [a = SMA]
      89            3 :       xL2 = 0.0756_dp * log_q**2 - 0.424_dp * abs(log_q) + 1.699_dp  ! [a]
      90              : 
      91            3 :       if (log_q > 0.0_dp) then  ! m2 is more massive
      92            0 :          xL1 = 1.0_dp - xL1
      93            0 :          xL2 = 1.0_dp - xL2
      94              :       end if
      95            3 :       mu = q / (1.0_dp + q)
      96              : 
      97              :       ! outer disk radius
      98            3 :       Rd_over_a = pow4(1.0_dp - xL1) / mu
      99              :       ! relavent potential energies
     100            3 :       PhiL1_dimless = -((1.0_dp - mu)/abs(xL1) + mu/abs(1.0_dp - xL1) + 0.5_dp*(xL1 - mu)**2)  ! [G(M1+M2)/a]
     101            3 :       PhiL2_dimless = -((1.0_dp - mu)/abs(xL2) + mu/abs(1.0_dp - xL2) + 0.5_dp*(xL2 - mu)**2)  ! [G(M1+M2)/a]
     102            3 :       PhiRd_dimless = -(1.0_dp - mu + mu/Rd_over_a + 0.5_dp*(1.0_dp - mu)**2)
     103              : 
     104            3 :       GM2 = standard_cgrav * M2
     105              : 
     106            3 :       Rd = Rd_over_a*a
     107            3 :       Phi_units = standard_cgrav * (M2 / mu) / a
     108            3 :       PhiL1 = PhiL1_dimless * Phi_units
     109            3 :       PhiL2 = PhiL2_dimless * Phi_units
     110            3 :       PhiRd = PhiRd_dimless * Phi_units
     111              :       ! Keplerian frequency at Rd
     112            3 :       omega_K = sqrt(GM2 / pow3(Rd))
     113              : 
     114              : 
     115              :       ! constants involved in numerical solutions
     116            3 :       c1 = two_thirds * pi * crad * disk_alpha * Rd / (omega_K * M1dot)
     117            3 :       c2 = boltzm * Rd / (GM2 * disk_mu * mp)
     118            3 :       c3 = 8.0_dp * pi2 * crad * disk_alpha * clight * Rd**2 / (M1dot**2 * omega_K)
     119            3 :       c4 = 2.0_dp * pi * disk_mu * crad * disk_alpha * omega_K * mp * pow3(Rd) / (boltzm * M1dot)
     120              : 
     121              :       ! Create logarithmically spaced grid for grid search for disk thickness [only used at the beginning]
     122          153 :       do i = 1, n_the
     123          153 :          the_grid(i) = exp10(log10(the_grid_min) + (i - 1) * (log10(the_grid_max) - log10(the_grid_min)) / (n_the - 1))
     124              :       end do
     125              : 
     126              :       ! only T < T_max is possible to calculate
     127            3 :       T_max = pow(four_twentyseventh / (c1**2 * c2), one_ninth)
     128              : 
     129          153 :       do i = 1, n_the
     130          153 :          T_arr(i) = 0.0_dp
     131              :       end do
     132              : 
     133          153 :       do i = 1, n_the
     134          150 :          the = the_grid(i)
     135              :          ! use bisection method
     136          150 :          T_left = 0.1_dp * min(the**2 / c2, T_max)
     137          150 :          f1_left = f1_the_T_fL2(the, T_left, 0.0_dp, c1, c2)
     138          150 :          T_right = T_max
     139         2855 :          do while (abs((T_left - T_right) / T_right) > tol)
     140         2705 :             T = 0.5_dp * (T_left + T_right)
     141         2705 :             f1 = f1_the_T_fL2(the, T, 0.0_dp, c1, c2)
     142         2855 :             if (f1 * f1_left > 0) then
     143         1462 :                T_left = T
     144         1462 :                f1_left = f1
     145              :             else
     146              :                T_right = T
     147              :             end if
     148              :          end do
     149          153 :          T_arr(i) = 0.5_dp * (T_left + T_right)
     150              :       end do
     151              :       ! now we have obtained numerical relation between the and T
     152          153 :       do i = 1, n_the
     153          150 :          logT_arr(i) = log10(T_arr(i))
     154          153 :          logthe_grid(i) = log10(the_grid(i))
     155              :       end do
     156            3 :       dlogthe = logthe_grid(2) - logthe_grid(1)
     157              : 
     158              :       ! bisection to find the numerical solution to f2(the, T, fL2=0)=0
     159            3 :       the_right = 1.0_dp
     160              :       f2_right = f2_the_T_fL2(the_right, T_the_nofL2(the_right, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
     161            3 :                               c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     162            3 :       separation_factor = 0.95_dp
     163            3 :       the_left = separation_factor * the_right
     164              :       f2_left = f2_the_T_fL2(the_left, T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
     165            3 :                              c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     166           35 :       do while (f2_left * f2_right > 0)
     167              :          ! need to decrease the_left
     168           32 :          the_right = the_left
     169           32 :          f2_right = f2_left
     170           32 :          the_left = the_left * separation_factor
     171              :          f2_left = f2_the_T_fL2(the_left, T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
     172           35 :                                 c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     173              :       end do
     174              :       ! now the solution is between the_left and the_right
     175           72 :       do while (abs((the_left - the_right) / the_right) > tol)
     176           69 :          the = 0.5_dp * (the_left + the_right)
     177              :          f2 = f2_the_T_fL2(the, T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
     178           69 :                            c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     179           72 :          if (f2 * f2_left > 0.0_dp) then
     180           27 :             the_left = the
     181           27 :             f2_left = f2
     182              :          else
     183           42 :             the_right = the
     184              :          end if
     185              :        end do
     186              :       ! solution
     187            3 :       the = 0.5_dp * (the_left + the_right)
     188            3 :       T = T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
     189            3 :       the_max = sqrt(three_eigths * c2 * T + one_fourth * (PhiL2 - PhiRd) / (GM2 / Rd) - one_eigth)
     190              : 
     191            3 :       if (the < the_max) then
     192              :          ! return a tiny numner
     193            0 :          fL2 = eps_small
     194            3 :       else if (mass_transfer_rate < eps_small) then
     195              :          ! no mass transfer, so no L2 mass loss
     196            1 :          fL2 = 0.0_dp
     197              :       else
     198            2 :          the_min = ( 0.5_dp * sqrt((PhiL2 - PhiRd) / (GM2 / Rd) - 0.5_dp) )  ! corresponding to fL2=1, T=0
     199              :          ! need to find the maximum corresponding to fL2=0
     200              :          ! this is given by the intersection between T_the(the), T_the_nofL2(the)
     201            2 :          the_left = the_min
     202            2 :          the_right = 1.0_dp
     203            2 :          f_left = T_the(the_left, c2, PhiL2, PhiRd, GM2, Rd) - T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe)
     204           60 :          do while (abs((the_left - the_right) / the_right) > tol)
     205           58 :             the = 0.5_dp * (the_left + the_right)
     206           58 :             f = T_the(the, c2, PhiL2, PhiRd, GM2, Rd) - T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
     207            2 :             if (f * f_left > 0.0_dp) then
     208           19 :                the_left = the
     209           19 :                f_left = f
     210              :             else
     211           39 :                the_right = the
     212              :             end if
     213              :          end do
     214            2 :          the_max = 0.5_dp * (the_left + the_right)  ! this corresponds to fL2=0
     215              : 
     216              :          ! --- numerical solution for f2(the, T, fL2)=0 under non-zero fL2
     217              : 
     218              :          ! -- do not use exactly the_min (corresponding to T = 0, bc. kap table breaks down)
     219              :          ! -- define another the_min based on T_floor (kap table won't be a problem)
     220              :          the_min = sqrt( three_eigths * c2 * T_floor &
     221            2 :                        + one_fourth * (PhiL2 - PhiRd) / (GM2 / Rd) - one_eigth )
     222            2 :          the_left = the_min
     223              :          f2_left = f2_the_T_fL2(the_left, &
     224              :                                 T_the(the_left, c2, PhiL2, PhiRd, GM2, Rd), &
     225              :                                 fL2_the(the_left, c1, c2, PhiL2, PhiRd, GM2, Rd), &
     226            2 :                                 c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     227            2 :          the_right = the_max / (1.0_dp + eps_small)
     228              :          ! bisection again
     229           44 :          do while (abs((the_left - the_right) / the_right) > tol)
     230           42 :             the = 0.5_dp * (the_left + the_right)
     231              :             f2 = f2_the_T_fL2(the, T_the(the, c2, PhiL2, PhiRd, GM2, Rd), fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd), &
     232           42 :                               c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     233            2 :             if (f2 * f2_left > 0.0_dp) then
     234           22 :                the_left = the
     235           22 :                f2_left = f2
     236              :             else
     237           20 :                the_right = the
     238              :             end if
     239              :          end do
     240              :          ! solution
     241            2 :          the = 0.5_dp * (the_left + the_right)
     242            2 :          fL2 = fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd)
     243              :       end if
     244              : 
     245            3 :    end subroutine eval_L2_mass_loss_fraction
     246              : 
     247              : 
     248              :    ! Helper Functions
     249         2855 :    real(dp) function f1_the_T_fL2(the, T, fL2, c1, c2)
     250              :       real(dp), intent(in) :: the, T, fL2, c1, c2
     251         2855 :       f1_the_T_fL2 = c1 * pow4(T) * pow3(the) / (1.0_dp - fL2) - the**2 + c2 * T
     252         2855 :    end function f1_the_T_fL2
     253              : 
     254          151 :    real(dp) function  kap(rho, T)
     255              :       ! simplified Kramers rule (cgs; approximate)
     256              :       real(dp), intent(in) :: rho, T
     257          151 :       kap = 0.34_dp + 3.0d24 * rho * pow(T, -3.5_dp)
     258          151 :    end function kap
     259              : 
     260          151 :    real(dp) function f2_the_T_fL2(the, T, fL2, &
     261              :                                   c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
     262              :       real(dp), intent(in) :: the, T, fL2
     263              :       real(dp), intent(in) :: c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2
     264          151 :       real(dp) :: x, U_over_P, rho
     265          151 :       x = c4 * pow3(T * the) / (1.0_dp - fL2)
     266          151 :       U_over_P = (1.5_dp + x) / (1.0_dp + one_third * x)
     267          151 :       rho = (1.0_dp - fL2) * M1dot / (2.0_dp * pi * disk_alpha * omega_K * pow3(Rd)) / pow2(the)
     268              :       f2_the_T_fL2 = &
     269              :          seven_fourths &
     270              :          - (1.5_dp * U_over_P + c3 * pow4(T) / kap(rho, T) / (1.0_dp - fL2) ** 2) * the**2 &
     271              :          - PhiRd / (GM2 / Rd) &
     272          151 :          + (PhiL1 - fL2 * PhiL2) / (GM2 / Rd) / (1.0_dp - fL2)
     273          151 :    end function f2_the_T_fL2
     274              : 
     275          170 :    real(dp) function T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
     276              :       real(dp), intent(in) :: the
     277              :       integer, intent(in) :: n_the
     278              :       real(dp), intent(in) :: logthe_grid(n_the), logT_arr(n_the)
     279              :       real(dp), intent(in) :: dlogthe
     280          170 :       real(dp) :: logthe, slope, logT
     281              :       integer :: i_the
     282              :       ! under the assumption fL2=0
     283          170 :       logthe = log10(the)
     284          170 :       if (logthe > logthe_grid(n_the-1)) then
     285              :          ! use analytic extrapolation
     286            6 :          T_the_nofL2 = exp10(logT_arr(n_the-1) - 0.25_dp * (logthe - logthe_grid(n_the-1)))
     287          164 :       else if (logthe < logthe_grid(1)) then
     288              :          ! analytic extrapolation
     289            0 :          T_the_nofL2 = exp10(logT_arr(1) + 2.0_dp * (logthe - logthe_grid(1)))
     290              :       else
     291          164 :          i_the = floor((logthe - logthe_grid(1)) / dlogthe) + 1
     292          164 :          slope = (logT_arr(i_the + 1) - logT_arr(i_the)) / dlogthe
     293          164 :          logT = logT_arr(i_the) + (logthe - logthe_grid(i_the)) * slope
     294          164 :          T_the_nofL2 = exp10(logT)
     295              :       end if
     296          170 :    end function T_the_nofL2
     297              : 
     298          150 :    real(dp) function T_the(the, c2, PhiL2, PhiRd, GM2, Rd)
     299              :       real(dp), intent(in) :: the
     300              :       real(dp), intent(in) :: c2, PhiL2, PhiRd, GM2, Rd
     301              :       ! only for non-zero fL2
     302          104 :       T_the = (8.0_dp * the**2 + 1.0_dp - 2.0_dp * (PhiL2 - PhiRd) / (GM2 / Rd)) / (3.0_dp * c2)
     303            0 :    end function T_the
     304              : 
     305           46 :    real(dp) function fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd)
     306              :       real(dp), intent(in) :: the
     307              :       real(dp), intent(in) :: c1, c2, PhiL2, PhiRd, GM2, Rd
     308           46 :       real(dp) :: T
     309              :       ! only for non-zero fL2
     310           46 :       T = T_the(the, c2, PhiL2, PhiRd, GM2, Rd)
     311           46 :       fL2_the =  1.0_dp - c1 * pow4(T) * pow3(the) / (the**2 - c2 * T)
     312           46 :    end function fL2_the
     313              : 
     314              : 
     315              : end module binary_disk
        

Generated by: LCOV version 2.0-1