MPI-AMRVAC 3.2
The MPI - Adaptive Mesh Refinement - Versatile Advection Code (development version)
Loading...
Searching...
No Matches
mod_radiative_cooling.t
Go to the documentation of this file.
1!> module radiative cooling -- add optically thin radiative cooling
2!>
3!> only uses the (Townsend) exact integration method, can be used in HD, ffhd, MHD, twofl
4!>
5!> Assumptions: full ionized plasma dominated by H and He, ionization equilibrium
6!> Formula: Q=-n_H*n_e*f(T), positive f(T) function is pre-computed and tabulated or a piecewise power law
7!> Uses the various cooling tables stored in mod_radloss_tables.t
8!>
10
11 use mod_global_parameters, only: std_len
12 use mod_physics
14 use mod_comm_lib, only: mpistop
16 implicit none
17
18 !> Per-rank cooling-only compute accumulator for lb_diagnose. Sums the
19 !> wall time spent inside radiative_cooling_add_source across all blocks
20 !> and substages within one full advance() call. Reset by
21 !> mod_advance::advance at the start of each step. Note: this is a subset
22 !> of lb_compute_accum (which already encloses the whole advect1 block
23 !> loop); it isolates the cooling Newton/exact-solver kernel cost from
24 !> the surrounding finite-volume work.
25 double precision, public :: lb_cool_accum = 0.0d0
26
27 !> Helium abundance over Hydrogen
28 double precision, private :: he_abundance
29
30 !> The adiabatic index
31 double precision, private :: rc_gamma
32
33 !> The adiabatic index minus 1
34 double precision, private :: rc_gamma_1
35
36 !> inverse of the adiabatic index minus 1
37 double precision, private :: invgam
38
39 !> Voigt escape probability lookup table.
40 !> E(tau) is the frequency-integrated single-flight escape probability
41 !> from a uniform slab with a Voigt line profile truncated at x_max
42 !> Doppler widths (CL12 Sec 2.1, mimics PRD):
43 !> E(tau_0) = [phi(0)/tau_0] * integral_{-xmax}^{xmax} [1 - exp(-tau_0 phi(x)/phi(0))] dx
44 !> Precomputed at initialisation via Gauss-Legendre quadrature, then
45 !> interpolated at runtime from a table in log10(tau).
46 !> Reference Voigt parameter: a_ref = 4.7e-4 at T = 10 kK.
47 !> Sensitivity to a is < 20% for tau < 1e6 (see voigt_escape_derivation.py).
48 integer, parameter, private :: n_voigt_table = 500
49 double precision, parameter, private :: voigt_logtau_min = -2.0d0 ! tau = 0.01
50 double precision, parameter, private :: voigt_logtau_max = 8.0d0 ! tau = 1e8
51 double precision, parameter, private :: voigt_a_ref = 4.7d-4
52 double precision, parameter, private :: voigt_xmax = 6.0d0
53 !> Temperature in K above which c_V is constant, the upper end of the Y_mod path
54 double precision, parameter :: rc_t_ymod_max_k = 2.0d5
55 double precision, private :: voigt_e_table(n_voigt_table)
56 double precision, private :: voigt_logtau_step
57 logical, private :: voigt_table_ready = .false.
58
59 abstract interface
60 subroutine get_subr1(w,x,ixI^L,ixO^L,res)
62 integer, intent(in) :: ixI^L, ixO^L
63 double precision, intent(in) :: w(ixI^S,nw)
64 double precision, intent(in) :: x(ixI^S,1:ndim)
65 double precision, intent(out):: res(ixI^S)
66 end subroutine get_subr1
67
68 subroutine get_2var_subr(ixI^L, ixO^L, w, x, ne, nH)
70 integer, intent(in) :: ixI^L, ixO^L
71 double precision, intent(in) :: w(ixI^S, nw)
72 double precision, intent(in) :: x(ixI^S, 1:ndim)
73 double precision, intent(out):: ne(ixI^S), nH(ixI^S)
74 end subroutine get_2var_subr
75
76 !> Optional local multiplier for density-squared radiative losses.
77 subroutine local_rho2_factor_subr(ixI^L,ixO^L,w,x,factor)
79 integer, intent(in) :: ixI^L, ixO^L
80 double precision, intent(in) :: w(ixI^S,1:nw), x(ixI^S,1:ndim)
81 double precision, intent(out) :: factor(ixI^S)
82 end subroutine local_rho2_factor_subr
83
84 !> Scalar EoS inverse, e.g. fl%eint_from_T(log_nH, log_T)
85 double precision function eos_scalar2_func(a, b)
86 double precision, intent(in) :: a, b
87 end function eos_scalar2_func
88 end interface
89
91
92 double precision :: rad_damp_height
93 double precision :: rad_damp_scale
94
95 ! these are set in init method
96 double precision, allocatable :: tcool(:), lcool(:), dldtcool(:)
97 double precision, allocatable :: yc(:)
98 double precision :: tref, lref, tcoolmin,tcoolmax
99 double precision :: lgtcoolmin, lgtcoolmax, lgstep
100
101 ! The piecewise powerlaw (PPL) tabels and variabels
102 ! x_* en t_* are given as log_10
103 double precision, allocatable :: y_ppl(:), t_ppl(:), l_ppl(:), a_ppl(:)
104
105 !> Lower limit of temperature
106 double precision :: tlow
107
108 !> Index of the energy density
109 integer :: e_
110 !> Index of cut off temperature for TRAC
111 integer :: tcoff_
112
113 ! these are set as parameters
114 !> Resolution of temperature in interpolated tables
115 integer :: ncool
116
117 integer :: n_ppl
118
119 !> Fixed temperature not lower than tlow
120 logical :: tfix
121
122 !> Add cooling source in a split way (.true.) or un-split way (.false.)
123 logical :: rc_split
124
125 logical :: isppl = .false.
126
127 !> Suppress cooling for T below this threshold (Kelvin) within
128 !> rad_cut_hgt of footpoints. When > 0, cells inside the spatial
129 !> taper region with T < rad_suppress_temp get factor=0; coronal
130 !> cooling above that T proceeds unmodified. Default 0 = disabled.
131 double precision :: rad_suppress_temp = 0.0d0
132 !> Internal: suppress threshold in code units (set from rad_suppress_temp
133 !> during init). Not a namelist parameter.
134 double precision :: suppress_temp_code = 0.0d0
135 !> Master switch for radiative loss modification (spatial + density taper)
136 logical :: rad_modify
137 !> Apply spatial taper at both boundaries (default: lower only)
138 logical :: rad_modify_sym
139 !> Spatial taper height (HEAD addition): suppress cooling within rad_cut_hgt of boundary
140 double precision :: rad_cut_hgt = 0.0d0
141 !> Spatial taper width (HEAD addition): Gaussian dey for the spatial taper
142 double precision :: rad_cut_dey = 0.15d0
143 !> Cooling-curve dt-fraction (HEAD addition; used by legacy explicit-mode dt scaling)
144 double precision :: cfrac = 0.1d0
145 !> cutoff radiative cooling below rad_damp_height
146 logical :: rad_damp
147 ! these are to be set directly
148 !> whether background equilibrium contribution is split off
149 logical :: has_equi = .false.
150 !> whether background equilibrium is compensated in thermal balance
151 logical :: subtract_equi = .false.
152
153 double precision, allocatable :: frac_lowfip(:)
154 !> Index of primitive FIP abundance variable, -1 if disabled
155 integer :: fip_ = -1
156 !> Enable local Newton cooling/heating approximation for optically thick losses
157 logical :: rad_newton = .false.
158 double precision :: rad_newton_pthick = 25.d0
159 double precision :: rad_newton_trad = 0.006d0
160 double precision :: rad_newton_rhosurf = 1.d4
161
162 !> Density threshold for Gaussian taper (code units)
163 double precision :: rad_taper_rho
164 !> Gaussian decay width for density taper
165 double precision :: rad_taper_dey
166
167 !> Enable escape probability cooling modification
168 logical :: rad_escape_prob = .false.
169 !> Effective opacity for escape probability (code units: 1/(density*length))
170 double precision :: rad_kappa_eff = 0.0d0
171 !> Temperature above which kappa->0 (stored in code units, input in Kelvin); 0 = constant kappa
172 double precision :: rad_kappa_tcutoff = 0.0d0
173 !> Sigmoid sharpness exponent for kappa(T) cutoff
174 double precision :: rad_kappa_alpha = 4.0d0
175 !> Escape probability type: 'slab' = (1-exp(-tau))/tau, 'voigt' = Voigt CRD
176 character(len=10) :: rad_escape_type = 'slab'
177 !> Exponential cutoff scale: E *= exp(-tau/tau_cutoff); 0 = disabled
178 double precision :: rad_escape_tau_cutoff = 0.0d0
179 !> Max height from footpoint for column mass integration (code units); 0 = no limit
180 double precision :: rad_escape_height = 0.0d0
181 !> Index into wextra for column mass (set during init)
182 integer :: iw_colmass_ = -1
183 !> wextra index of a user mask multiplying the thin losses (0 off, 1 on); -1 = no mask
184 integer :: iw_mask_ = -1
185 !> wextra index of a user mask for TRAC scaling of the losses (0 unscaled, 1 scaled); -1 none
186 integer :: iw_tracmask_ = -1
187
188 !> Name of cooling curve
189 character(len=std_len) :: coolcurve
190
191 procedure(get_subr1), pointer, nopass :: get_rho => null()
192 procedure(get_subr1), pointer, nopass :: get_te => null()
193 procedure(get_subr1), pointer, nopass :: get_rho_equi => null()
194 procedure(get_subr1), pointer, nopass :: get_pthermal => null()
195 procedure(get_subr1), pointer, nopass :: get_pthermal_equi => null()
196 procedure(get_subr1), pointer, nopass :: get_var_rfactor => null()
197 procedure(get_2var_subr), pointer, nopass :: get_ne_nh => null()
198 procedure(get_2var_subr), pointer, nopass :: get_ne_nh_equi => null()
199 procedure(local_rho2_factor_subr), pointer, nopass :: get_rho2_factor => null()
200 procedure(get_subr1), pointer, nopass :: get_temperature_equi => null()
201 !> EoS snapshots + scalar inverse accessors (set in bind_eos_to_source); let
202 !> cooling reach thermodynamics only through this object, never mod_eos.
203 logical :: ione = .false.
204 !> analytic Saha EoS method, set at bind
205 logical :: method_analytic = .false.
206 double precision :: inv_gamma_minus_1
207 double precision :: nh2rhofactor
208 double precision :: eion_per_nh
209 procedure(eos_scalar2_func), pointer, nopass :: eint_from_t => null()
210 !> lower log10(T) bound of the (rho,T) inverse table, to clamp tlow lookups
211 double precision :: log_t_floor = -30.0d0
212 procedure(eos_scalar2_func), pointer, nopass :: p2eint => null()
213 procedure(eos_scalar2_func), pointer, nopass :: t_from_eint => null()
214 procedure(eos_scalar2_func), pointer, nopass :: y_from_eint => null()
215
216 !> Variable-c_V Townsend extension (Y_mod). Built only when fl%ionE.
217 !>
218 !> Y_mod(j, i) is the modified TEF (units of code time) at the
219 !> (log10 nH index j, T index i) grid point. Indexing follows the
220 !> existing AMRVAC convention: Y(ncool) = 0 at the top T = tcoolmax,
221 !> and Y monotonically increases as T decreases. The j axis matches
222 !> the eos%eint_from_T table's nH grid (var1_min..var1_max, dim1).
223 !>
224 !> Construction: change-of-variables u = e_int/n_H. The integrand
225 !> 1/(n_e Lambda) is sampled at composite Simpson or Boole nodes
226 !> between [u(T_i), u(T_{i+1})]. See build_Y_mod_table.
227 !>
228 !> Lookups: findY_mod(T, nH, fl) returns Y in (log_nH, log_T), with nH*Y interpolated
229 !> along nH. findT_mod(Y, nH, fl) returns T by bisection
230 !> on the row at the interpolated nH. (A precomputed inverse table
231 !> variant was prototyped during development but proved unusable:
232 !> at extreme nH the per-row Y_max varies so widely that bilinear
233 !> blending between rows mixes physically saturated and unsaturated
234 !> entries. The bisect path is O(log ncool) ~ 12 iterations anyway.)
235 double precision, allocatable :: y_mod(:,:)
236 double precision, allocatable :: y_mod_max_per_row(:)
237 integer :: y_mod_n_nh = 0
238 double precision :: y_mod_lg_nh_min = 0.0d0
239 double precision :: y_mod_lg_nh_max = 0.0d0
240 double precision :: y_mod_lg_nh_step_inv = 0.0d0
241 !> step of the nH axis in log10, zero for a single row
242 double precision :: y_mod_lg_nh_step = 0.0d0
243 !> Build flag - set to .true. only after the table has been populated
244 !> by build_Y_mod_table (called from bind_eos_to_source after eos_finalise).
245 logical :: y_mod_built = .false.
246 !> rc_T_Ymod_max_K in code units, set at bind: above it c_V is constant and Townsend is exact
247 double precision :: t_ymod_max = huge(1.0d0)
248 !> Quadrature method: 'simpson' (3-point, O(h^4)) or 'boole' (5-point, O(h^6))
249 character(len=8) :: y_mod_quadrature = 'boole'
250 !> Number of sub-intervals per [u_i, u_{i+1}] segment for the quadrature
251 integer :: y_mod_n_sub = 16
252
253 !> SPEX-style two-table cooling support.
254 !>
255 !> The SPEX/SPEX_DM cooling tables follow Schure et al. (2009) which
256 !> publishes the cooling function in two parts:
257 !> Lambda_SPEX(T) -- the cooling rate per n_e n_H (their Lambda_N, Table 2)
258 !> nenh_SPEX(T) -- the CIE equilibrium n_e/n_H ratio at temperature T
259 !> Reconstructing the volumetric cooling rate is then:
260 !> Q = n_H^2 * nenh_eq(T) * Lambda_SPEX(T)
261 !>
262 !> All other cooling tables in AMRVAC follow the standard Dere/CHIANTI
263 !> convention where Q = n_e * n_H * Lambda(T) and the equilibrium
264 !> ionisation balance is baked into Lambda(T) itself.
265 !>
266 !> Historically, AMRVAC handled the SPEX two-table convention by
267 !> absorbing log10(nenh_SPEX) into Lambda_table at construction time, so
268 !> that the standard formula Q = n_e n_H * Lambda_table happened to give
269 !> the right answer when n_e ~ n_H (the FI assumption with neOnH ~ 1.2).
270 !> This trick silently *breaks* in LTE+ionE mode where the simulation
271 !> n_e is the actual Saha value: n_e/n_H << 1 at low T, so the formula
272 !> double-counts the equilibrium factor and badly under-counts cooling.
273 !>
274 !> The fix below: do NOT absorb nenh into Lambda_table. Instead, store
275 !> the equilibrium array nenh_eq_table on the same tcool grid, and at
276 !> runtime use Q = n_H^2 * nenh_eq(T) * Lambda_table whenever
277 !> lambda_needs_nenh_table is .true. This honours the published SPEX
278 !> convention regardless of the EoS choice.
279 logical :: lambda_needs_nenh_table = .false.
280 double precision, allocatable :: nenh_eq_table(:)
281
282 end type rc_fluid
283
284 contains
285
286 subroutine radiative_cooling_rho2_factor(ixI^L,ixO^L,w,x,fl,factor)
288 integer, intent(in) :: ixI^L, ixO^L
289 double precision, intent(in) :: w(ixI^S,1:nw), x(ixI^S,1:ndim)
290 type(rc_fluid), intent(in) :: fl
291 double precision, intent(out) :: factor(ixI^S)
292 factor(ixo^s)=one
293 if(associated(fl%get_rho2_factor)) &
294 call fl%get_rho2_factor(ixi^l,ixo^l,w,x,factor)
295 end subroutine radiative_cooling_rho2_factor
296
297 !> Radiative cooling initialization
298 subroutine radiative_cooling_init_params(phys_gamma,He_abund)
300 double precision, intent(in) :: phys_gamma,He_abund
301
302 rc_gamma=phys_gamma
303 he_abundance=he_abund
304 end subroutine radiative_cooling_init_params
305
306 !> Build the Voigt escape probability lookup table.
307 !> Called once (guarded by voigt_table_ready flag).
308 !> Uses 64-point Gauss-Legendre quadrature on [0, x_max] to evaluate
309 !> E(tau_0) = (2 phi(0)/tau_0) * integral_0^{x_max} [1-exp(-tau_0 g(x))] dx
310 !> where g(x) = phi(x)/phi(0) for the Voigt profile H(a,x) truncated at x_max.
312 use mod_global_parameters, only: dpi
313 implicit none
314 integer, parameter :: nquad = 64
315 double precision :: xq(nquad), wq(nquad)
316 double precision :: logtau, tau0, phi0, gx, integrand, E_val
317 double precision :: a_rep
318 integer :: i, k
319
320 if(voigt_table_ready) return
321
322 ! Step size in log10(tau)
323 voigt_logtau_step = (voigt_logtau_max - voigt_logtau_min) / dble(n_voigt_table - 1)
324
325 ! Get Gauss-Legendre nodes and weights on [0, x_max]
326 call voigt_gauss_legendre(0.0d0, voigt_xmax, nquad, xq, wq)
327
328 ! Representative Voigt parameter (a_ref at T = 10 kK)
329 a_rep = voigt_a_ref
330
331 ! phi(0) for the Voigt profile: H(a,0) = exp(a^2)*erfc(a)/sqrt(pi) ~ 1/sqrt(pi)
332 phi0 = 1.0d0 / sqrt(dpi)
333
334 ! Precompute g(x) = phi(x)/phi(0) at quadrature nodes
335 ! For the Voigt profile: phi(x) = H(a,x), the real part of the
336 ! Faddeeva function divided by sqrt(pi).
337 ! For small a, H(a,x) ~ exp(-x^2)/sqrt(pi) + a/(pi*x^2) for |x|>few.
338 ! Use the exact Humlicek (1982) rational approximation.
339
340 do i = 1, n_voigt_table
341 logtau = voigt_logtau_min + dble(i-1) * voigt_logtau_step
342 tau0 = 10.0d0**logtau
343
344 if(tau0 < 1.0d-6) then
345 voigt_e_table(i) = 1.0d0
346 cycle
347 end if
348
349 integrand = 0.0d0
350 do k = 1, nquad
351 gx = voigt_profile_ratio(a_rep, xq(k)) ! phi(x)/phi(0)
352 ! [1 - exp(-tau0 * g(x))]
353 if(tau0 * gx > 500.0d0) then
354 integrand = integrand + wq(k) * 1.0d0
355 else if(tau0 * gx < 1.0d-10) then
356 integrand = integrand + wq(k) * tau0 * gx
357 else
358 integrand = integrand + wq(k) * (1.0d0 - exp(-tau0 * gx))
359 end if
360 end do
361
362 e_val = 2.0d0 * phi0 / tau0 * integrand
363 ! Clamp to [0, 1]
364 voigt_e_table(i) = max(0.0d0, min(1.0d0, e_val))
365 end do
366
367 voigt_table_ready = .true.
368
369 end subroutine voigt_escape_init_table
370
371 !> Voigt profile ratio phi(x)/phi(0) using Humlicek (1982) Region I/II approx.
372 !> For the small-a regime (a < 0.01), this simplifies to:
373 !> H(a,x)/H(a,0) ~ exp(-x^2) + a*sqrt(pi)/x^2 for |x| > ~2
374 !> We use the exact Gaussian core + Lorentzian wing decomposition.
375 double precision function voigt_profile_ratio(a, x)
376 use mod_global_parameters, only: dpi
377 implicit none
378 double precision, intent(in) :: a, x
379 double precision :: gauss_part, lorentz_part, phi_x, phi_0
380
381 ! phi(0) = H(a,0) ~ 1/sqrt(pi) * (1 + ...) for small a
382 phi_0 = 1.0d0 / sqrt(dpi)
383
384 ! For small a: H(a,x) ~ exp(-x^2)/sqrt(pi) for |x| < ~3
385 ! and H(a,x) ~ a/(pi*x^2) for |x| >> 1 where Lorentzian dominates
386 ! Use additive approximation: H(a,x) ~ exp(-x^2)/sqrt(pi) + a/(pi*(x^2+a^2))
387 gauss_part = exp(-x*x) / sqrt(dpi)
388 if(x*x + a*a > 1.0d-30) then
389 lorentz_part = a / (dpi * (x*x + a*a))
390 else
391 lorentz_part = 0.0d0
392 end if
393 phi_x = gauss_part + lorentz_part
394
395 voigt_profile_ratio = phi_x / phi_0
396
397 end function voigt_profile_ratio
398
399 !> Look up the Voigt escape probability for a given tau.
400 !> Uses linear interpolation in log10(tau) space.
401 double precision function voigt_escape_lookup(tau)
402 implicit none
403 double precision, intent(in) :: tau
404 double precision :: logtau, frac
405 integer :: idx
406
407 if(tau < 1.0d-6) then
408 voigt_escape_lookup = 1.0d0
409 return
410 end if
411
412 logtau = log10(tau)
413
414 if(logtau <= voigt_logtau_min) then
415 voigt_escape_lookup = voigt_e_table(1)
416 return
417 end if
418
419 if(logtau >= voigt_logtau_max) then
420 ! Extrapolate with 1/tau scaling from last table entry
421 voigt_escape_lookup = voigt_e_table(n_voigt_table) &
422 * (10.0d0**voigt_logtau_max) / tau
423 return
424 end if
425
426 ! Linear interpolation
427 frac = (logtau - voigt_logtau_min) / voigt_logtau_step
428 idx = int(frac) + 1
429 idx = max(1, min(idx, n_voigt_table - 1))
430 frac = frac - dble(idx - 1)
431
432 voigt_escape_lookup = voigt_e_table(idx) * (1.0d0 - frac) &
433 + voigt_e_table(idx + 1) * frac
434
435 end function voigt_escape_lookup
436
437 !> Gauss-Legendre quadrature nodes and weights on [a,b].
438 !> Uses the Golub-Welsch algorithm for n points.
439 subroutine voigt_gauss_legendre(a, b, n, x, w)
440 use mod_global_parameters, only: dpi
441 implicit none
442 double precision, intent(in) :: a, b
443 integer, intent(in) :: n
444 double precision, intent(out) :: x(n), w(n)
445 double precision :: xi, wi, p0, p1, p2, pp, z, z1
446 integer :: i, j, k, m
447
448 m = (n + 1) / 2
449
450 do i = 1, m
451 ! Initial guess for i-th root
452 z = cos(dpi * (dble(i) - 0.25d0) / (dble(n) + 0.5d0))
453
454 ! Newton iteration
455 do j = 1, 100
456 p0 = 1.0d0
457 p1 = 0.0d0
458 do k = 1, n
459 p2 = p1
460 p1 = p0
461 p0 = ((2.0d0*dble(k) - 1.0d0) * z * p1 - (dble(k) - 1.0d0) * p2) / dble(k)
462 end do
463 ! p0 = P_n(z), derivative:
464 pp = dble(n) * (z * p0 - p1) / (z*z - 1.0d0)
465 z1 = z
466 z = z - p0 / pp
467 if(abs(z - z1) < 1.0d-15) exit
468 end do
469
470 ! Map from [-1,1] to [a,b]
471 xi = 0.5d0 * ((b - a) * z + (b + a))
472 wi = (b - a) / ((1.0d0 - z*z) * pp*pp)
473
474 x(i) = xi
475 w(i) = wi
476 x(n + 1 - i) = a + b - xi
477 w(n + 1 - i) = wi
478 end do
479
480 end subroutine voigt_gauss_legendre
481
482 subroutine radiative_cooling_init(fl,read_params)
484 interface
485 subroutine read_params(fl)
487 import rc_fluid
488 type(rc_fluid), intent(inout) :: fl
489
490 end subroutine read_params
491 end interface
492
493 type(rc_fluid), intent(inout) :: fl
494
495 double precision, dimension(:), allocatable :: t_table
496 double precision, dimension(:), allocatable :: L_table
497 double precision, dimension(:), allocatable :: f_table
498 double precision :: ratt, fact1, fact2, fact3, dL1, dL2
499 double precision :: tstep, Lstep
500 integer :: ntable, i, j
501 logical :: jump
502 Character(len=65) :: PPL_curves(1:6)
503
504 fl%ncool=4000
505 fl%coolcurve='JCcorona'
506 fl%tlow=bigdouble
507 fl%Tfix=.false.
508 fl%rc_split=.false.
509 fl%rad_suppress_temp=0.0d0
510 fl%rad_cut_hgt=0.0d0
511 fl%rad_cut_dey=0.15d0
512 fl%rad_modify=.false.
513 fl%rad_modify_sym=.false.
514 fl%rad_taper_rho=bigdouble
515 fl%rad_taper_dey=0.0d0
516 fl%rad_damp=.false.
517 fl%rad_damp_height=0.5d0
518 fl%rad_damp_scale=0.15d0
519 call read_params(fl)
520
521 ! Build Voigt escape lookup table if needed (once, shared across fluids)
522 if(fl%rad_escape_prob .and. fl%rad_escape_type == 'voigt') then
524 if(mype == 0) then
525 write(*,'(A,I0,A,ES9.2,A,F4.1,A)') &
526 ' Voigt escape table: ', n_voigt_table, ' points, a_ref=', &
527 voigt_a_ref, ', x_max=', voigt_xmax, ' Doppler widths'
528 end if
529 end if
530
531 if (fl%fip_ > 0) then
532 select case (trim(fl%coolcurve))
533 case ('Dere_photo', 'Dere_photo_DM')
534 case default
535 call mpistop("FIP cooling requires coolcurve='Dere_photo' or 'Dere_photo_DM'")
536 end select
537 end if
538
539 if(fl%rc_split) any_source_split=.true.
540
541 ! Checks if coolcurve is a piecewise power law (PPL)
542 ppl_curves = [Character(len=65) :: 'Hildner','FM', 'Rosner', 'Klimchuk','SPEX_DM_rough','SPEX_DM_fine']
543 do i=1,size(ppl_curves)
544 if (ppl_curves(i)==fl%coolcurve) then
545 fl%isPPL = .true.
546 end if
547 end do
548
549 ! Init for PPL
550 if (fl%isPPL) then
551 ! Read in tables and create t_PPL, l_PPL, a_PPL
552 select case(fl%coolcurve)
553
554 case('Hildner')
555 if(mype ==0) &
556 print *,'Use Hildner (1974) piecewise power law'
557 fl%n_PPL = n_hildner
558 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
559 allocate(fl%a_PPL(1:fl%n_PPL))
560 fl%t_PPL(1:fl%n_PPL+1) = t_hildner(1:n_hildner+1)
561 fl%a_PPL(1:fl%n_PPL) = a_hildner(1:n_hildner)
562 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_hildner(1:n_hildner) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
563
564 case('FM')
565 if(mype==0) &
566 print *,'Use Forbes and Malherbe (1991)-like piecewise power law'
567 fl%n_PPL = n_fm
568 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
569 allocate(fl%a_PPL(1:fl%n_PPL))
570 fl%t_PPL(1:fl%n_PPL+1) = t_fm(1:n_fm+1)
571 fl%a_PPL(1:fl%n_PPL) = a_fm(1:n_fm)
572 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_fm(1:n_fm) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
573
574 case('Rosner')
575 if(mype==0) &
576 print *,'Use piecewise power law according to Rosner (1978)'
577 if(mype ==0) &
578 print *,'and extended by Priest (1982) from Van Der Linden (1991)'
579 fl%n_PPL = n_rosner
580 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
581 allocate(fl%a_PPL(1:fl%n_PPL))
582 fl%t_PPL(1:fl%n_PPL+1) = t_rosner(1:n_rosner+1)
583 fl%a_PPL(1:fl%n_PPL) = a_rosner(1:n_rosner)
584 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_rosner(1:n_rosner) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
585
586 case('Klimchuk')
587 if(mype==0) &
588 print *,'Use Klimchuk (2008) piecewise power law'
589 fl%n_PPL = n_klimchuk
590 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
591 allocate(fl%a_PPL(1:fl%n_PPL))
592 fl%t_PPL(1:fl%n_PPL+1) = t_klimchuk(1:n_klimchuk+1)
593 fl%a_PPL(1:fl%n_PPL) = a_klimchuk(1:n_klimchuk)
594 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_klimchuk(1:n_klimchuk) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
595
596 case('SPEX_DM_rough')
597 if(mype==0) &
598 print *,'Use the rough piece wise power law fit to the SPEX_DM curve (2009)'
599 fl%n_PPL = n_spex_dm_rough
600 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
601 allocate(fl%a_PPL(1:fl%n_PPL))
602 fl%t_PPL(1:fl%n_PPL+1) = t_spex_dm_rough(1:n_spex_dm_rough+1)
603 fl%a_PPL(1:fl%n_PPL) = a_spex_dm_rough(1:n_spex_dm_rough)
604 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_spex_dm_rough(1:n_spex_dm_rough) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
605
606 case('SPEX_DM_fine')
607 if(mype==0) &
608 print *,'Use the fine, detailed piece wise power law fit to the SPEX_DM curve (2009)'
609 fl%n_PPL = n_spex_dm_fine
610 allocate(fl%t_PPL(1:fl%n_PPL+1), fl%l_PPL(1:fl%n_PPL+1))
611 allocate(fl%a_PPL(1:fl%n_PPL))
612 fl%t_PPL(1:fl%n_PPL+1) = t_spex_dm_fine(1:n_spex_dm_fine+1)
613 fl%a_PPL(1:fl%n_PPL) = a_spex_dm_fine(1:n_spex_dm_fine)
614 fl%l_PPL(1:fl%n_PPL) = 10.d0**x_spex_dm_fine(1:n_spex_dm_fine) * (10.d0**fl%t_PPL(1:fl%n_PPL))**fl%a_PPL(1:fl%n_PPL)
615
616 case default
617 call mpistop("This piecewise power law is unknown")
618 end select
619
620 ! Go from logarithmic to actual values.
621 fl%t_PPL(1:fl%n_PPL+1) = 10.d0**fl%t_PPL(1:fl%n_PPL+1)
622 ! Change unit of table if SI is used instead of cgs
623 if (si_unit) fl%l_PPL(1:fl%n_PPL) = fl%l_PPL(1:fl%n_PPL) * 10.0d0**(-13)
624
625 ! Make dimensionless
626 fl%t_PPL(1:fl%n_PPL+1) = fl%t_PPL(1:fl%n_PPL+1) / unit_temperature
627 fl%l_PPL(1:fl%n_PPL) = fl%l_PPL(1:fl%n_PPL) * unit_numberdensity**2 * unit_time / unit_pressure
628
629 ! Set tref en lref
630 fl%l_PPL(fl%n_PPL+1) = fl%l_PPL(fl%n_PPL) * ( fl%t_PPL(fl%n_PPL+1) / fl%t_PPL(fl%n_PPL) )**fl%a_PPL(fl%n_PPL)
631 fl%lref = fl%l_PPL(fl%n_PPL+1)
632 fl%tref = fl%t_PPL(fl%n_PPL+1)
633
634 ! Set tcoolmin and tcoolmax
635 fl%tcoolmin = fl%t_PPL(1)
636 fl%tcoolmax = fl%t_PPL(fl%n_PPL+1)
637 ! smaller value for lowest temperatures from cooling table and user's choice
638 if (fl%tlow==bigdouble) fl%tlow=fl%tcoolmin
639 !create y_PPL
640 call create_y_ppl(fl)
641
642 else
643
644 ! Init for interpolatable tables
645 allocate(fl%tcool(1:fl%ncool), fl%Lcool(1:fl%ncool), fl%dLdtcool(1:fl%ncool))
646 allocate(fl%Yc(1:fl%ncool))
647 if(fl%fip_ > 0) allocate(fl%frac_lowFIP(1:fl%ncool))
648
649 fl%tcool(1:fl%ncool) = zero
650 fl%Lcool(1:fl%ncool) = zero
651 fl%dLdtcool(1:fl%ncool) = zero
652
653 ! Read in the selected cooling curve
654 select case(fl%coolcurve)
655
656 case('JCcorona')
657 if(mype ==0) &
658 print *,'Use Colgan & Feldman (2008) cooling curve'
659 if(mype ==0) &
660 print *,'This version only till 10000 K, beware for floor T treatment'
661 ntable = n_jccorona
662 allocate(t_table(1:ntable))
663 allocate(l_table(1:ntable))
664 t_table(1:ntable) = t_jccorona(1:n_jccorona)
665 l_table(1:ntable) = l_jccorona(1:n_jccorona)
666
667 case('DM')
668 if(mype ==0) &
669 print *,'Use Dalgarno & McCray (1972) cooling curve'
670 ntable = n_dm
671 allocate(t_table(1:ntable))
672 allocate(l_table(1:ntable))
673 t_table(1:ntable) = t_dm(1:n_dm)
674 l_table(1:ntable) = l_dm(1:n_dm)
675
676 case('MB')
677 if(mype ==0) &
678 write(*,'(3a)') 'Use MacDonald & Bailey (1981) cooling curve '&
679 ,'as implemented in ZEUS-3D, with the values '&
680 ,'from Dalgarno & McCRay (1972) for low temperatures.'
681 ntable = n_mb + 20
682 allocate(t_table(1:ntable))
683 allocate(l_table(1:ntable))
684 t_table(1:ntable) = t_dm(1:21)
685 l_table(1:ntable) = l_dm(1:21)
686 t_table(22:ntable) = t_mb(2:n_mb)
687 l_table(22:ntable) = l_mb(2:n_mb)
688
689 case('MLcosmol')
690 if(mype ==0) &
691 print *,'Use Mellema & Lundqvist (2002) cooling curve '&
692 ,'for zero metallicity '
693 ntable = n_mlcosmol
694 allocate(t_table(1:ntable))
695 allocate(l_table(1:ntable))
696 t_table(1:ntable) = t_mlcosmol(1:n_mlcosmol)
697 l_table(1:ntable) = l_mlcosmol(1:n_mlcosmol)
698
699 case('MLwc')
700 if(mype ==0) &
701 print *,'Use Mellema & Lundqvist (2002) cooling curve '&
702 ,'for WC-star metallicity '
703 ntable = n_mlwc
704 allocate(t_table(1:ntable))
705 allocate(l_table(1:ntable))
706 t_table(1:ntable) = t_mlwc(1:n_mlwc)
707 l_table(1:ntable) = l_mlwc(1:n_mlwc)
708
709 case('MLsolar1')
710 if(mype ==0) &
711 print *,'Use Mellema & Lundqvist (2002) cooling curve '&
712 ,'for solar metallicity '
713 ntable = n_mlsolar1
714 allocate(t_table(1:ntable))
715 allocate(l_table(1:ntable))
716 t_table(1:ntable) = t_mlsolar1(1:n_mlsolar1)
717 l_table(1:ntable) = l_mlsolar1(1:n_mlsolar1)
718
719 case('cloudy_ism')
720 if(mype ==0) &
721 print *,'Use Cloudy based cooling curve '&
722 ,'for ism metallicity '
723 ntable = n_cl_ism
724 allocate(t_table(1:ntable))
725 allocate(l_table(1:ntable))
726 t_table(1:ntable) = t_cl_ism(1:n_cl_ism)
727 l_table(1:ntable) = l_cl_ism(1:n_cl_ism)
728
729 case('cloudy_solar')
730 if(mype ==0) &
731 print *,'Use Cloudy based cooling curve '&
732 ,'for solar metallicity '
733 ntable = n_cl_solar
734 allocate(t_table(1:ntable))
735 allocate(l_table(1:ntable))
736 t_table(1:ntable) = t_cl_solar(1:n_cl_solar)
737 l_table(1:ntable) = l_cl_solar(1:n_cl_solar)
738
739 case('composite_solar')
740 if(mype ==0) then
741 print *, 'Use composite cooling curve for solar metallicity:'
742 print *, ' T > 12 kK: Dere/Colgan/SPEX weighted average'
743 print *, ' 7-12 kK: SPEX CIE through the Lya transition'
744 print *, ' T < 7 kK: cloudy_solar fine-structure/molecular'
745 end if
746 ntable = n_composite
747 allocate(t_table(1:ntable))
748 allocate(l_table(1:ntable))
749 t_table(1:ntable) = t_composite(1:n_composite)
750 l_table(1:ntable) = l_composite(1:n_composite)
751
752 case('SPEX')
753 if(mype ==0) &
754 print *,'Use SPEX cooling curve (Schure et al. 2009) '&
755 ,'for solar metallicity '
756 ntable = n_spex
757 allocate(t_table(1:ntable))
758 allocate(l_table(1:ntable))
759 t_table(1:ntable) = t_spex(1:n_spex)
760 l_table(1:ntable) = l_spex(1:n_spex) + log10(nenh_spex(1:n_spex))
761 ! SPEX two-table convention: the absorbed nenh_SPEX factor in
762 ! L_table makes Q = n_e * n_H * Lambda give the published rate
763 ! ONLY when n_e ~ n_H (the FI assumption). In LTE+ionE mode the
764 ! actual Saha n_e is much smaller than n_H at low T and the
765 ! formula double-counts the equilibrium factor. The flag below
766 ! tells the LTE+ionE code path to substitute n_H for n_e in the
767 ! cooling rate, which recovers the correct published rate
768 ! Q = n_H^2 * (nenh_eq * Lambda_SPEX) = n_H^2 * Lambda_table.
769 fl%lambda_needs_nenh_table = .true.
770
771 case('SPEX_DM')
772 if(mype ==0) then
773 print *, 'Use SPEX cooling curve for solar metallicity above 10^4 K. '
774 print *, 'At lower temperatures,use Dalgarno & McCray (1972), '
775 print *, 'with a pre-set ionization fraction of 10^-3. '
776 print *, 'as described by Schure et al. (2009). '
777 endif
778 ntable = n_spex + n_dm_2 - 6
779 allocate(t_table(1:ntable))
780 allocate(l_table(1:ntable))
781 t_table(1:n_dm_2-1) = t_dm_2(1:n_dm_2-1)
782 l_table(1:n_dm_2-1) = l_dm_2(1:n_dm_2-1)
783 t_table(n_dm_2:ntable) = t_spex(6:n_spex)
784 l_table(n_dm_2:ntable) = l_spex(6:n_spex) + log10(nenh_spex(6:n_spex))
785 ! Same SPEX two-table convention as the pure SPEX case above.
786 ! The DM_2 segment was tabulated assuming y_DM = 10^-3 already
787 ! built into the published L_DM_2 values, so it follows the
788 ! same "Lambda_table includes the nenh factor" convention as
789 ! the SPEX segment above (modulo a constant 10^-3 instead of
790 ! the SPEX equilibrium ratio). Treating both halves with the
791 ! same n_H^2 * Lambda_table formula in LTE+ionE mode gives
792 ! consistent rates and removes the double-counting.
793 fl%lambda_needs_nenh_table = .true.
794
795 case('Dere_corona')
796 if(mype ==0) &
797 print *,'Use Dere (2009) cooling curve for solar corona'
798 ntable = n_dere
799 allocate(t_table(1:ntable))
800 allocate(l_table(1:ntable))
801 t_table(1:ntable) = t_dere(1:n_dere)
802 l_table(1:ntable) = l_dere_corona(1:n_dere)
803
804 case('Dere_corona_DM')
805 if(mype==0)&
806 print *, 'Combination of Dere_corona (2009) for high temperatures and'
807 if(mype==0)&
808 print *, 'Dalgarno & McCray (1972), DM2, for low temperatures'
809 ntable = n_dere + n_dm_2 - 1
810 allocate(t_table(1:ntable))
811 allocate(l_table(1:ntable))
812 t_table(1:n_dm_2-1) = t_dm_2(1:n_dm_2-1)
813 l_table(1:n_dm_2-1) = l_dm_2(1:n_dm_2-1)
814 t_table(n_dm_2:ntable) = t_dere(1:n_dere)
815 l_table(n_dm_2:ntable) = l_dere_corona(1:n_dere)
816
817 case('Dere_photo')
818 if(mype ==0) &
819 print *,'Use Dere (2009) cooling curve for solar photophere'
820 ntable = n_dere
821 allocate(t_table(1:ntable))
822 allocate(l_table(1:ntable))
823 if (fl%fip_ > 0) allocate(f_table(1:ntable))
824 t_table(1:ntable) = t_dere(1:n_dere)
825 l_table(1:ntable) = l_dere_photo(1:n_dere)
826 if (fl%fip_ > 0) f_table(1:ntable) = lowfip_frac(1:n_dere)
827
828 case('Dere_photo_DM')
829 if(mype==0)&
830 print *, 'Combination of Dere_photo (2009) for high temperatures and'
831 if(mype==0)&
832 print *, 'Dalgarno & McCray (1972), DM2, for low temperatures'
833 ntable = n_dere + n_dm_2 - 1
834 allocate(t_table(1:ntable))
835 allocate(l_table(1:ntable))
836 if (fl%fip_ > 0) allocate(f_table(1:ntable))
837 t_table(1:n_dm_2-1) = t_dm_2(1:n_dm_2-1)
838 l_table(1:n_dm_2-1) = l_dm_2(1:n_dm_2-1)
839 t_table(n_dm_2:ntable) = t_dere(1:n_dere)
840 l_table(n_dm_2:ntable) = l_dere_photo(1:n_dere)
841 if (fl%fip_ > 0) then
842 f_table(1:n_dm_2-1) = zero
843 f_table(n_dm_2:ntable) = lowfip_frac(1:n_dere)
844 end if
845
846 case('Colgan')
847 if(mype==0) &
848 print *, 'Use Colgan (2008) cooling curve'
849 ntable = n_colgan
850 allocate(t_table(1:ntable))
851 allocate(l_table(1:ntable))
852 t_table(1:ntable) = t_colgan(1:n_colgan)
853 l_table(1:ntable) = l_colgan(1:n_colgan)
854
855 case('Colgan_DM')
856 if(mype==0)&
857 print *, 'Combination of Colgan (2008) for high temperatures and'
858 if(mype==0)&
859 print *, 'Dalgarno & McCray (1972), DM2, for low temperatures'
860 ntable = n_colgan + n_dm_2
861 allocate(t_table(1:ntable))
862 allocate(l_table(1:ntable))
863 t_table(1:n_dm_2) = t_dm_2(1:n_dm_2)
864 l_table(1:n_dm_2) = l_dm_2(1:n_dm_2)
865 t_table(n_dm_2+1:ntable) = t_colgan(1:n_colgan)
866 l_table(n_dm_2+1:ntable) = l_colgan(1:n_colgan)
867
868 case default
869 call mpistop("This coolingcurve is unknown")
870 end select
871
872
873 ! create cooling table(s) for use in amrvac
874 fl%tcoolmax = t_table(ntable)
875 fl%tcoolmin = t_table(1)
876 ratt = (fl%tcoolmax-fl%tcoolmin)/( dble(fl%ncool-1) + smalldouble)
877
878 fl%tcool(1) = fl%tcoolmin
879 fl%Lcool(1) = l_table(1)
880
881 fl%tcool(fl%ncool) = fl%tcoolmax
882 fl%Lcool(fl%ncool) = l_table(ntable)
883
884 if (fl%fip_ > 0) then
885 fl%frac_lowFIP(1) = f_table(1)
886 fl%frac_lowFIP(fl%ncool) = f_table(ntable)
887 end if
888
889 do i=2,fl%ncool ! loop to create one table
890 fl%tcool(i) = fl%tcool(i-1)+ratt
891 do j=1,ntable-1 ! loop to create one spot on a table
892 ! Second order polynomial interpolation, except at the outer edge,
893 ! or in case of a large jump.
894 if(fl%tcool(i) < t_table(j+1)) then
895 if(j.eq. ntable-1 )then
896 fact1 = (fl%tcool(i)-t_table(j+1)) &
897 /(t_table(j)-t_table(j+1))
898 fact2 = (fl%tcool(i)-t_table(j)) &
899 /(t_table(j+1)-t_table(j))
900 fl%Lcool(i) = l_table(j)*fact1 + l_table(j+1)*fact2
901 if (fl%fip_ > 0) then
902 fl%frac_lowFIP(i) = f_table(j)*fact1 + f_table(j+1)*fact2
903 end if
904 exit
905 else
906 dl1 = l_table(j+1)-l_table(j)
907 dl2 = l_table(j+2)-l_table(j+1)
908 jump =(max(dabs(dl1),dabs(dl2)) > 2*min(dabs(dl1),dabs(dl2)))
909 end if
910 if( jump ) then
911 fact1 = (fl%tcool(i)-t_table(j+1)) &
912 /(t_table(j)-t_table(j+1))
913 fact2 = (fl%tcool(i)-t_table(j)) &
914 /(t_table(j+1)-t_table(j))
915 fl%Lcool(i) = l_table(j)*fact1 + l_table(j+1)*fact2
916 if (fl%fip_ > 0) then
917 fl%frac_lowFIP(i) = f_table(j)*fact1 + f_table(j+1)*fact2
918 end if
919 exit
920 else
921 fact1 = ((fl%tcool(i)-t_table(j+1)) &
922 * (fl%tcool(i)-t_table(j+2))) &
923 / ((t_table(j)-t_table(j+1)) &
924 * (t_table(j)-t_table(j+2)))
925 fact2 = ((fl%tcool(i)-t_table(j)) &
926 * (fl%tcool(i)-t_table(j+2))) &
927 / ((t_table(j+1)-t_table(j)) &
928 * (t_table(j+1)-t_table(j+2)))
929 fact3 = ((fl%tcool(i)-t_table(j)) &
930 * (fl%tcool(i)-t_table(j+1))) &
931 / ((t_table(j+2)-t_table(j)) &
932 * (t_table(j+2)-t_table(j+1)))
933 fl%Lcool(i) = l_table(j)*fact1 + l_table(j+1)*fact2 &
934 + l_table(j+2)*fact3
935 if (fl%fip_ > 0) then
936 fl%frac_lowFIP(i) = f_table(j)*fact1 + f_table(j+1)*fact2 &
937 + f_table(j+2)*fact3
938 end if
939 exit
940 end if
941 end if
942 end do ! end loop to find create one spot on a table
943 end do ! end loop to create one table
944
945 ! Go from logarithmic to actual values.
946 fl%tcool(1:fl%ncool) = 10.0d0**fl%tcool(1:fl%ncool)
947 fl%Lcool(1:fl%ncool) = 10.0d0**fl%Lcool(1:fl%ncool)
948
949 ! Change unit of table if SI is used instead of cgs
950 if (si_unit) fl%Lcool(1:fl%ncool) = fl%Lcool(1:fl%ncool) * 10.0d0**(-13)
951
952 ! Scale both T and Lambda
953 fl%tcool(1:fl%ncool) = fl%tcool(1:fl%ncool) / unit_temperature
954 fl%Lcool(1:fl%ncool) = fl%Lcool(1:fl%ncool) * unit_numberdensity**2 * unit_time / unit_pressure
955
956 fl%tcoolmin = fl%tcool(1)+smalldouble ! avoid pointless interpolation
957 ! Convert rad_suppress_temp from Kelvin to code units
958 if(fl%rad_suppress_temp > 0.0d0) then
959 fl%suppress_temp_code = fl%rad_suppress_temp / unit_temperature
960 if(mype == 0) then
961 write(*,'(A,ES10.3,A)') ' Cooling suppression active: disabled below T = ', &
962 fl%rad_suppress_temp, ' K within rad_cut_hgt'
963 end if
964 end if
965 ! smaller value for lowest temperatures from cooling table and user's choice
966 if (fl%tlow==bigdouble) fl%tlow=fl%tcoolmin
967 fl%tcoolmax = fl%tcool(fl%ncool)
968 fl%lgtcoolmin = dlog10(fl%tcoolmin)
969 fl%lgtcoolmax = dlog10(fl%tcoolmax)
970 fl%lgstep = (fl%lgtcoolmax-fl%lgtcoolmin) * 1.d0 / (fl%ncool-1)
971 fl%dLdtcool(1) = (fl%Lcool(2)-fl%Lcool(1))/(fl%tcool(2)-fl%tcool(1))
972 fl%dLdtcool(fl%ncool) = (fl%Lcool(fl%ncool)-fl%Lcool(fl%ncool-1))/(fl%tcool(fl%ncool)-fl%tcool(fl%ncool-1))
973
974 do i=2,fl%ncool-1
975 fl%dLdtcool(i) = (fl%Lcool(i+1)-fl%Lcool(i-1))/(fl%tcool(i+1)-fl%tcool(i-1))
976 end do
977
978 deallocate(t_table)
979 deallocate(l_table)
980 if (allocated(f_table)) deallocate(f_table)
981
982 fl%tref = fl%tcoolmax
983 fl%lref = fl%Lcool(fl%ncool)
984 fl%Yc(fl%ncool) = zero
985 do i=fl%ncool-1, 1, -1
986 fl%Yc(i) = fl%Yc(i+1)
987 do j=1,100
988 tstep = 1.0d-2*(fl%tcool(i+1)-fl%tcool(i))
989 call findl(fl%tcool(i+1)-j*tstep, lstep, fl)
990 fl%Yc(i) = fl%Yc(i) + fl%lref/fl%tref*tstep/lstep
991 end do
992 end do
993 end if
994
995 rc_gamma_1=rc_gamma-1.d0
996 invgam = 1.d0/rc_gamma_1
997
998 end subroutine radiative_cooling_init
999
1000 subroutine create_y_ppl(fl)
1001 ! creates the constants of integration needed for solving
1002 ! the cooling law exact for a piecewise power law
1003 ! In correspondence with eq. A6 of Townsend (2009)
1005 type(rc_fluid) :: fl
1006 double precision :: y_extra, factor
1007 integer :: i
1008
1009 allocate(fl%y_PPL(1:fl%n_PPL+1))
1010
1011 fl%y_PPL(1:fl%n_PPL+1) = zero
1012
1013 do i=fl%n_PPL, 1, -1
1014 factor = fl%l_PPL(fl%n_PPL+1) * fl%t_PPL(i) / (fl%l_PPL(i) * fl%t_PPL(fl%n_PPL+1))
1015 if (fl%a_PPL(i) == 1.d0) then
1016 y_extra = log( fl%t_PPL(i) / fl%t_PPL(i+1) )
1017 else
1018 y_extra = 1 / (1 - fl%a_PPL(i)) * (1 - ( fl%t_PPL(i) / fl%t_PPL(i+1) )**(fl%a_PPL(i)-1) )
1019 end if
1020 fl%y_PPL(i) = fl%y_PPL(i+1) - factor*y_extra
1021 end do
1022 end subroutine create_y_ppl
1023
1024 subroutine getvar_cooling(ixI^L,ixO^L,w,x,coolrate,fl)
1025 ! Create extra variable to show cooling rate in the output
1026 ! Uses a simple explicit scheme.
1027 ! N.B. Since there is no knowledge of the timestep size,
1028 ! there is no upper limit for the cooling rate.
1030
1031 integer, intent(in) :: ixI^L,ixO^L
1032 double precision, intent(in) :: x(ixI^S,1:ndim)
1033 double precision :: w(ixI^S,1:nw)
1034 double precision, intent(out):: coolrate(ixI^S)
1035 type(rc_fluid), intent(in) :: fl
1036
1037 double precision :: rho(ixI^S)
1038 double precision :: L1,Te(ixI^S),Rfactor(ixI^S)
1039 double precision :: ne(ixI^S), nH_arr(ixI^S), rho2_factor(ixI^S)
1040 double precision :: taper
1041 integer :: ix^D
1042
1043 call fl%get_rho(w,x,ixi^l,ixo^l,rho)
1044 call fl%get_var_Rfactor(w,x,ixi^l,ixo^l,rfactor)
1045 call fl%get_Te(w,x,ixi^l,ixo^l,te)
1046 call fl%get_ne_nH(ixi^l, ixo^l, w, x, ne, nh_arr)
1047 call radiative_cooling_rho2_factor(ixi^l,ixo^l,w,x,fl,rho2_factor)
1048
1049 {do ix^db = ixo^lim^db\}
1050 ! Determine explicit cooling
1051 if(te(ix^d) <= fl%tcoolmin) then
1052 l1 = zero
1053 else if(te(ix^d) >= fl%tcoolmax)then
1054 call calc_l_extended(te(ix^d),l1,fl)
1055 l1 = l1*ne(ix^d)*nh_arr(ix^d)
1056 else
1057 call findl(te(ix^d),l1,fl)
1058 l1 = l1*ne(ix^d)*nh_arr(ix^d)
1059 end if
1060 if(slab_uniform .and. fl%rad_damp .and. x(ix^d,ndim) .le. fl%rad_damp_height) then
1061 l1 = l1*exp(-(x(ix^d,ndim)-fl%rad_damp_height)**2/fl%rad_damp_scale**2)
1062 end if
1063 call radiative_cooling_taper(ix^d, x(ix^d,ndim), rho(ix^d), te(ix^d), rfactor(ix^d), &
1064 fl, taper)
1065 l1 = l1 * taper
1066 coolrate(ix^d) = l1*rho2_factor(ix^d)
1067 {end do\}
1068 end subroutine getvar_cooling
1069
1070 subroutine getvar_cooling_exact(qdt, ixI^L, ixO^L, wCT, w, x, coolrate, fl)
1071 ! Calculates cooling rate using the exact cooling method,
1073
1074 integer, intent(in) :: ixI^L, ixO^L
1075 double precision, intent(in) :: qdt, x(ixI^S, 1:ndim), wCT(ixI^S, 1:nw)
1076 double precision :: w(ixI^S, 1:nw)
1077 double precision, intent(out) :: coolrate(ixI^S)
1078 type(rc_fluid), intent(in) :: fl
1079 double precision :: y1, y2, l1, tlocal2
1080 double precision :: Te(ixI^S), pnew(ixI^S), rho(ixI^S), rhonew(ixI^S)
1081 double precision :: emin, Lmax, fact, Rfactor(ixI^S), pth(ixI^S)
1082 double precision :: ne(ixI^S), nH_arr(ixI^S), rho2_factor(ixI^S)
1083 double precision :: taper
1084 ! LTE+IonE variables
1085 double precision :: nH_val, log_nH, log_p_nH
1086 double precision :: eint_current
1087 double precision :: y_l, T_l
1088 integer :: ix^D
1089
1090 call fl%get_pthermal(wct, x, ixi^l, ixo^l, pth)
1091 call fl%get_rho(wct, x, ixi^l, ixo^l, rho)
1092 call fl%get_var_Rfactor(wct,x,ixi^l,ixo^l,rfactor)
1093 call fl%get_Te(wct, x, ixi^l, ixo^l, te)
1094 call fl%get_ne_nH(ixi^l, ixo^l, wct, x, ne, nh_arr)
1095 call radiative_cooling_rho2_factor(ixi^l,ixo^l,wct,x,fl,rho2_factor)
1096 ! Te(ixO^S)=pth(ixO^S)/(rho(ixO^S)*Rfactor(ixO^S))
1097
1098 call fl%get_pthermal(w, x, ixi^l, ixo^l, pnew)
1099 call fl%get_rho(w, x, ixi^l, ixo^l, rhonew)
1100
1101 fact=fl%lref*qdt/fl%tref
1102
1103 {do ix^db = ixo^lim^db\}
1104 emin = rhonew(ix^d) * fl%tlow * rfactor(ix^d) * invgam
1105 if (fl%ionE) then
1106 nh_val = rhonew(ix^d) / fl%nH2rhoFactor
1107 log_nh = dlog10(nh_val)
1108 ! the analytic route keeps the ideal floor energy, the tables include ionisation
1109 if (fl%method_analytic) then
1110 t_l = te(ix^d)
1111 y_l = wct(ix^d, iw_ne) / nh_val
1112 eint_current = fl%inv_gamma_minus_1 * (1.0d0 + y_l) * nh_val * t_l &
1113 + y_l * fl%eion_per_nH * nh_val
1114 else
1115 emin = rc_eint_floor(fl, log_nh, nh_val)
1116 log_p_nh = dlog10(pnew(ix^d) / nh_val)
1117 eint_current = pnew(ix^d) * fl%p2eint(log_nh, log_p_nh)
1118 end if
1119 lmax = max(zero, (eint_current - emin) / qdt)
1120 else
1121 lmax = max(zero, ( pnew(ix^d)*invgam - emin ) / qdt)
1122 end if
1123
1124 ! No cooling if temperature is below floor level.
1125 ! Assuming Bremsstrahlung if temperature is higher than maximum.
1126 if( te(ix^d)<= fl%tcoolmin) then
1127 l1 = zero
1128 else if( te(ix^d)>= fl%tcoolmax ) then
1129 call calc_l_extended(te(ix^d), l1, fl)
1130 if (fl%lambda_needs_nenh_table) then
1131 l1 = l1 * nh_arr(ix^d) * nh_arr(ix^d)
1132 else
1133 l1 = l1 * ne(ix^d) * nh_arr(ix^d)
1134 end if
1135 l1 = min(l1*rho2_factor(ix^d), lmax)
1136 else
1137 !> Always classical Townsend first. Upgrade to Y_mod only where
1138 !> ionisation buffering matters (large dT, recombination zone)
1139 call findy(te(ix^d), y1, fl)
1140 if (fl%lambda_needs_nenh_table) then
1141 y2 = y1 + rho2_factor(ix^d)*fact * nh_arr(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1142 / (rho(ix^d) * rfactor(ix^d))
1143 else
1144 y2 = y1 + rho2_factor(ix^d)*fact * ne(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1145 / (rho(ix^d) * rfactor(ix^d))
1146 end if
1147 call findt(tlocal2, y2, fl)
1148
1149 if (fl%Y_mod_built .and. rc_variable_cv(fl, te(ix^d), tlocal2)) then
1150 y1 = findy_mod(te(ix^d), nh_arr(ix^d), fl)
1151 if (y1 == y1 .and. abs(y1) < huge(1.0d0)) then
1152 y2 = y1 + rho2_factor(ix^d)*qdt
1153 tlocal2 = findt_mod(y2, nh_arr(ix^d), fl)
1154 end if
1155 end if
1156
1157 if( tlocal2 <= fl%tcoolmin ) then
1158 l1 = lmax
1159 else if (rc_variable_cv(fl, te(ix^d), tlocal2)) then
1160 !> Recombination zone: table-based de for variable c_V.
1161 l1 = ((fl%eint_from_T(log_nh, dlog10(te(ix^d))) &
1162 - fl%eint_from_T(log_nh, dlog10(tlocal2))) * nh_val) / qdt
1163 l1 = max(l1, zero)
1164 else
1165 !> Saturated y or non-ionE: identical Townsend kinetic form.
1166 l1 = (te(ix^d)- tlocal2)*rho(ix^d)*rfactor(ix^d)*invgam/qdt
1167 end if
1168 l1 = min(l1, lmax)
1169 end if
1170 call radiative_cooling_taper(ix^d, x(ix^d,ndim), rho(ix^d), te(ix^d), rfactor(ix^d), &
1171 fl, taper)
1172 l1 = l1 * taper
1173 if(slab_uniform .and. fl%rad_damp .and. x(ix^d,ndim) .le. fl%rad_damp_height) then
1174 l1 = l1*exp(-(x(ix^d,ndim)-fl%rad_damp_height)**2/fl%rad_damp_scale**2)
1175 end if
1176 coolrate(ix^d) = l1
1177 {end do\}
1178 end subroutine getvar_cooling_exact
1179
1180 subroutine radiative_cooling_add_source(qdt,ixI^L,ixO^L,wCT,wCTprim,w,x,&
1181 qsourcesplit,active,fl)
1182 ! w[iw]=w[iw]+qdt*S[wCT,x] where S is the source based on wCT within ixO
1184 integer, intent(in) :: ixI^L, ixO^L
1185 double precision, intent(in) :: qdt, x(ixI^S,1:ndim), wCT(ixI^S,1:nw), wCTprim(ixI^S,1:nw)
1186 double precision, intent(inout) :: w(ixI^S,1:nw)
1187 logical, intent(in) :: qsourcesplit
1188 logical, intent(inout) :: active
1189 type(rc_fluid), intent(in) :: fl
1190 double precision, allocatable, dimension(:^D&) :: Lequi
1191 double precision :: lb_t0_cool
1192
1193 if (lb_diagnose) lb_t0_cool = mpi_wtime()
1194 if(qsourcesplit .eqv.fl%rc_split) then
1195 active = .true.
1196 call cool_exact(qdt,ixi^l,ixo^l,wct,wctprim,w,x,fl)
1197 if(fl%subtract_equi) then
1198 allocate(lequi(ixi^s))
1199 call get_cool_equi(qdt,ixi^l,ixo^l,wct,w,x,fl,lequi)
1200 w(ixo^s,fl%e_) = w(ixo^s,fl%e_)+lequi(ixo^s)
1201 deallocate(lequi)
1202 endif
1203 if( fl%Tfix ) call floortemperature(qdt,ixi^l,ixo^l,wct,w,x,fl)
1204 end if
1205 if (lb_diagnose) lb_cool_accum = lb_cool_accum + (mpi_wtime() - lb_t0_cool)
1206 end subroutine radiative_cooling_add_source
1207
1208 subroutine floortemperature(qdt,ixI^L,ixO^L,wCT,w,x,fl)
1209 ! Force minimum temperature to a fixed temperature
1211 integer, intent(in) :: ixI^L, ixO^L
1212 double precision, intent(in) :: qdt, x(ixI^S,1:ndim), wCT(ixI^S,1:nw)
1213 double precision, intent(inout) :: w(ixI^S,1:nw)
1214 type(rc_fluid), intent(in) :: fl
1215 double precision :: etherm(ixI^S), rho(ixI^S), Rfactor(ixI^S),emin
1216 double precision :: nH_val, log_nH, eint_floor, eint_current
1217 integer :: ix^D
1218
1219 call fl%get_pthermal(w,x,ixi^l,ixo^l,etherm)
1220 call fl%get_rho(w,x,ixi^l,ixo^l,rho)
1221 call fl%get_var_Rfactor(wct,x,ixi^l,ixo^l,rfactor)
1222 {do ix^db = ixo^lim^db\}
1223 emin = rho(ix^d)*fl%tlow*rfactor(ix^d)
1224 if(etherm(ix^d) < emin) then
1225 if (fl%ionE .and. .not. fl%method_analytic) then
1226 ! energy to reach the floor temperature from the tables, ionisation included
1227 nh_val = rho(ix^d) / fl%nH2rhoFactor
1228 log_nh = dlog10(nh_val)
1229 eint_floor = rc_eint_floor(fl, log_nh, nh_val)
1230 eint_current = etherm(ix^d) * fl%p2eint(log_nh, dlog10(etherm(ix^d)/nh_val))
1231 w(ix^d,fl%e_)=w(ix^d,fl%e_)+max(eint_floor-eint_current, zero)
1232 else
1233 w(ix^d,fl%e_)=w(ix^d,fl%e_)+(emin-etherm(ix^d))*invgam
1234 end if
1235 end if
1236 {end do\}
1237 end subroutine floortemperature
1238
1239 !> TRAC factor (T/Tcoff)^(5/2) of the losses below Tcoff, and 1 without TRAC or where a user
1240 !> TRAC mask switches the scaling off
1241 double precision function rc_trac_factor(fl, ix^D, Te_val)
1243 type(rc_fluid), intent(in) :: fl
1244 integer, intent(in) :: ix^d
1245 double precision, intent(in) :: te_val
1246 rc_trac_factor = 1.0d0
1247 if(.not. phys_trac) return
1248 if(fl%iw_tracmask_ > 0) then
1249 if(block%wextra(ix^d, fl%iw_tracmask_) <= 0.5d0) return
1250 end if
1251 if(te_val < block%wextra(ix^d,fl%Tcoff_)) &
1252 rc_trac_factor = sqrt((te_val/block%wextra(ix^d,fl%Tcoff_))**5)
1253 end function rc_trac_factor
1254
1255 !> True where ionisation buffers the cooling step, so that c_V varies: a large change of T
1256 !> below the temperature above which c_V is constant
1257 logical function rc_variable_cv(fl, T1, T2)
1258 type(rc_fluid), intent(in) :: fl
1259 double precision, intent(in) :: t1, t2
1260 rc_variable_cv = fl%ionE .and. dabs(t1 - t2) > 1.0d-4 * t1 .and. t1 < fl%T_Ymod_max
1261 end function rc_variable_cv
1262
1263 !> Internal energy per volume at the floor temperature tlow, ionisation included
1264 double precision function rc_eint_floor(fl, log_nH, nH_val)
1265 type(rc_fluid), intent(in) :: fl
1266 double precision, intent(in) :: log_nh, nh_val
1267 rc_eint_floor = fl%eint_from_T(log_nh, max(dlog10(fl%tlow), fl%log_T_floor)) * nh_val
1268 end function rc_eint_floor
1269
1270 subroutine radiative_cooling_taper(ix^D, x_ndim, rho_val, Te_val, Rf_val, fl, factor)
1271 !> Compute multiplicative taper factor for radiative cooling.
1272 !> Returns 1.0 when no tapering applies; < 1.0 where a user mask or the SC transfer takes
1273 !> the losses, near boundaries, at high density, or in optically thick regions
1275 integer, intent(in) :: ix^D
1276 double precision, intent(in) :: x_ndim, rho_val, Te_val, Rf_val
1277 type(rc_fluid), intent(in) :: fl
1278 double precision, intent(out) :: factor
1279 double precision :: d_boundary, tau, kappa_local
1280
1281 factor = 1.0d0
1282
1283 if(fl%iw_mask_ > 0) then
1284 ! the mask is a weight in [0, 1], so an undershoot below zero switches the losses off
1285 factor = max(0.0d0, block%wextra(ix^d, fl%iw_mask_))
1286 if(factor == 0.0d0) return
1287 end if
1288
1289 ! thin losses only where the SC RT does not take over; rt_sc_thin_weight is the one
1290 ! definition of the split, so thin + SC = 1, and it is 1 outside the RT bands
1291 factor = factor * rt_sc_thin_weight(te_val, x_ndim, rho_val*te_val*rf_val)
1292
1293 ! Spatial + density taper
1294 if(slab_uniform .and. fl%rad_modify) then
1295 ! Spatial taper: distance from nearest relevant boundary
1296 if(fl%rad_modify_sym) then
1297 d_boundary = min(x_ndim - xprobmin^nd, xprobmax^nd - x_ndim)
1298 else
1299 d_boundary = x_ndim - xprobmin^nd
1300 end if
1301 if(d_boundary .le. fl%rad_cut_hgt) then
1302 if(fl%suppress_temp_code > 0.0d0) then
1303 ! Temperature suppression: kill cooling for T below threshold.
1304 ! Coronal cooling above threshold proceeds unmodified.
1305 if(te_val .lt. fl%suppress_temp_code) then
1306 factor = 0.0d0
1307 return
1308 end if
1309 else
1310 ! Standard: Gaussian taper on ALL cooling near boundary
1311 factor = factor * exp(-((d_boundary - fl%rad_cut_hgt) / fl%rad_cut_dey)**2)
1312 end if
1313 end if
1314
1315 ! Density taper
1316 if(rho_val .gt. fl%rad_taper_rho) then
1317 factor = factor * exp(-((rho_val - fl%rad_taper_rho) / fl%rad_taper_dey)**2)
1318 end if
1319 end if
1320
1321 ! Escape probability: cooling suppression by optical depth
1322 ! kappa(T) = kappa_0 / (1 + (T/T_cutoff)^alpha) - sigmoid cutoff
1323 if(fl%rad_escape_prob .and. fl%iw_colmass_ > 0) then
1324 kappa_local = fl%rad_kappa_eff
1325 if(fl%rad_kappa_Tcutoff > 0.0d0) then
1326 kappa_local = kappa_local &
1327 / (1.0d0 + (te_val / fl%rad_kappa_Tcutoff)**fl%rad_kappa_alpha)
1328 end if
1329 tau = kappa_local * block%wextra(ix^d, fl%iw_colmass_)
1330 if(tau > 1.0d-6) then
1331 select case(fl%rad_escape_type)
1332 case('slab')
1333 ! Plane-parallel slab: beta(tau) = (1 - exp(-tau))/tau
1334 factor = factor * (1.0d0 - exp(-tau)) / tau
1335 case('voigt')
1336 ! Frequency-integrated escape from a truncated Voigt profile
1337 ! (CL12 Sec 2.1). Precomputed lookup table; see voigt_escape_init_table.
1338 factor = factor * voigt_escape_lookup(tau)
1339 case default
1340 call mpistop("Unknown rad_escape_type: use 'slab' or 'voigt'")
1341 end select
1342 ! Exponential cutoff at large tau: kills residual cooling
1343 ! where rho^2 outpaces the escape function decay
1344 if(fl%rad_escape_tau_cutoff > 0.0d0) then
1345 factor = factor * exp(-tau / fl%rad_escape_tau_cutoff)
1346 end if
1347 end if
1348 end if
1349 end subroutine radiative_cooling_taper
1350
1351 subroutine get_cool_equi(qdt,ixI^L,ixO^L,wCT,w,x,fl,res)
1353
1354 integer, intent(in) :: ixI^L, ixO^L
1355 double precision, intent(in) :: qdt, x(ixI^S,1:ndim), wCT(ixI^S,1:nw)
1356 double precision, intent(inout) :: w(ixI^S,1:nw)
1357 type(rc_fluid), intent(in) :: fl
1358 double precision, intent(out) :: res(ixI^S)
1359
1360 double precision :: pth(ixI^S),rho(ixI^S),Rfactor(ixI^S),L1,Tlocal2
1361 double precision :: Te(ixI^S)
1362 double precision :: emin, Lmax
1363 double precision :: Y1, Y2
1364 double precision :: de, emax,fact
1365 double precision :: ne(ixI^S), nH_arr(ixI^S), rho2_factor(ixI^S)
1366 double precision :: taper
1367 ! LTE+IonE variables
1368 double precision :: nH_val, log_nH, log_p_nH
1369 double precision :: eint_current
1370 double precision :: y_l, T_l
1371 integer :: ix^D
1372
1373 call fl%get_pthermal_equi(wct,x,ixi^l,ixo^l,pth)
1374 call fl%get_rho_equi(wct,x,ixi^l,ixo^l,rho)
1375 call fl%get_var_Rfactor(wct,x,ixi^l,ixo^l,rfactor)
1376 ! Te(ixO^S)=pth(ixO^S)/(rho(ixO^S)*Rfactor(ixO^S))
1377 ! temperature and densities of the background alone, to match pth and rho
1378 call fl%get_temperature_equi(wct,x,ixi^l,ixo^l,te)
1379 call fl%get_ne_nH_equi(ixi^l, ixo^l, wct, x, ne, nh_arr)
1380 call radiative_cooling_rho2_factor(ixi^l,ixo^l,wct,x,fl,rho2_factor)
1381
1382 res=0d0
1383
1384 fact = fl%lref*qdt/fl%tref
1385 {do ix^db = ixo^lim^db\}
1386 emin = rho(ix^d)*fl%tlow*rfactor(ix^d)*invgam
1387 if (fl%ionE) then
1388 nh_val = rho(ix^d) / fl%nH2rhoFactor
1389 log_nh = dlog10(nh_val)
1390 ! the analytic route keeps the ideal floor energy, the tables include ionisation
1391 if (fl%method_analytic) then
1392 t_l = te(ix^d)
1393 y_l = wct(ix^d, iw_ne) / nh_val
1394 eint_current = 1.5d0 * (1.0d0 + y_l) * nh_val * t_l &
1395 + y_l * fl%eion_per_nH * nh_val
1396 else
1397 emin = rc_eint_floor(fl, log_nh, nh_val)
1398 log_p_nh = dlog10(pth(ix^d) / nh_val)
1399 eint_current = pth(ix^d) * fl%p2eint(log_nh, log_p_nh)
1400 end if
1401 lmax = max(zero, (eint_current - emin) / qdt)
1402 emax = max(zero, eint_current - emin)
1403 else
1404 lmax = max(zero,(pth(ix^d)*invgam-emin)/qdt)
1405 emax = max(zero, pth(ix^d)*invgam-emin)
1406 end if
1407 ! Determine explicit cooling
1408 ! If temperature is below floor level, no cooling.
1409 ! Stop wasting time and go to next gridpoint.
1410 ! If the temperature is higher than the maximum,
1411 ! assume Bremsstrahlung
1412 if( te(ix^d)<=fl%tcoolmin ) then
1413 ! res already initialised to 0d0 above; no cooling
1414 else if( te(ix^d)>=fl%tcoolmax )then
1415 call calc_l_extended(te(ix^d), l1,fl)
1416 if (fl%lambda_needs_nenh_table) then
1417 l1 = l1 * nh_arr(ix^d) * nh_arr(ix^d)
1418 else
1419 l1 = l1 * ne(ix^d) * nh_arr(ix^d)
1420 end if
1421 l1 = l1*rc_trac_factor(fl, ix^d, te(ix^d))
1422 l1 = min(rho2_factor(ix^d)*l1,lmax)
1423 res(ix^d) = l1*qdt
1424 else
1425 !> Always classical Townsend first. Upgrade to Y_mod only in the
1426 !> recombination zone (large dT) - saturated y uses the identical
1427 !> formula as ionE=false, removing per-substep asymmetry.
1428 call findy(te(ix^d),y1,fl)
1429 if (fl%lambda_needs_nenh_table) then
1430 y2 = y1 + rho2_factor(ix^d)*fact * nh_arr(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1431 / (rho(ix^d) * rfactor(ix^d))
1432 else
1433 y2 = y1 + rho2_factor(ix^d)*fact * ne(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1434 / (rho(ix^d) * rfactor(ix^d))
1435 end if
1436 call findt(tlocal2,y2,fl)
1437
1438 if (fl%Y_mod_built .and. rc_variable_cv(fl, te(ix^d), tlocal2)) then
1439 y1 = findy_mod(te(ix^d), nh_arr(ix^d), fl)
1440 if (y1 == y1 .and. abs(y1) < huge(1.0d0)) then
1441 y2 = y1 + rho2_factor(ix^d)*qdt
1442 tlocal2 = findt_mod(y2, nh_arr(ix^d), fl)
1443 end if
1444 end if
1445
1446 if(tlocal2<=fl%tcoolmin) then
1447 de = emax
1448 else if (rc_variable_cv(fl, te(ix^d), tlocal2)) then
1449 de = (fl%eint_from_T(log_nh, dlog10(te(ix^d))) &
1450 - fl%eint_from_T(log_nh, dlog10(tlocal2))) * nh_val
1451 de = max(de, zero)
1452 else
1453 de = (te(ix^d)-tlocal2)*rho(ix^d)*rfactor(ix^d)*invgam
1454 end if
1455 de = de*rc_trac_factor(fl, ix^d, te(ix^d))
1456 de = min(de,emax)
1457 res(ix^d) = de
1458 end if
1459 call radiative_cooling_taper(ix^d, x(ix^d,ndim), rho(ix^d), te(ix^d), rfactor(ix^d), &
1460 fl, taper)
1461 res(ix^d) = res(ix^d) * taper
1462 {end do\}
1463 end subroutine get_cool_equi
1464
1465 subroutine cool_exact(qdt,ixI^L,ixO^L,wCT,wCTprim,w,x,fl)
1466 ! Cooling routine using exact integration method from Townsend 2009
1468 use mod_physics, only: phys_get_ei
1469 integer, intent(in) :: ixI^L, ixO^L
1470 double precision, intent(in) :: qdt, x(ixI^S,1:ndim), wCT(ixI^S,1:nw), wCTprim(ixI^S,1:nw)
1471 double precision, intent(inout) :: w(ixI^S,1:nw)
1472 type(rc_fluid), intent(in) :: fl
1473 double precision :: Y1, Y2
1474 double precision :: L1, pth(ixI^S), Tlocal2, pnew(ixI^S)
1475 double precision :: rho(ixI^S), Te(ixI^S), rhonew(ixI^S), Rfactor(ixI^S)
1476 double precision :: emin, Lmax, fact
1477 double precision :: de, emax
1478 double precision :: ne(ixI^S), nH_arr(ixI^S), rho2_factor(ixI^S)
1479 double precision :: taper
1480 ! LTE+IonE variables
1481 double precision :: nH_val, log_nH
1482 double precision :: eint_w(ixI^S) ! actual internal energy from conserved state
1483 double precision :: de_thin, de_thick, emax_rem
1484 double precision :: T1, T2, p1(ixI^S), tau, xi
1485 double precision :: xi_arr(ixI^S), emax_rem_arr(ixI^S)
1486 double precision :: cool_fac, fip_prim, frac_lowFIP, fip_factor
1487 integer :: ix^D
1488
1489 call fl%get_rho(wct,x,ixi^l,ixo^l,rho)
1490 call fl%get_var_Rfactor(wct,x,ixi^l,ixo^l,rfactor)
1491 call fl%get_Te(wct,x,ixi^l,ixo^l,te)
1492 call fl%get_ne_nH(ixi^l, ixo^l, wct, x, ne, nh_arr)
1493 call radiative_cooling_rho2_factor(ixi^l,ixo^l,wct,x,fl,rho2_factor)
1494 call fl%get_pthermal(w,x,ixi^l,ixo^l,pnew)
1495 call fl%get_rho(w,x,ixi^l,ixo^l,rhonew)
1496 if (fl%ionE) eint_w(ixo^s) = phys_get_ei(w, ixi^l, ixo^l)
1497
1498 fact = fl%lref*qdt/fl%tref
1499
1500 xi_arr = one
1501 emax_rem_arr = zero
1502 {do ix^db = ixo^lim^db\}
1503 emin = rhonew(ix^d)*fl%tlow*rfactor(ix^d)*invgam
1504 if (fl%ionE) then
1505 ! LTE+IonE: EoS quantities for Y-advance; cap from conserved state
1506 nh_val = rhonew(ix^d) / fl%nH2rhoFactor
1507 log_nh = dlog10(nh_val)
1508 ! eint_w includes the ionisation energy, so the floor does too, as in floortemperature;
1509 ! the analytic route keeps the ideal floor energy
1510 if (.not. fl%method_analytic) emin = rc_eint_floor(fl, log_nh, nh_val)
1511 lmax = max(zero, eint_w(ix^d) - emin) / qdt
1512 emax = max(zero, eint_w(ix^d) - emin)
1513 else
1514 lmax = max(zero,pnew(ix^d)*invgam-emin)/qdt
1515 emax = max(zero,pnew(ix^d)*invgam-emin)
1516 end if
1517
1518 ! Skip cells below cooling floor: no de_thin contribution, no rad_newton thick step queued.
1519 if (te(ix^d) <= fl%tcoolmin) cycle
1520
1521 ! Multiplicative cool_fac (upstream): xi (optically thick Newton) * FIP * geometric damping.
1522 ! Multiplied INTO the integration (Y2 increment / extended-Bremsstrahlung L1) so the
1523 ! Townsend EI mapping sees the reduced Lambda_eff = cool_fac * Lambda.
1524 if (fl%rad_newton) then
1525 xi = exp(-pnew(ix^d) / fl%rad_newton_pthick)
1526 xi = min(max(xi, zero), one)
1527 else
1528 xi = one
1529 end if
1530 cool_fac = xi*rho2_factor(ix^d)
1531
1532 if (fl%fip_ > 0) then
1533 fip_prim = min(maxfip, max(minfip, wctprim(ix^d,fl%fip_)))
1534 ! frac_lowFIP(T) in [0,1]: low-FIP contribution to total Lambda at T
1535 frac_lowfip = lowfip_fraction(te(ix^d), fl)
1536 fip_factor = one - frac_lowfip + fip_prim * frac_lowfip
1537 cool_fac = cool_fac * fip_factor
1538 end if
1539
1540 ! Geometric (Gaussian) damping near lower boundary (chromosphere/photosphere).
1541 if (slab_uniform .and. fl%rad_damp .and. x(ix^d,ndim) <= xprobmin1 + fl%rad_damp_height) then
1542 cool_fac = cool_fac * exp(-(x(ix^d,ndim)-xprobmin1-fl%rad_damp_height)**2/fl%rad_damp_scale**2)
1543 end if
1544 {^ifoned
1545 if (slab_uniform .and. fl%rad_damp .and. x(ix^d,ndim) >= xprobmax1 - fl%rad_damp_height) then
1546 cool_fac = cool_fac * exp(-(x(ix^d,ndim)-xprobmax1+fl%rad_damp_height)**2/fl%rad_damp_scale**2)
1547 end if
1548 }
1549
1550 if( te(ix^d)>=fl%tcoolmax )then
1551 call calc_l_extended(te(ix^d), l1,fl)
1552 if (fl%lambda_needs_nenh_table) then
1553 l1 = l1 * nh_arr(ix^d) * nh_arr(ix^d)
1554 else
1555 l1 = l1 * ne(ix^d) * nh_arr(ix^d)
1556 end if
1557 l1 = cool_fac * l1
1558 l1 = l1*rc_trac_factor(fl, ix^d, te(ix^d))
1559 l1 = min(l1,lmax)
1560 call radiative_cooling_taper(ix^d, x(ix^d,ndim), rho(ix^d), te(ix^d), rfactor(ix^d), &
1561 fl, taper)
1562 l1 = l1 * taper
1563 de_thin = l1 * qdt
1564 w(ix^d,fl%e_) = w(ix^d,fl%e_) - de_thin
1565 else
1566 !> Always compute the CLASSICAL Townsend advance first. This gives
1567 !> Tlocal2 using the same path as ionE=false. If the resulting dT
1568 !> is small (saturated y region), the classical kinetic form is
1569 !> exact and we're done. If dT is large (recombination zone), we
1570 !> redo the advance with the Y_mod path to capture variable-c_V
1571 !> dynamics from ionisation buffering.
1572 call findy(te(ix^d),y1,fl)
1573 if (fl%lambda_needs_nenh_table) then
1574 y2 = y1 + cool_fac * fact * nh_arr(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1575 / (rho(ix^d) * rfactor(ix^d))
1576 else
1577 y2 = y1 + cool_fac * fact * ne(ix^d) * nh_arr(ix^d) * rc_gamma_1 &
1578 / (rho(ix^d) * rfactor(ix^d))
1579 end if
1580 call findt(tlocal2,y2,fl)
1581
1582 !> Upgrade to Y_mod only in the recombination zone (large dT).
1583 !> Guard: if findY_mod overflows (cooling curve near-zero at this T),
1584 !> keep the classical result -- variable c_V is irrelevant where Lambda ~ 0.
1585 if (fl%Y_mod_built .and. rc_variable_cv(fl, te(ix^d), tlocal2)) then
1586 y1 = findy_mod(te(ix^d), nh_arr(ix^d), fl)
1587 if (y1 == y1 .and. abs(y1) < huge(1.0d0)) then
1588 y2 = y1 + cool_fac * qdt
1589 tlocal2 = findt_mod(y2, nh_arr(ix^d), fl)
1590 end if
1591 end if
1592
1593 if(tlocal2<=fl%tcoolmin) then
1594 de = emax
1595 else if (rc_variable_cv(fl, te(ix^d), tlocal2)) then
1596 !> Recombination zone: use table-based de for variable c_V.
1597 de = (fl%eint_from_T(log_nh, dlog10(te(ix^d))) &
1598 - fl%eint_from_T(log_nh, dlog10(tlocal2))) * nh_val
1599 de = max(de, zero)
1600 else
1601 !> Saturated y or non-ionE: classical Townsend kinetic form.
1602 de = (te(ix^d)-tlocal2)*rho(ix^d)*rfactor(ix^d)*invgam
1603 end if
1604 de = de*rc_trac_factor(fl, ix^d, te(ix^d))
1605 de = min(de,emax)
1606 call radiative_cooling_taper(ix^d, x(ix^d,ndim), rho(ix^d), te(ix^d), rfactor(ix^d), &
1607 fl, taper)
1608 de = de * taper
1609 de_thin = de
1610 w(ix^d,fl%e_) = w(ix^d,fl%e_) - de_thin
1611 end if
1612
1613 ! Queue remaining energy budget for the optically thick step (upstream rad_newton).
1614 if (fl%rad_newton) then
1615 xi_arr(ix^d) = xi
1616 emax_rem_arr(ix^d) = max(zero, emax - de_thin)
1617 end if
1618 {end do\}
1619
1620 ! ------- (B) OPTICALLY-THICK (NEWTON) PART --------
1621 if (fl%rad_newton) then
1622 call fl%get_pthermal(w, x, ixi^l, ixo^l, p1)
1623 {do ix^db = ixo^lim^db\}
1624 t1 = p1(ix^d) / (rho(ix^d) * rfactor(ix^d))
1625 tau = max(0.1d0 * sqrt( fl%rad_newton_rhosurf / rho(ix^d)), 4.d0 * qdt)
1626 t2 = fl%rad_newton_trad + (t1 - fl%rad_newton_trad) * exp(-qdt / tau)
1627 de_thick = min((one - xi_arr(ix^d)) * (t1 - t2) * rho(ix^d) * rfactor(ix^d) * invgam, emax_rem_arr(ix^d))
1628 w(ix^d,fl%e_) = w(ix^d,fl%e_) - de_thick
1629 {end do\}
1630 end if
1631 end subroutine cool_exact
1632
1633 subroutine calc_l_extended (tpoint, lpoint,fl)
1634 ! Calculate l for t beyond tcoolmax
1635 ! Assumes Bremsstrahlung for the interpolated tables
1636 ! Uses the power law for piecewise power laws
1637 double precision, intent(IN) :: tpoint
1638 double precision, intent(OUT) :: lpoint
1639 type(rc_fluid), intent(in) :: fl
1640
1641 if(fl%isPPL) then
1642 lpoint =fl%l_PPL(fl%n_PPL) * ( tpoint / fl%t_PPL(fl%n_PPL) )**fl%a_PPL(fl%n_PPL)
1643 else
1644 lpoint = fl%Lcool(fl%ncool) * sqrt( tpoint / fl%tcoolmax)
1645 end if
1646 end subroutine calc_l_extended
1647
1648 double precision function lowfip_fraction(tpoint, fl)
1650
1651 double precision, intent(in) :: tpoint
1652 type(rc_fluid), intent(in) :: fl
1653
1654 double precision :: lgtp
1655 integer :: jl
1656
1657 if (tpoint <= fl%tcool(1)) then
1658 lowfip_fraction = fl%frac_lowFIP(1)
1659 return
1660 else if (tpoint >= fl%tcool(fl%ncool)) then
1661 lowfip_fraction = fl%frac_lowFIP(fl%ncool)
1662 return
1663 end if
1664
1665 lgtp = dlog10(tpoint)
1666 jl = int((lgtp - fl%lgtcoolmin) / fl%lgstep) + 1
1667 jl = max(1, min(fl%ncool-1, jl))
1668
1669 lowfip_fraction = fl%frac_lowFIP(jl) &
1670 + (tpoint - fl%tcool(jl)) &
1671 * (fl%frac_lowFIP(jl+1) - fl%frac_lowFIP(jl)) &
1672 / (fl%tcool(jl+1) - fl%tcool(jl))
1673 end function lowfip_fraction
1674
1675 subroutine findl (tpoint,Lpoint,fl)
1676 ! Fast search option to find correct point
1677 ! in cooling curve
1679
1680 double precision,intent(IN) :: tpoint
1681 double precision, intent(OUT) :: Lpoint
1682 type(rc_fluid), intent(in) :: fl
1683
1684 double precision :: lgtp
1685 integer :: jl,i
1686
1687 if(fl%isPPL) then
1688 i = maxloc(fl%t_PPL, dim=1, mask=fl%t_PPL<tpoint)
1689 lpoint = fl%l_PPL(i) * (tpoint / fl%t_PPL(i))**fl%a_PPL(i)
1690 else
1691 lgtp = dlog10(tpoint)
1692 jl = int((lgtp - fl%lgtcoolmin) /fl%lgstep) + 1
1693 lpoint = fl%Lcool(jl)+ (tpoint-fl%tcool(jl)) &
1694 * (fl%Lcool(jl+1)-fl%Lcool(jl)) &
1695 / (fl%tcool(jl+1)-fl%tcool(jl))
1696 end if
1697
1698 end subroutine findl
1699
1700 subroutine findy (tpoint,Ypoint,fl)
1701 ! Fast search option to find correct point in cooling time
1703
1704 double precision,intent(IN) :: tpoint
1705 double precision, intent(OUT) :: Ypoint
1706 type(rc_fluid), intent(in) :: fl
1707
1708 double precision :: lgtp
1709 double precision :: y_extra,factor
1710 integer :: jl,i
1711
1712 if(fl%isPPL) then
1713 i = maxloc(fl%t_PPL, dim=1, mask=fl%t_PPL<tpoint)
1714 factor = fl%l_PPL(fl%n_PPL+1) * fl%t_PPL(i) / (fl%l_PPL(i) * fl%t_PPL(fl%n_PPL+1))
1715 if(fl%a_PPL(i)==1.d0) then
1716 y_extra = log( fl%t_PPL(i) / tpoint )
1717 else
1718 y_extra = 1 / (1 - fl%a_PPL(i)) * (1 - ( fl%t_PPL(i) / tpoint )**(fl%a_PPL(i)-1) )
1719 end if
1720 ypoint = fl%y_PPL(i) + factor*y_extra
1721 else
1722 lgtp = dlog10(tpoint)
1723 jl = int((lgtp - fl%lgtcoolmin) / fl%lgstep) + 1
1724 ! Bounds check: jl must satisfy 1 <= jl <= ncool-1 so jl+1 <= ncool
1725 if(jl < 1 .or. jl >= fl%ncool) then
1726 write(*,'(a,es14.6,a,i0,a,2es14.6)') &
1727 'findY: tpoint=',tpoint,' jl=',jl,' out of bounds [1,ncool-1]; tcoolmin/max=', &
1728 fl%tcoolmin,fl%tcoolmax
1729 call mpistop('findY: temperature index out of bounds')
1730 end if
1731 ypoint = fl%Yc(jl)+ (tpoint-fl%tcool(jl)) &
1732 * (fl%Yc(jl+1)-fl%Yc(jl)) &
1733 / (fl%tcool(jl+1)-fl%tcool(jl))
1734 end if
1735
1736 end subroutine findy
1737
1738 subroutine findt (tpoint,Ypoint,fl)
1739 ! Fast search option to find correct temperature
1740 ! from temporal evolution function. Only possible this way because T is a monotonously
1741 ! decreasing function for the interpolated tables
1742 ! Uses eq. A7 from Townsend 2009 for piecewise power laws
1744
1745 double precision,intent(OUT) :: tpoint
1746 double precision, intent(IN) :: Ypoint
1747 type(rc_fluid), intent(in) :: fl
1748
1749 double precision :: factor
1750 integer :: jl,jc,jh,i
1751
1752 if(fl%isPPL) then
1753 i = minloc(fl%y_PPL, dim=1, mask=fl%y_PPL>ypoint)
1754 factor = fl%l_PPL(i) * fl%t_PPL(fl%n_PPL+1) / (fl%l_PPL(fl%n_PPL+1) * fl%t_PPL(i))
1755 if(fl%a_PPL(i)==1.d0) then
1756 tpoint = fl%t_PPL(i) * exp( -1.d0 * factor * ( ypoint - fl%y_PPL(i)))
1757 else
1758 tpoint = fl%t_PPL(i) * (1 - (1 - fl%a_PPL(i)) * factor * (ypoint - fl%y_PPL(i)))**(1 / (1 - fl%a_PPL(i)))
1759 end if
1760 else
1761 if(ypoint >= fl%Yc(1)) then
1762 tpoint = fl%tcoolmin
1763 else if (ypoint == fl%Yc(fl%ncool)) then
1764 tpoint = fl%tcoolmax
1765 else
1766 jl=0
1767 jh=fl%ncool+1
1768 do
1769 if(jh-jl <= 1) exit
1770 jc=(jh+jl)/2
1771 if(ypoint <= fl%Yc(jc)) then
1772 jl=jc
1773 else
1774 jh=jc
1775 end if
1776 end do
1777 ! Linear interpolation to obtain correct temperature
1778 tpoint = fl%tcool(jl)+ (ypoint-fl%Yc(jl)) &
1779 * (fl%tcool(jl+1)-fl%tcool(jl)) &
1780 / (fl%Yc(jl+1)-fl%Yc(jl))
1781 end if
1782 end if
1783 end subroutine findt
1784
1785 subroutine finddldt (tpoint,dLpoint,fl)
1786 ! Fast search option to find correct point
1787 ! in derivative of cooling curve
1788 ! Does not work for the piecewise power laws
1790
1791 double precision,intent(IN) :: tpoint
1792 double precision, intent(OUT) :: dLpoint
1793 type(rc_fluid), intent(in) :: fl
1794
1795 double precision :: lgtp
1796 integer :: jl,jc,jh
1797
1798 lgtp = dlog10(tpoint)
1799 jl = int((lgtp -fl%lgtcoolmin) / fl%lgstep) + 1
1800 dlpoint = fl%dLdtcool(jl)+ (tpoint-fl%tcool(jl)) &
1801 * (fl%dLdtcool(jl+1)-fl%dLdtcool(jl)) &
1802 / (fl%tcool(jl+1)-fl%tcool(jl))
1803
1804! if (tpoint == tcoolmin) then
1805! dLpoint = dLdtcool(1)
1806! else if (tpoint == tcoolmax) then
1807! dLpoint = dLdtcool(ncool)
1808! else
1809! jl=0
1810! jh=ncool+1
1811! do
1812! if (jh-jl <= 1) exit
1813! jc=(jh+jl)/2
1814! if (tpoint >= tcool(jc)) then
1815! jl=jc
1816! else
1817! jh=jc
1818! end if
1819! end do
1820! ! Linear interpolation to obtain correct cooling derivative
1821! dLpoint = dLdtcool(jl)+ (tpoint-tcool(jl)) &
1822! * (dLdtcool(jl+1)-dLdtcool(jl)) &
1823! / (tcool(jl+1)-tcool(jl))
1824! end if
1825 end subroutine finddldt
1826
1827 !> ===================================================================
1828 !> Variable-c_V Townsend extension (Y_mod)
1829 !> ===================================================================
1830 !>
1831 !> The original Townsend (2009) exact integration scheme assumes a
1832 !> constant heat capacity c_V = rhoR/(gamma-1). For LTE plasmas with H/He
1833 !> ionisation, c_V is a strong function of temperature in the
1834 !> recombination zone, where ionisation buffers the cooling
1835 !>
1836 !> The derivation defines a modified TEF
1837 !> Ytilde(T; rho) = int _T^{T_ref} c_V(T'; rho) / (n_H n_e(T'; rho) Lambda(T')) dT'
1838 !> with c_V the LTE volumetric heat capacity *including* the
1839 !> ionisation-energy reservoir. The integral is recast via the change
1840 !> of variables u = e_int/n_H so that the discrete construction never
1841 !> finite-differences c_V - both integrand and energy update are
1842 !> driven by the same eint_from_T table, guaranteeing bit-consistent
1843 !> energy conservation.
1844 !>
1845 !> Must be called *after* eos_finalise() so that eos%eint_from_T,
1846 !> eos%T (forward), and eos%neOnH are all in code units. The hook is
1847 !> bind_eos_to_source() in mod_hd_eos.t / mod_mhd_eos.t.
1848 !>
1849 !> Algorithm: change of variables from T to u = e_int/n_H. For each
1850 !> log10 nH grid point j (taken from the eos%eint_from_T table's nH axis):
1851 !> 1. Cache u_i = e_int/n_H at every cooling-curve T_i.
1852 !> 2. Walk i = ncool-1 down to 1 and accumulate
1853 !> Y_mod(j, i) = Y_mod(j, i+1) + int _{u_i}^{u_{i+1}} du / (n_e Lambda)
1854 !> using a composite Simpson (3-point, O(h^4)) or Boole (5-point,
1855 !> O(h^6)) rule with N_sub sub-intervals.
1856 !> 3. The integrand evaluates n_e via y_from_nH_eint and Lambda via findL
1857 !> (with T from T_from_nH_eint).
1858 !>
1859 !> Per-row inverse table T_mod_inv(j, k) is also built for the
1860 !> 'table' inverse method (alternative to bisection).
1861 subroutine build_y_mod_table(fl)
1863 type(rc_fluid), intent(inout) :: fl
1864
1865 integer :: i, j, k, n_nH, ncool, N_sub
1866 double precision :: log_nH_j, nH_j_code, u_lo, u_hi, du_total, du_step
1867 double precision :: u_s, log_u_s, T_s, y_s, ne_s, Lambda_s, integ
1868 double precision, allocatable :: u_at_T(:), f_node(:)
1869 double precision :: Y_max_global, Y_min_global
1870
1871 ! Preconditions for the variable-c_V Townsend extension:
1872 ! 1. LTE with ionisation energy (otherwise c_V is constant and classical
1873 ! Townsend already correct)
1874 ! 2. Townsend exact cooling method (Y-advance is its signature)
1875 ! 3. Tabulated cooling curve, not piecewise power law (PPL has its own
1876 ! y_PPL path and does not allocate fl%tcool / fl%Lcool)
1877 ! 4. eos%eint_from_T table must be built (excludes analytic Saha mode)
1878 ! 5. Build only once per run
1879 if (.not. fl%ionE) return
1880 if (fl%isPPL) return
1881 if (fl%Y_mod_built) return
1882
1883 ! The (log_nH, log_T) inverse-table grid (extents + n_nH) was snapshotted
1884 ! into fl by eos_get_eintT_grid in bind_eos_to_source; n_nH=0 means no such
1885 ! table (analytic/FI). The build below queries fl%eint_from_T which already
1886 ! dispatches on the EoS method, so we only need the grid extents here.
1887 n_nh = fl%Y_mod_n_nH
1888 if (n_nh <= 0) then
1889 if (mype == 0) write(*,*) ' build_Y_mod_table: no (rho,T) inverse table allocated; skipping'
1890 return
1891 end if
1892
1893 ! Validate quadrature option
1894 select case (trim(fl%Y_mod_quadrature))
1895 case ('simpson', 'boole')
1896 ! ok
1897 case default
1898 call mpistop('build_Y_mod_table: rc_Y_mod_quadrature must be simpson or boole')
1899 end select
1900
1901 ncool = fl%ncool
1902 n_sub = max(2, fl%Y_mod_N_sub)
1903 ! For Boole's rule we need N_sub to be a multiple of 4; round up if not.
1904 if (trim(fl%Y_mod_quadrature) == 'boole') then
1905 if (mod(n_sub, 4) /= 0) n_sub = n_sub + (4 - mod(n_sub, 4))
1906 else
1907 ! Simpson needs N_sub even
1908 if (mod(n_sub, 2) /= 0) n_sub = n_sub + 1
1909 end if
1910
1911 fl%Y_mod_n_nH = n_nh
1912 if (n_nh > 1) then
1913 fl%Y_mod_lg_nH_step_inv = dble(n_nh - 1) &
1914 / (fl%Y_mod_lg_nH_max - fl%Y_mod_lg_nH_min)
1915 fl%Y_mod_lg_nH_step = 1.0d0/fl%Y_mod_lg_nH_step_inv
1916 else
1917 fl%Y_mod_lg_nH_step_inv = 0.0d0
1918 fl%Y_mod_lg_nH_step = 0.0d0
1919 end if
1920
1921 allocate(fl%Y_mod(n_nh, ncool))
1922 allocate(fl%Y_mod_max_per_row(n_nh))
1923 allocate(u_at_t(ncool))
1924 allocate(f_node(0:n_sub))
1925
1926 do j = 1, n_nh
1927 log_nh_j = fl%Y_mod_lg_nH_min &
1928 + dble(j - 1) * (fl%Y_mod_lg_nH_max - fl%Y_mod_lg_nH_min) / dble(max(1, n_nh - 1))
1929 nh_j_code = 10.0d0**log_nh_j
1930
1931 ! Cache u_i = e_int/n_H at each cooling-curve temperature
1932 do i = 1, ncool
1933 u_at_t(i) = fl%eint_from_T(log_nh_j, dlog10(fl%tcool(i)))
1934 end do
1935
1936 fl%Y_mod(j, ncool) = 0.0d0
1937
1938 ! Step downward in T, accumulating int du / (n_e Lambda)
1939 do i = ncool - 1, 1, -1
1940 u_lo = u_at_t(i)
1941 u_hi = u_at_t(i + 1)
1942 du_total = u_hi - u_lo
1943 if (du_total <= 0.0d0) then
1944 ! Degenerate segment (shouldn't happen physically); skip
1945 fl%Y_mod(j, i) = fl%Y_mod(j, i + 1)
1946 cycle
1947 end if
1948 du_step = du_total / dble(n_sub)
1949
1950 ! Sample integrand at composite quadrature nodes
1951 do k = 0, n_sub
1952 u_s = u_lo + dble(k) * du_step
1953 if (u_s <= 0.0d0) then
1954 f_node(k) = 0.0d0
1955 cycle
1956 end if
1957 log_u_s = dlog10(u_s)
1958 t_s = fl%T_from_eint(log_nh_j, log_u_s)
1959 if (fl%lambda_needs_nenh_table) then
1960 ! SPEX-style two-table convention: the equilibrium n_e/n_H is
1961 ! already absorbed into Lambda_table at construction time, so
1962 ! the cooling rate is Q = n_H^2 * Lambda_table. The integrand
1963 ! 1/(n_e * Lambda) becomes 1/(n_H * Lambda); equivalently,
1964 ! substitute n_e -> n_H by setting y_s = 1.
1965 y_s = 1.0d0
1966 ne_s = nh_j_code
1967 else
1968 y_s = fl%y_from_eint(log_nh_j, log_u_s)
1969 ne_s = y_s * nh_j_code
1970 end if
1971 if (t_s <= fl%tcoolmin) then
1972 ! Below cooling table: no cooling, so integrand = 0
1973 f_node(k) = 0.0d0
1974 cycle
1975 else if (t_s >= fl%tcoolmax) then
1976 call calc_l_extended(t_s, lambda_s, fl)
1977 else
1978 call findl(t_s, lambda_s, fl)
1979 end if
1980 if (ne_s * lambda_s > 0.0d0) then
1981 f_node(k) = 1.0d0 / (ne_s * lambda_s)
1982 else
1983 f_node(k) = 0.0d0
1984 end if
1985 end do
1986
1987 select case (trim(fl%Y_mod_quadrature))
1988 case ('boole')
1989 integ = boole_composite(f_node, n_sub, du_step)
1990 case default
1991 integ = simpson_composite(f_node, n_sub, du_step)
1992 end select
1993
1994 fl%Y_mod(j, i) = fl%Y_mod(j, i + 1) + integ
1995 end do
1996
1997 fl%Y_mod_max_per_row(j) = fl%Y_mod(j, 1)
1998 end do
1999
2000 deallocate(u_at_t)
2001 deallocate(f_node)
2002
2003 fl%Y_mod_built = .true.
2004
2005 if (mype == 0) then
2006 y_max_global = maxval(fl%Y_mod_max_per_row)
2007 y_min_global = minval(fl%Y_mod_max_per_row)
2008 write(*,'(A,I0,A,I0,A,A,A,I0)') &
2009 ' Y_mod table built: ', n_nh, ' nH x ', ncool, ' T quadrature=', &
2010 trim(fl%Y_mod_quadrature), ' N_sub=', n_sub
2011 write(*,'(A,F8.4,A,F8.4)') &
2012 ' log10 nH range = ', fl%Y_mod_lg_nH_min, ' to ', fl%Y_mod_lg_nH_max
2013 write(*,'(A,ES12.4,A,ES12.4,A)') &
2014 ' Y_max per row range = [', y_min_global, ', ', y_max_global, '] code time'
2015 write(*,'(A)') ' inverse=bisect (row-interpolated, O(log ncool))'
2016 end if
2017 end subroutine build_y_mod_table
2018
2019 !> Composite Simpson's rule on (N+1) equally spaced samples (N even).
2020 !> N must be a positive even integer; h is the step size.
2021 function simpson_composite(f, N, h) result(s)
2022 integer, intent(in) :: n
2023 double precision, intent(in) :: f(0:n), h
2024 double precision :: s
2025 integer :: k
2026 s = f(0) + f(n)
2027 do k = 1, n - 1, 2
2028 s = s + 4.0d0 * f(k)
2029 end do
2030 do k = 2, n - 2, 2
2031 s = s + 2.0d0 * f(k)
2032 end do
2033 s = s * h / 3.0d0
2034 end function simpson_composite
2035
2036 !> Composite Boole's rule on (N+1) equally spaced samples (N a multiple of 4).
2037 !> Each 4-step block contributes (2h/45)*(7 f0 + 32 f1 + 12 f2 + 32 f3 + 7 f4).
2038 function boole_composite(f, N, h) result(s)
2039 integer, intent(in) :: n
2040 double precision, intent(in) :: f(0:n), h
2041 double precision :: s
2042 integer :: k
2043 s = 0.0d0
2044 do k = 0, n - 4, 4
2045 s = s + 7.0d0 * f(k) &
2046 + 32.0d0 * f(k + 1) &
2047 + 12.0d0 * f(k + 2) &
2048 + 32.0d0 * f(k + 3) &
2049 + 7.0d0 * f(k + 4)
2050 end do
2051 s = s * 2.0d0 * h / 45.0d0
2052 end function boole_composite
2053
2054 !> Bisection on a single Y_mod row to find log10(T) such that
2055 !> Y_mod_row(i) = y_target. The row is monotonically increasing in
2056 !> *decreasing* i (since Y(ncool)=0 grows toward Y(1)=Y_max).
2057 !> Returns log10 T (code units).
2058 function invert_row_bisect(Y_row, t_grid, ncool, y_target) result(log_T_out)
2059 integer, intent(in) :: ncool
2060 double precision, intent(in) :: y_row(ncool), t_grid(ncool), y_target
2061 double precision :: log_t_out
2062 integer :: jl, jh, jc
2063 double precision :: f_lo, f_hi
2064
2065 if (y_target <= y_row(ncool)) then
2066 log_t_out = dlog10(t_grid(ncool))
2067 return
2068 end if
2069 if (y_target >= y_row(1)) then
2070 log_t_out = dlog10(t_grid(1))
2071 return
2072 end if
2073
2074 ! Bracket: find jl, jh = jl+1 such that Y_row(jh) <= y_target <= Y_row(jl)
2075 jl = 1
2076 jh = ncool
2077 do
2078 if (jh - jl <= 1) exit
2079 jc = (jl + jh) / 2
2080 if (y_row(jc) >= y_target) then
2081 jl = jc
2082 else
2083 jh = jc
2084 end if
2085 end do
2086 f_lo = y_row(jl)
2087 f_hi = y_row(jh)
2088 if (f_lo == f_hi) then
2089 log_t_out = 0.5d0 * (dlog10(t_grid(jl)) + dlog10(t_grid(jh)))
2090 else
2091 ! Linear interpolation in log T (the cooling-curve T grid is uniform in log T)
2092 log_t_out = dlog10(t_grid(jl)) &
2093 + (y_target - f_lo) / (f_hi - f_lo) &
2094 * (dlog10(t_grid(jh)) - dlog10(t_grid(jl)))
2095 end if
2096 end function invert_row_bisect
2097
2098 !> Forward Y_mod lookup in (log10 nH, log10 T), both axes uniform in log space;
2099 !> along nH the product nH*Y is interpolated (Y_mod_row_weights)
2100 function findy_mod(Te_loc, nH_loc, fl) result(Y_out)
2101 double precision, intent(in) :: te_loc, nh_loc
2102 type(rc_fluid), intent(in) :: fl
2103 double precision :: y_out
2104 double precision :: log_nh, log_t, ry, rx
2105 integer :: jy, jy1, jx, jx1
2106 double precision :: fy, fx, wlo, whi
2107
2108 log_nh = dlog10(nh_loc)
2109 log_t = dlog10(te_loc)
2110
2111 ! Clamp into table range
2112 ry = (log_nh - fl%Y_mod_lg_nH_min) * fl%Y_mod_lg_nH_step_inv
2113 ry = max(0.0d0, min(ry, dble(fl%Y_mod_n_nH - 1)))
2114 jy = int(ry)
2115 jy1 = min(jy + 1, fl%Y_mod_n_nH - 1)
2116 fy = ry - dble(jy)
2117
2118 rx = (log_t - fl%lgtcoolmin) / fl%lgstep
2119 rx = max(0.0d0, min(rx, dble(fl%ncool - 1)))
2120 jx = int(rx)
2121 jx1 = min(jx + 1, fl%ncool - 1)
2122 fx = rx - dble(jx)
2123
2124 call y_mod_row_weights(fy, fl, wlo, whi)
2125 y_out = wlo * ((1.0d0 - fx) * fl%Y_mod(jy + 1, jx + 1) &
2126 + fx * fl%Y_mod(jy + 1, jx1 + 1)) &
2127 + whi * ((1.0d0 - fx) * fl%Y_mod(jy1 + 1, jx + 1) &
2128 + fx * fl%Y_mod(jy1 + 1, jx1 + 1))
2129 end function findy_mod
2130
2131 !> Inverse Y_mod lookup: given Y_target and nH, return T such that
2132 !> Y_mod(log10 nH, log10 T) = Y_target. Bisection on the cooling-table
2133 !> i index using values interpolated between the two adjacent nH rows
2134 !> with the nH*Y weights of Y_mod_row_weights. Saturates at the
2135 !> table extremes when Y_target falls outside [Y(tcoolmax), Y(tcoolmin)].
2136 function findt_mod(Y_target, nH_loc, fl) result(T_out)
2137 double precision, intent(in) :: y_target, nh_loc
2138 type(rc_fluid), intent(in) :: fl
2139 double precision :: t_out
2140 double precision :: log_nh, ry, fy
2141 integer :: jy, jy1
2142 double precision :: log_t_lo, log_t_hi
2143 integer :: jl, jh, jc, ncool
2144 double precision :: yc_lo, yc_hi, wlo, whi
2145
2146 log_nh = dlog10(nh_loc)
2147 ry = (log_nh - fl%Y_mod_lg_nH_min) * fl%Y_mod_lg_nH_step_inv
2148 ry = max(0.0d0, min(ry, dble(fl%Y_mod_n_nH - 1)))
2149 jy = int(ry)
2150 jy1 = min(jy + 1, fl%Y_mod_n_nH - 1)
2151 fy = ry - dble(jy)
2152 ncool = fl%ncool
2153 call y_mod_row_weights(fy, fl, wlo, whi)
2154
2155 yc_lo = wlo * fl%Y_mod(jy + 1, 1) + whi * fl%Y_mod(jy1 + 1, 1)
2156 yc_hi = wlo * fl%Y_mod(jy + 1, ncool) + whi * fl%Y_mod(jy1 + 1, ncool)
2157 if (y_target >= yc_lo) then
2158 t_out = fl%tcoolmin
2159 return
2160 end if
2161 if (y_target <= yc_hi) then
2162 t_out = fl%tcoolmax
2163 return
2164 end if
2165 jl = 1
2166 jh = ncool
2167 do
2168 if (jh - jl <= 1) exit
2169 jc = (jl + jh) / 2
2170 if ((wlo * fl%Y_mod(jy + 1, jc) + whi * fl%Y_mod(jy1 + 1, jc)) &
2171 >= y_target) then
2172 jl = jc
2173 else
2174 jh = jc
2175 end if
2176 end do
2177 yc_lo = wlo * fl%Y_mod(jy + 1, jl) + whi * fl%Y_mod(jy1 + 1, jl)
2178 yc_hi = wlo * fl%Y_mod(jy + 1, jh) + whi * fl%Y_mod(jy1 + 1, jh)
2179 log_t_lo = dlog10(fl%tcool(jl))
2180 log_t_hi = dlog10(fl%tcool(jh))
2181 if (yc_lo == yc_hi) then
2182 t_out = fl%tcool(jl)
2183 else
2184 t_out = 10.0d0**(log_t_lo &
2185 + (y_target - yc_lo) / (yc_hi - yc_lo) * (log_t_hi - log_t_lo))
2186 end if
2187 end function findt_mod
2188
2189 !> weights between the nH rows jy and jy+1: nH*Y is interpolated linearly in log10 nH,
2190 !> exact for Y ~ 1/nH (fully ionised), where interpolating Y itself overestimates it
2191 subroutine y_mod_row_weights(fy, fl, wlo, whi)
2192 double precision, intent(in) :: fy
2193 type(rc_fluid), intent(in) :: fl
2194 double precision, intent(out) :: wlo, whi
2195 wlo = (1.0d0 - fy) * 10.0d0**(-fy*fl%Y_mod_lg_nH_step)
2196 whi = fy * 10.0d0**((1.0d0 - fy)*fl%Y_mod_lg_nH_step)
2197 end subroutine y_mod_row_weights
2198
2199end module mod_radiative_cooling
Scalar EoS inverse, e.g. fleint_from_T(log_nH, log_T).
Optional local multiplier for density-squared radiative losses.
subroutine, public mpistop(message)
Exit MPI-AMRVAC with an error message.
This module contains definitions of global parameters and variables and some generic functions/subrou...
type(state), pointer block
Block pointer for using one block and its previous state.
logical lb_diagnose
Per-rank load-balance timing diagnostic toggle (off by default). When .true., per-rank wall times are...
integer, parameter unitpar
file handle for IO
character(len=std_len), dimension(:), allocatable par_files
Which par files are used as input.
integer mype
The rank of the current MPI task.
double precision, dimension(:), allocatable, parameter d
double precision unit_temperature
Physical scaling factor for temperature.
logical phys_trac
Use TRAC for MHD or 1D HD.
logical slab_uniform
uniform Cartesian geometry or not (stretched Cartesian)
This module defines the procedures of a physics module. It contains function pointers for the various...
Definition mod_physics.t:4
procedure(sub_get_ei), pointer phys_get_ei
Definition mod_physics.t:68
module radiative cooling – add optically thin radiative cooling
double precision function voigt_profile_ratio(a, x)
Voigt profile ratio phi(x)/phi(0) using Humlicek (1982) Region I/II approx. For the small-a regime (a...
subroutine voigt_gauss_legendre(a, b, n, x, w)
Gauss-Legendre quadrature nodes and weights on [a,b]. Uses the Golub-Welsch algorithm for n points.
subroutine getvar_cooling(ixil, ixol, w, x, coolrate, fl)
double precision function invert_row_bisect(y_row, t_grid, ncool, y_target)
Bisection on a single Y_mod row to find log10(T) such that Y_mod_row(i) = y_target....
subroutine radiative_cooling_rho2_factor(ixil, ixol, w, x, fl, factor)
subroutine radiative_cooling_init_params(phys_gamma, he_abund)
Radiative cooling initialization.
subroutine findl(tpoint, lpoint, fl)
double precision function findt_mod(y_target, nh_loc, fl)
Inverse Y_mod lookup: given Y_target and nH, return T such that Y_mod(log10 nH, log10 T) = Y_target....
double precision function rc_eint_floor(fl, log_nh, nh_val)
Internal energy per volume at the floor temperature tlow, ionisation included.
subroutine get_cool_equi(qdt, ixil, ixol, wct, w, x, fl, res)
subroutine radiative_cooling_init(fl, read_params)
subroutine finddldt(tpoint, dlpoint, fl)
subroutine voigt_escape_init_table()
Build the Voigt escape probability lookup table. Called once (guarded by voigt_table_ready flag)....
subroutine y_mod_row_weights(fy, fl, wlo, whi)
weights between the nH rows jy and jy+1: nH*Y is interpolated linearly in log10 nH,...
double precision, public lb_cool_accum
Per-rank cooling-only compute accumulator for lb_diagnose. Sums the wall time spent inside radiative_...
logical function rc_variable_cv(fl, t1, t2)
True where ionisation buffers the cooling step, so that c_V varies: a large change of T below the tem...
double precision, parameter rc_t_ymod_max_k
Temperature in K above which c_V is constant, the upper end of the Y_mod path.
double precision function boole_composite(f, n, h)
Composite Boole's rule on (N+1) equally spaced samples (N a multiple of 4). Each 4-step block contrib...
subroutine build_y_mod_table(fl)
===================================================================
subroutine radiative_cooling_add_source(qdt, ixil, ixol, wct, wctprim, w, x, qsourcesplit, active, fl)
subroutine calc_l_extended(tpoint, lpoint, fl)
double precision function lowfip_fraction(tpoint, fl)
double precision function findy_mod(te_loc, nh_loc, fl)
Forward Y_mod lookup in (log10 nH, log10 T), both axes uniform in log space; along nH the product nH*...
subroutine cool_exact(qdt, ixil, ixol, wct, wctprim, w, x, fl)
subroutine findt(tpoint, ypoint, fl)
double precision function voigt_escape_lookup(tau)
Look up the Voigt escape probability for a given tau. Uses linear interpolation in log10(tau) space.
subroutine floortemperature(qdt, ixil, ixol, wct, w, x, fl)
double precision function rc_trac_factor(fl, ixd, te_val)
TRAC factor (T/Tcoff)^(5/2) of the losses below Tcoff, and 1 without TRAC or where a user TRAC mask s...
subroutine getvar_cooling_exact(qdt, ixil, ixol, wct, w, x, coolrate, fl)
subroutine radiative_cooling_taper(ixd, x_ndim, rho_val, te_val, rf_val, fl, factor)
double precision function simpson_composite(f, n, h)
Composite Simpson's rule on (N+1) equally spaced samples (N even). N must be a positive even integer;...
subroutine findy(tpoint, ypoint, fl)
module containing all optically thin radiative cooling tables
double precision, dimension(1:101) l_dere_corona
double precision, dimension(1:71) t_mlsolar1
double precision, dimension(1:151) l_cl_solar
double precision, dimension(1:5) t_fm
double precision, dimension(1:14) a_spex_dm_fine
double precision, dimension(1:9) a_rosner
double precision, dimension(1:110) l_spex
double precision, dimension(1:51) l_mb
double precision, dimension(1:10) t_rosner
double precision, dimension(1:5) a_hildner
double precision, dimension(1:9) x_rosner
double precision, dimension(1:7) x_klimchuk
double precision, dimension(1:151) l_composite
double precision, dimension(1:151) l_cl_ism
double precision, dimension(1:8) t_spex_dm_rough
double precision, dimension(1:110) nenh_spex
double precision, dimension(1:110) t_spex
double precision, dimension(1:76) l_dm_2
double precision, dimension(1:151) t_composite
double precision, dimension(1:15) t_spex_dm_fine
double precision, dimension(1:7) x_spex_dm_rough
double precision, dimension(1:14) x_spex_dm_fine
double precision, dimension(1:71) l_mlsolar1
double precision, dimension(1:45) t_jccorona
double precision, dimension(1:5) x_hildner
double precision, dimension(1:71) t_mlcosmol
double precision, dimension(1:151) t_cl_ism
double precision, dimension(1:151) t_cl_solar
double precision, dimension(1:51) t_mb
double precision, dimension(1:8) t_klimchuk
double precision, dimension(1:55) t_colgan
double precision, dimension(1:55) l_colgan
double precision, dimension(1:4) a_fm
double precision, dimension(1:101) l_dere_photo
double precision, dimension(1:45) l_jccorona
double precision, dimension(1:71) l_mlwc
double precision, dimension(1:71) l_dm
double precision, dimension(1:71) t_mlwc
double precision, dimension(1:7) a_spex_dm_rough
double precision, dimension(1:71) t_dm
double precision, dimension(1:7) a_klimchuk
double precision, dimension(1:71) l_mlcosmol
double precision, dimension(1:76) t_dm_2
double precision, dimension(1:6) t_hildner
double precision, dimension(1:4) x_fm
double precision, dimension(1:101) t_dere
double precision, dimension(1:101) lowfip_frac
Public face of the short-characteristics (SC) radiative transfer and its coupling to the (M)HD.
pure elemental double precision function, public rt_sc_thin_weight(te_code, y_code, p_code)
Share of the radiative losses taken by the optically thin treatment, with SC taking the rest,...