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