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_eighth = 1.0_dp / 8.0_dp
34 : real(dp), parameter :: three_eighths = 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 6 : real(dp) :: GM2, Rd, Phi_units, PhiL1, PhiL2, PhiRd, omega_K
64 3 : 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 3 : if (mass_transfer_rate < eps_small) then
80 : ! no mass transfer, so no L2 mass loss
81 1 : fL2 = 0.0_dp
82 1 : return
83 : end if
84 :
85 : ! key parameters
86 2 : M2 = accretor_mass * Msun
87 2 : M1dot = mass_transfer_rate * Msun/secyer
88 2 : a = orbital_separation * Rsun
89 2 : q = accretor_mass / donor_mass ! mass ratio M2/M1
90 :
91 2 : log_q = log10(q)
92 :
93 : ! positions of Lagrangian points (based on analytic fits)
94 2 : xL1 = -0.0355_dp * log_q**2 + 0.251_dp * abs(log_q) + 0.500_dp ! [a = SMA]
95 2 : xL2 = 0.0756_dp * log_q**2 - 0.424_dp * abs(log_q) + 1.699_dp ! [a]
96 :
97 2 : if (log_q > 0.0_dp) then ! m2 is more massive
98 0 : xL1 = 1.0_dp - xL1
99 0 : xL2 = 1.0_dp - xL2
100 : end if
101 2 : mu = q / (1.0_dp + q)
102 :
103 : ! outer disk radius
104 2 : Rd_over_a = pow4(1.0_dp - xL1) / mu
105 : ! relevant potential energies
106 2 : PhiL1_dimless = -((1.0_dp - mu)/abs(xL1) + mu/abs(1.0_dp - xL1) + 0.5_dp*(xL1 - mu)**2) ! [G(M1+M2)/a]
107 2 : PhiL2_dimless = -((1.0_dp - mu)/abs(xL2) + mu/abs(1.0_dp - xL2) + 0.5_dp*(xL2 - mu)**2) ! [G(M1+M2)/a]
108 2 : PhiRd_dimless = -(1.0_dp - mu + mu/Rd_over_a + 0.5_dp*(1.0_dp - mu)**2)
109 :
110 2 : GM2 = standard_cgrav * M2
111 :
112 2 : Rd = Rd_over_a*a
113 2 : Phi_units = standard_cgrav * (M2 / mu) / a
114 2 : PhiL1 = PhiL1_dimless * Phi_units
115 2 : PhiL2 = PhiL2_dimless * Phi_units
116 2 : PhiRd = PhiRd_dimless * Phi_units
117 : ! Keplerian frequency at Rd
118 2 : omega_K = sqrt(GM2 / pow3(Rd))
119 :
120 :
121 : ! constants involved in numerical solutions
122 2 : c1 = two_thirds * pi * crad * disk_alpha * Rd / (omega_K * M1dot)
123 2 : c2 = boltzm * Rd / (GM2 * disk_mu * mp)
124 2 : c3 = 8.0_dp * pi2 * crad * disk_alpha * clight * Rd**2 / (M1dot**2 * omega_K)
125 2 : c4 = 2.0_dp * pi * disk_mu * crad * disk_alpha * omega_K * mp * pow3(Rd) / (boltzm * M1dot)
126 :
127 : ! Create logarithmically spaced grid for grid search for disk thickness [only used at the beginning]
128 102 : do i = 1, n_the
129 102 : the_grid(i) = exp10(log10(the_grid_min) + (i - 1) * (log10(the_grid_max) - log10(the_grid_min)) / (n_the - 1))
130 : end do
131 :
132 : ! only T < T_max is possible to calculate
133 2 : T_max = pow(four_twentyseventh / (c1**2 * c2), one_ninth)
134 :
135 102 : do i = 1, n_the
136 102 : T_arr(i) = 0.0_dp
137 : end do
138 :
139 102 : do i = 1, n_the
140 100 : the = the_grid(i)
141 : ! use bisection method
142 100 : T_left = 0.1_dp * min(the**2 / c2, T_max)
143 100 : f1_left = f1_the_T_fL2(the, T_left, 0.0_dp, c1, c2)
144 100 : T_right = T_max
145 2805 : do while (abs((T_left - T_right) / T_right) > tol)
146 2705 : T = 0.5_dp * (T_left + T_right)
147 2705 : f1 = f1_the_T_fL2(the, T, 0.0_dp, c1, c2)
148 2805 : if (f1 * f1_left > 0) then
149 1462 : T_left = T
150 1462 : f1_left = f1
151 : else
152 : T_right = T
153 : end if
154 : end do
155 102 : T_arr(i) = 0.5_dp * (T_left + T_right)
156 : end do
157 : ! now we have obtained numerical relation between the and T
158 102 : do i = 1, n_the
159 100 : logT_arr(i) = log10(T_arr(i))
160 102 : logthe_grid(i) = log10(the_grid(i))
161 : end do
162 2 : dlogthe = logthe_grid(2) - logthe_grid(1)
163 :
164 : ! bisection to find the numerical solution to f2(the, T, fL2=0)=0
165 2 : the_right = 1.0_dp
166 : f2_right = f2_the_T_fL2(the_right, T_the_nofL2(the_right, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
167 2 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
168 2 : separation_factor = 0.95_dp
169 2 : the_left = separation_factor * the_right
170 : f2_left = f2_the_T_fL2(the_left, T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
171 2 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
172 34 : do while (f2_left * f2_right > 0)
173 : ! need to decrease the_left
174 32 : the_right = the_left
175 32 : f2_right = f2_left
176 32 : the_left = the_left * separation_factor
177 : f2_left = f2_the_T_fL2(the_left, T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
178 34 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
179 : end do
180 : ! now the solution is between the_left and the_right
181 48 : do while (abs((the_left - the_right) / the_right) > tol)
182 46 : the = 0.5_dp * (the_left + the_right)
183 : f2 = f2_the_T_fL2(the, T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe), 0.0_dp, &
184 46 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
185 48 : if (f2 * f2_left > 0.0_dp) then
186 27 : the_left = the
187 27 : f2_left = f2
188 : else
189 19 : the_right = the
190 : end if
191 : end do
192 : ! solution
193 2 : the = 0.5_dp * (the_left + the_right)
194 2 : T = T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
195 2 : the_max = sqrt(three_eighths * c2 * T + one_fourth * (PhiL2 - PhiRd) / (GM2 / Rd) - one_eighth)
196 :
197 2 : if (the < the_max) then
198 : ! return a tiny number
199 0 : fL2 = eps_small
200 : else
201 2 : the_min = ( 0.5_dp * sqrt((PhiL2 - PhiRd) / (GM2 / Rd) - 0.5_dp) ) ! corresponding to fL2=1, T=0
202 : ! need to find the maximum corresponding to fL2=0
203 : ! this is given by the intersection between T_the(the), T_the_nofL2(the)
204 2 : the_left = the_min
205 2 : the_right = 1.0_dp
206 2 : f_left = T_the(the_left, c2, PhiL2, PhiRd, GM2, Rd) - T_the_nofL2(the_left, logthe_grid, logT_arr, n_the, dlogthe)
207 60 : do while (abs((the_left - the_right) / the_right) > tol)
208 58 : the = 0.5_dp * (the_left + the_right)
209 58 : f = T_the(the, c2, PhiL2, PhiRd, GM2, Rd) - T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
210 2 : if (f * f_left > 0.0_dp) then
211 19 : the_left = the
212 19 : f_left = f
213 : else
214 39 : the_right = the
215 : end if
216 : end do
217 2 : the_max = 0.5_dp * (the_left + the_right) ! this corresponds to fL2=0
218 :
219 : ! --- numerical solution for f2(the, T, fL2)=0 under non-zero fL2
220 :
221 : ! -- do not use exactly the_min (corresponding to T = 0, bc. kap table breaks down)
222 : ! -- define another the_min based on T_floor (kap table won't be a problem)
223 : the_min = sqrt( three_eighths * c2 * T_floor &
224 2 : + one_fourth * (PhiL2 - PhiRd) / (GM2 / Rd) - one_eighth )
225 2 : the_left = the_min
226 : f2_left = f2_the_T_fL2(the_left, &
227 : T_the(the_left, c2, PhiL2, PhiRd, GM2, Rd), &
228 : fL2_the(the_left, c1, c2, PhiL2, PhiRd, GM2, Rd), &
229 2 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
230 2 : the_right = the_max / (1.0_dp + eps_small)
231 : ! bisection again
232 44 : do while (abs((the_left - the_right) / the_right) > tol)
233 42 : the = 0.5_dp * (the_left + the_right)
234 : f2 = f2_the_T_fL2(the, T_the(the, c2, PhiL2, PhiRd, GM2, Rd), fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd), &
235 42 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
236 2 : if (f2 * f2_left > 0.0_dp) then
237 22 : the_left = the
238 22 : f2_left = f2
239 : else
240 20 : the_right = the
241 : end if
242 : end do
243 : ! solution
244 2 : the = 0.5_dp * (the_left + the_right)
245 2 : fL2 = fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd)
246 : end if
247 :
248 : end subroutine eval_L2_mass_loss_fraction
249 :
250 :
251 : ! Helper Functions
252 2805 : real(dp) function f1_the_T_fL2(the, T, fL2, c1, c2)
253 : real(dp), intent(in) :: the, T, fL2, c1, c2
254 2805 : f1_the_T_fL2 = c1 * pow4(T) * pow3(the) / (1.0_dp - fL2) - the**2 + c2 * T
255 2805 : end function f1_the_T_fL2
256 :
257 126 : real(dp) function kap(rho, T)
258 : ! simplified Kramers rule (cgs; approximate)
259 : real(dp), intent(in) :: rho, T
260 126 : kap = 0.34_dp + 3.0d24 * rho * pow(T, -3.5_dp)
261 126 : end function kap
262 :
263 126 : real(dp) function f2_the_T_fL2(the, T, fL2, &
264 : c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2)
265 : real(dp), intent(in) :: the, T, fL2
266 : real(dp), intent(in) :: c3, c4, M1dot, disk_alpha, omega_K, Rd, PhiRd, GM2, PhiL1, PhiL2
267 126 : real(dp) :: x, U_over_P, rho
268 126 : x = c4 * pow3(T * the) / (1.0_dp - fL2)
269 126 : U_over_P = (1.5_dp + x) / (1.0_dp + one_third * x)
270 126 : rho = (1.0_dp - fL2) * M1dot / (2.0_dp * pi * disk_alpha * omega_K * pow3(Rd)) / pow2(the)
271 : f2_the_T_fL2 = &
272 : seven_fourths &
273 : - (1.5_dp * U_over_P + c3 * pow4(T) / kap(rho, T) / (1.0_dp - fL2) ** 2) * the**2 &
274 : - PhiRd / (GM2 / Rd) &
275 126 : + (PhiL1 - fL2 * PhiL2) / (GM2 / Rd) / (1.0_dp - fL2)
276 126 : end function f2_the_T_fL2
277 :
278 144 : real(dp) function T_the_nofL2(the, logthe_grid, logT_arr, n_the, dlogthe)
279 : real(dp), intent(in) :: the
280 : integer, intent(in) :: n_the
281 : real(dp), intent(in) :: logthe_grid(n_the), logT_arr(n_the)
282 : real(dp), intent(in) :: dlogthe
283 144 : real(dp) :: logthe, slope, logT
284 : integer :: i_the
285 : ! under the assumption fL2=0
286 144 : logthe = log10(the)
287 144 : if (logthe > logthe_grid(n_the-1)) then
288 : ! use analytic extrapolation
289 2 : T_the_nofL2 = exp10(logT_arr(n_the-1) - 0.25_dp * (logthe - logthe_grid(n_the-1)))
290 142 : else if (logthe < logthe_grid(1)) then
291 : ! analytic extrapolation
292 0 : T_the_nofL2 = exp10(logT_arr(1) + 2.0_dp * (logthe - logthe_grid(1)))
293 : else
294 142 : i_the = floor((logthe - logthe_grid(1)) / dlogthe) + 1
295 142 : slope = (logT_arr(i_the + 1) - logT_arr(i_the)) / dlogthe
296 142 : logT = logT_arr(i_the) + (logthe - logthe_grid(i_the)) * slope
297 142 : T_the_nofL2 = exp10(logT)
298 : end if
299 144 : end function T_the_nofL2
300 :
301 150 : real(dp) function T_the(the, c2, PhiL2, PhiRd, GM2, Rd)
302 : real(dp), intent(in) :: the
303 : real(dp), intent(in) :: c2, PhiL2, PhiRd, GM2, Rd
304 : ! only for non-zero fL2
305 104 : T_the = (8.0_dp * the**2 + 1.0_dp - 2.0_dp * (PhiL2 - PhiRd) / (GM2 / Rd)) / (3.0_dp * c2)
306 0 : end function T_the
307 :
308 46 : real(dp) function fL2_the(the, c1, c2, PhiL2, PhiRd, GM2, Rd)
309 : real(dp), intent(in) :: the
310 : real(dp), intent(in) :: c1, c2, PhiL2, PhiRd, GM2, Rd
311 46 : real(dp) :: T
312 : ! only for non-zero fL2
313 46 : T = T_the(the, c2, PhiL2, PhiRd, GM2, Rd)
314 46 : fL2_the = 1.0_dp - c1 * pow4(T) * pow3(the) / (the**2 - c2 * T)
315 46 : end function fL2_the
316 :
317 :
318 : end module binary_disk
|