MPI-AMRVAC 3.2
The MPI - Adaptive Mesh Refinement - Versatile Advection Code (development version)
Loading...
Searching...
No Matches
mod_eos_lte_saha.t
Go to the documentation of this file.
1!=============================================================================
2!> Analytic hydrogen Saha EoS with neutral helium (eos_method == 'analytic')
3!>
4!> Ground-state Saha for hydrogen, solved per call without tables:
5!> X(T) = saha_pf * T^{3/2} * exp(-chi/kB/T)
6!> y(=ne/nH) = 2X / (X + sqrt(X^2 + 4 nH X))
7!> Public entry points take and return code units; the CGS conversion is internal.
8!> T(nH, eint/nH) is found by bisection and p(nH, T) closes the state.
9!> build_gamma1_analytic_table tabulates Gamma_1(nH, p/nH) once for the 'exact' gamma1 path,
10!> on the axes that gamma1_from_nH_p reads for every method
11!=============================================================================
16 use mod_comm_lib, only: mpistop
18 implicit none
19 private
20
22 public :: saha_t_from_nh_eint
24
25 ! Prefactor (2 pi m_e k_B/h^2)^{3/2} in CGS (cm^-3)
26 ! SI value is 2.4146830395719654e21 m^-3; divide by 1e6 for CGS
27 double precision, parameter :: saha_pf = 2.4146830395719654d15
28 double precision, parameter :: saha_chi_kB = 157763.42386247337d0
29 double precision, parameter :: saha_kB_cgs = 1.380649d-16
30 double precision, parameter :: saha_chi_H_cgs = 2.178710282685096d-11
31
32 !> One-shot guard: a bisection that exhausts max_iter without meeting the
33 !> tolerance warns once (rank 0) rather than silently returning an
34 !> under-converged T every offending cell.
35 logical :: saha_nonconv_warned = .false.
36
37contains
38
39 !> Analytic-method load: no binary tables (on-the-fly Saha solve); just
40 !> announce the method.
41 subroutine load_analytic_lte()
42 if (mype == 0) then
43 write(*,*) 'EoS method: analytic (hydrogen Saha, helium neutral)'
44 write(*,*) ' gamma1_method: ', trim(eos%gamma1_method)
45 end if
46 end subroutine load_analytic_lte
47
48 !> Analytic-method finalise: no table unit conversion needed;
49 !> build the Gamma1 table only for 'exact' mode, then the shared FI-bypass
50 !> constants.
52 if (eos%gamma1_method == 'exact') then
53 call build_gamma1_analytic_table()
54 end if
55 call precompute_fi_bypass_constants(saha_chi_h_cgs)
56 end subroutine finalise_analytic_lte
57
58 !> Ionization fraction from Saha in CGS (no unit conversions).
59 !> Use this inside bisection loops where nH_cgs is constant.
60 double precision function saha_y_cgs(nH_cgs, T_cgs) result(y)
61 double precision, intent(in) :: nh_cgs, t_cgs
62 double precision :: x_saha, disc
63
64 x_saha = saha_pf * t_cgs * dsqrt(t_cgs) * dexp(-saha_chi_kb / t_cgs)
65 disc = dsqrt(x_saha * x_saha + 4.0d0 * nh_cgs * x_saha)
66 ! Use numerically stable form to avoid catastrophic cancellation:
67 ! When X >> nH (high ionization): standard form loses precision in -X + sqrt(...)
68 ! Stable form: y = 2*X / (X + sqrt(X^2 + 4*nH*X)) -> 1 as X -> inf
69 y = 2.0d0 * x_saha / (x_saha + disc)
70
71 end function saha_y_cgs
72
73 !> Ionisation fraction y = ne/nH from Saha at (nH, T) in code units
74 double precision function saha_y_from_nh_t(nH_code, T_code) result(y)
75 double precision, intent(in) :: nh_code, t_code
76
77 y = saha_y_cgs(nh_code * unit_numberdensity, t_code * unit_temperature)
78
79 end function saha_y_from_nh_t
80
81 !> eint/nH in CGS with helium neutral, 1.5 (1+He+y) kB T, plus y chi_H when ionE
82 double precision function saha_eint_cgs(y, T_cgs) result(e)
83 double precision, intent(in) :: y, t_cgs
84 e = 1.5d0 * (1.0d0 + eos%He_abundance + y) * saha_kb_cgs * t_cgs
85 if (eos%ionE) e = e + y * saha_chi_h_cgs
86 end function saha_eint_cgs
87
88 !> eint/nH from Saha at (nH, T), in code units
89 double precision function saha_eint_from_nh_t(nH_code, T_code) result(eint_nH_code)
90 double precision, intent(in) :: nh_code, t_code
91
92 if (eos%method_id /= eos_analytic) call mpistop("saha_eint_from_nH_T requires eos_method=analytic")
93 eint_nh_code = saha_eint_cgs(saha_y_from_nh_t(nh_code, t_code), t_code * unit_temperature) &
95
96 end function saha_eint_from_nh_t
97
98 !> Given (nH, p) in code units, T, y and optionally eint/nH: bisection on
99 !> p = nH*(1+He+y(T))*T, with kB absorbed in the code units
100 subroutine saha_state_from_nh_p(nH_code, p_code, T_out, y_out, eint_nH_out)
101 double precision, intent(in) :: nh_code, p_code
102 double precision, intent(out) :: t_out, y_out
103 double precision, intent(out), optional :: eint_nh_out
104
105 double precision :: nh_cgs, ut
106 double precision :: t_lo, t_hi, t_mid, y_mid, p_eval
107 integer :: iter
108 integer, parameter :: max_iter = 30
109 double precision, parameter :: tol = 1.0d-8
110
111 if (eos%method_id /= eos_analytic) call mpistop("saha_state_from_nH_p requires eos_method=analytic")
112 nh_cgs = nh_code * unit_numberdensity
114
115 ! brackets: y = 0 gives T = p/(nH (1+He)), y = 1 gives T = p/(nH (2+He))
116 t_hi = p_code / (nh_code * (1.0d0 + eos%He_abundance))
117 t_lo = p_code / (nh_code * (2.0d0 + eos%He_abundance))
118 t_lo = max(t_lo, 100.0d0 / ut)
119
120 do iter = 1, max_iter
121 t_mid = 0.5d0 * (t_lo + t_hi)
122 y_mid = saha_y_cgs(nh_cgs, t_mid * ut)
123 p_eval = nh_code * (1.0d0 + eos%He_abundance + y_mid) * t_mid
124
125 if (p_eval < p_code) then
126 t_lo = t_mid
127 else
128 t_hi = t_mid
129 end if
130
131 if (dabs(t_hi - t_lo) < tol * t_mid) exit
132 end do
133
134 if (iter > max_iter .and. .not. saha_nonconv_warned .and. mype == 0) then
135 saha_nonconv_warned = .true.
136 write(*,*) "WARNING: saha_state_from_nH_p bisection reached max_iter ", &
137 "without converging; using last iterate. Not reported again."
138 end if
139
140 t_out = t_mid
141 y_out = y_mid
142
143 if (present(eint_nh_out)) &
144 eint_nh_out = saha_eint_cgs(y_mid, t_mid * ut) * unit_numberdensity / unit_pressure
145
146 end subroutine saha_state_from_nh_p
147
148 !> Temperature and ionisation fraction from (nH, eint/nH) in code units, by bisection
149 subroutine saha_t_from_nh_eint(nH_code, eint_nH_code, T_out, y_out)
150 double precision, intent(in) :: nh_code, eint_nh_code
151 double precision, intent(out) :: t_out, y_out
152
153 if (eos%method_id /= eos_analytic) call mpistop("saha_T_from_nH_eint requires eos_method=analytic")
154 call saha_t_bisection(nh_code, eint_nh_code, t_out, y_out)
155
156 end subroutine saha_t_from_nh_eint
157
158 !> Bisection for T(nH, eint/nH) between the fully ionised (y = 1) and neutral (y = 0) limits
159 subroutine saha_t_bisection(nH_code, eint_nH_code, T_out, y_out)
160 double precision, intent(in) :: nh_code, eint_nh_code
161 double precision, intent(out) :: t_out, y_out
162
163 double precision :: nh_cgs, eint_nh_cgs, t_lo, t_hi, t_mid
164 double precision :: y_mid, cv_ion, cv_neu
165 integer :: iter
166 integer, parameter :: max_iter = 30
167 double precision, parameter :: tol = 1.0d-8
168
169 nh_cgs = nh_code * unit_numberdensity
170 eint_nh_cgs = eint_nh_code * unit_pressure / unit_numberdensity
171
172 ! temperature bounds from the fully ionised (y = 1) and neutral (y = 0) limits
173 cv_ion = 1.5d0 * (2.0d0 + eos%He_abundance) * saha_kb_cgs
174 cv_neu = 1.5d0 * (1.0d0 + eos%He_abundance) * saha_kb_cgs
175 if (eos%ionE) then
176 t_lo = (eint_nh_cgs - saha_chi_h_cgs) / cv_ion
177 else
178 t_lo = eint_nh_cgs / cv_ion
179 end if
180 t_lo = max(t_lo, 100.0d0)
181 t_hi = max(eint_nh_cgs / cv_neu, t_lo + 1.0d0)
182
183 ! eint(T) is monotonic, so the bracket always holds; where hydrogen is already fully
184 ! ionised at T_lo (to 1e-10), T_lo = (eint - chi_H)/cv_ion is the solution
185 if (eos%ionE) then
186 if (saha_y_cgs(nh_cgs, t_lo) > 1.0d0 - 1.0d-10) then
187 t_out = t_lo / unit_temperature
188 y_out = 1.0d0
189 return
190 end if
191 end if
192
193 ! Bisect in CGS
194 do iter = 1, max_iter
195 t_mid = 0.5d0 * (t_lo + t_hi)
196 y_mid = saha_y_cgs(nh_cgs, t_mid)
197 if (saha_eint_cgs(y_mid, t_mid) < eint_nh_cgs) then
198 t_lo = t_mid
199 else
200 t_hi = t_mid
201 end if
202
203 if (dabs(t_hi - t_lo) < tol * t_mid) exit
204 end do
205
206 if (iter > max_iter .and. .not. saha_nonconv_warned .and. mype == 0) then
207 saha_nonconv_warned = .true.
208 write(*,*) "WARNING: saha_T_bisection reached max_iter without ", &
209 "converging; using last iterate. Not reported again."
210 end if
211
212 t_out = t_mid / unit_temperature
213 y_out = y_mid
214
215 end subroutine saha_t_bisection
216
217 !> Build the analytic Gamma_1 table in eos%gamma1_p on (log10 nH, log10 p/nH), the axes of the
218 !> LTE tables, so that gamma1_from_nH_p serves every method. Each node takes T and y from the
219 !> Saha solve at (nH, p), and Gamma_1 = chi_rho + chi_T^2 (1+He+y)/c_v, with c_v per nH in kB
220 subroutine build_gamma1_analytic_table()
221 integer, parameter :: ng = 256
222 integer :: i, j
223 double precision :: log_nh_min, log_nh_max, log_pn_min, log_pn_max, t_min, t_max
224 double precision :: dx1, dx2, nh_code, p_code, t_code, y, chi, dlny, npart, cv
225 double precision :: chi_t, chi_rho, g1_val, g1_min, g1_max
226
227 ! log10(nH [CGS]) from 5 to 19, and p/nH = (1+He+y) T over T from 10^2.8 K neutral to
228 ! 10^6.3 K ionised, all in code units
229 log_nh_min = 5.0d0 - dlog10(unit_numberdensity)
230 log_nh_max = 19.0d0 - dlog10(unit_numberdensity)
231 t_min = 10.0d0**2.8d0 / unit_temperature
232 t_max = 10.0d0**6.3d0 / unit_temperature
233 log_pn_min = dlog10((1.0d0 + eos%He_abundance) * t_min)
234 log_pn_max = dlog10((2.0d0 + eos%He_abundance) * t_max)
235
236 eos%gamma1_p%dim1 = ng
237 eos%gamma1_p%dim2 = ng
238 eos%gamma1_p%var1_min = log_nh_min
239 eos%gamma1_p%var1_max = log_nh_max
240 eos%gamma1_p%var2_min = log_pn_min
241 eos%gamma1_p%var2_max = log_pn_max
242 eos%gamma1_p%filename = 'computed_gamma1_analytic'
243 allocate(eos%gamma1_p%table(ng, ng))
244
245 dx1 = (log_nh_max - log_nh_min) / dble(ng - 1)
246 dx2 = (log_pn_max - log_pn_min) / dble(ng - 1)
247 g1_min = 1.0d30
248 g1_max = -1.0d30
249 do j = 1, ng
250 do i = 1, ng
251 nh_code = 10.0d0**(log_nh_min + (i - 1) * dx1)
252 p_code = nh_code * 10.0d0**(log_pn_min + (j - 1) * dx2)
253 call saha_state_from_nh_p(nh_code, p_code, t_code, y)
254 chi = saha_chi_kb / (t_code * unit_temperature)
255 ! dln y/dln T at fixed nH from y^2/(1-y) ~ T^1.5 exp(-chi_H/kT)/nH
256 dlny = (1.0d0 - y) / (2.0d0 - y) * (1.5d0 + chi)
257 npart = 1.0d0 + eos%He_abundance + y
258 cv = 1.5d0 * npart + 1.5d0 * y * dlny
259 if (eos%ionE) cv = cv + y * dlny * chi
260 chi_t = 1.0d0 + y * dlny / npart
261 chi_rho = 1.0d0 - y * (1.0d0 - y) / ((2.0d0 - y) * npart)
262 g1_val = chi_rho + chi_t**2 * npart / cv
263 eos%gamma1_p%table(i, j) = g1_val
264 g1_min = min(g1_min, g1_val)
265 g1_max = max(g1_max, g1_val)
266 end do
267 end do
268 call precompute_step_inv(eos%gamma1_p)
269
270 if (mype == 0) then
271 write(*, '(A,F8.4,A,F8.4)') &
272 ' Gamma1 analytic table built (nH x p/nH): min = ', g1_min, ', max = ', g1_max
273 end if
274
275 end subroutine build_gamma1_analytic_table
276
277end module mod_eos_lte_saha
278!> Needs a line after to pass the preprocessor
subroutine, public mpistop(message)
Exit MPI-AMRVAC with an error message.
EoS state container – the single thermodynamic authority for AMRVAC.
integer, parameter, public eos_analytic
type(eos_container), allocatable, public eos
The single EoS state object, allocated in eos_init and shared (read-mostly) across all EoS sub-module...
Interpolation kernels for the EoS tables (pure math; no EoS state).
subroutine, public precompute_step_inv(tc)
Analytic hydrogen Saha EoS with neutral helium (eos_method == 'analytic').
subroutine, public finalise_analytic_lte()
Analytic-method finalise: no table unit conversion needed; build the Gamma1 table only for 'exact' mo...
subroutine, public saha_t_from_nh_eint(nh_code, eint_nh_code, t_out, y_out)
Temperature and ionisation fraction from (nH, eint/nH) in code units, by bisection.
subroutine, public load_analytic_lte()
Analytic-method load: no binary tables (on-the-fly Saha solve); just announce the method.
double precision function, public saha_eint_from_nh_t(nh_code, t_code)
eint/nH from Saha at (nH, T), in code units
subroutine, public saha_state_from_nh_p(nh_code, p_code, t_out, y_out, eint_nh_out)
Given (nH, p) in code units, T, y and optionally eint/nH: bisection on p = nH*(1+He+y(T))*T,...
Shared LTE lookup-table infrastructure (build/IO; not on the hot path).
subroutine, public precompute_fi_bypass_constants(chi_h_only)
Precompute constants for bypassing table lookups when gas is fully ionised. Physics: above T ~ 50 kK,...
This module contains definitions of global parameters and variables and some generic functions/subrou...
double precision unit_numberdensity
Physical scaling factor for number density.
double precision unit_pressure
Physical scaling factor for pressure.
integer, dimension(:), allocatable ng
number of grid blocks in domain per dimension, in array over levels
integer mype
The rank of the current MPI task.
double precision, dimension(:), allocatable, parameter d
double precision unit_temperature
Physical scaling factor for temperature.