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