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 H-only Saha EoS (eos_method == 'analytic').
3!>
4!> Ground-state Saha for pure hydrogen, solved per call (no 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; CGS conversion is internal.
8!> T(nH, eint/nH) is found by bisection (default) or Newton; p(nH, T) closes
9!> the state. build_gamma1_analytic_table tabulates Gamma_1(nH, T) once for the
10!> 'exact' gamma1 path; saha_gamma1_from_nH_T reads it back.
11!=============================================================================
16 use mod_comm_lib, only: mpistop
18 implicit none
19 private
20
24
25 ! Analytical H-only Saha constants (module-level parameters)
26 ! Prefactor: (2*pi*m_e*k_B/h^2)^{3/2} in CGS (cm^-3)
27 ! SI value is 2.4146830395719654e21 m^-3; divide by 1e6 for CGS
28 double precision, parameter :: saha_pf = 2.4146830395719654d15
29 double precision, parameter :: saha_chi_kB = 157763.42386247337d0
30 double precision, parameter :: saha_kB_cgs = 1.380649d-16
31 double precision, parameter :: saha_chi_H_cgs = 2.178710282685096d-11
32
33 !> One-shot guard: a bisection that exhausts max_iter without meeting the
34 !> tolerance warns once (rank 0) rather than silently returning an
35 !> under-converged T every offending cell.
36 logical :: saha_nonconv_warned = .false.
37
38contains
39
40 !> Analytic-method load: no binary tables (on-the-fly Saha solve); just
41 !> announce the method.
42 subroutine load_analytic_lte()
43 if (mype == 0) then
44 write(*,*) 'EoS method: analytic (H-only Saha)'
45 write(*,*) ' gamma1_method: ', trim(eos%gamma1_method)
46 end if
47 end subroutine load_analytic_lte
48
49 !> Analytic-method finalise (H-only Saha): no table unit conversion needed;
50 !> build the Gamma1 table only for 'exact' mode, then the shared FI-bypass
51 !> constants.
53 if (eos%gamma1_method == 'exact') then
54 call build_gamma1_analytic_table()
55 end if
57 end subroutine finalise_analytic_lte
58
59 !> Ionization fraction from Saha in CGS (no unit conversions).
60 !> Use this inside bisection loops where nH_cgs is constant.
61 double precision function saha_y_cgs(nH_cgs, T_cgs) result(y)
62 double precision, intent(in) :: nh_cgs, t_cgs
63 double precision :: x_saha, disc
64
65 x_saha = saha_pf * t_cgs * dsqrt(t_cgs) * dexp(-saha_chi_kb / t_cgs)
66 disc = dsqrt(x_saha * x_saha + 4.0d0 * nh_cgs * x_saha)
67 ! Use numerically stable form to avoid catastrophic cancellation:
68 ! When X >> nH (high ionization): standard form loses precision in -X + sqrt(...)
69 ! Stable form: y = 2*X / (X + sqrt(X^2 + 4*nH*X)) -> 1 as X -> inf
70 y = 2.0d0 * x_saha / (x_saha + disc)
71
72 end function saha_y_cgs
73
74 !> Ionization fraction y = ne/nH from analytical Saha at given (nH, T) in CODE UNITS
75 double precision function saha_y_from_nh_t(nH_code, T_code) result(y)
76 double precision, intent(in) :: nh_code, t_code
77
78 y = saha_y_cgs(nh_code * unit_numberdensity, t_code * unit_temperature)
79
80 end function saha_y_from_nh_t
81
82 !> Internal energy per nH from analytical Saha, in CODE UNITS.
83 !> eint_nH = 1.5*(1+y)*kB*T [+ y*chi_H] in CGS, converted to code units
84 double precision function saha_eint_from_nh_t(nH_code, T_code) result(eint_nH_code)
85 double precision, intent(in) :: nh_code, t_code
86 double precision :: y, t_cgs, eint_nh_cgs
87
88 if (eos%method_id /= eos_analytic) call mpistop("saha_eint_from_nH_T requires eos_method=analytic")
89 y = saha_y_from_nh_t(nh_code, t_code)
90 t_cgs = t_code * unit_temperature
91
92 eint_nh_cgs = 1.5d0 * (1.0d0 + y) * saha_kb_cgs * t_cgs
93 if (eos%ionE) then
94 eint_nh_cgs = eint_nh_cgs + y * saha_chi_h_cgs
95 end if
96
97 eint_nh_code = eint_nh_cgs * unit_numberdensity / unit_pressure
98
99 end function saha_eint_from_nh_t
100
101 !> Given (nH, p) in CODE UNITS, find T and y by solving p = nH*(1+y(T))*T_code.
102 !> Uses bisection on T. In code units, kB is absorbed: p = nH*(1+y)*T.
103 !> Given (nH, p) in CODE UNITS, find T, y, and eint/nH.
104 !> Bisects on p = nH*(1+y(T))*T_code, then computes eint from final T,y.
105 !> Returns eint_nH in code units (avoids redundant Saha re-evaluation).
106 subroutine saha_state_from_nh_p(nH_code, p_code, T_out, y_out, eint_nH_out)
107 double precision, intent(in) :: nh_code, p_code
108 double precision, intent(out) :: t_out, y_out
109 double precision, intent(out), optional :: eint_nh_out
110
111 double precision :: nh_cgs, ut, eint_nh_cgs
112 double precision :: t_lo, t_hi, t_mid, y_mid, p_eval
113 integer :: iter
114 integer, parameter :: max_iter = 30
115 double precision, parameter :: tol = 1.0d-8
116
117 ! Hoist CGS conversion out of loop
118 if (eos%method_id /= eos_analytic) call mpistop("saha_state_from_nH_p requires eos_method=analytic")
119 nh_cgs = nh_code * unit_numberdensity
121
122 ! Brackets in code units: y=0 => T = p/nH, y=1 => T = p/(2*nH)
123 t_hi = p_code / nh_code
124 t_lo = p_code / (2.0d0 * nh_code)
125 t_lo = max(t_lo, 100.0d0 / ut)
126
127 do iter = 1, max_iter
128 t_mid = 0.5d0 * (t_lo + t_hi)
129 y_mid = saha_y_cgs(nh_cgs, t_mid * ut)
130 p_eval = nh_code * (1.0d0 + y_mid) * t_mid
131
132 if (p_eval < p_code) then
133 t_lo = t_mid
134 else
135 t_hi = t_mid
136 end if
137
138 if (dabs(t_hi - t_lo) < tol * t_mid) exit
139 end do
140
141 if (iter > max_iter .and. .not. saha_nonconv_warned .and. mype == 0) then
142 saha_nonconv_warned = .true.
143 write(*,*) "WARNING: saha_state_from_nH_p bisection reached max_iter ", &
144 "without converging; using last iterate. Not reported again."
145 end if
146
147 t_out = t_mid
148 y_out = y_mid
149
150 ! Optionally return eint/nH directly (avoids redundant Saha re-evaluation)
151 if (present(eint_nh_out)) then
152 eint_nh_cgs = 1.5d0 * (1.0d0 + y_mid) * saha_kb_cgs * (t_mid * ut)
153 if (eos%ionE) eint_nh_cgs = eint_nh_cgs + y_mid * saha_chi_h_cgs
154 eint_nh_out = eint_nh_cgs * unit_numberdensity / unit_pressure
155 end if
156
157 end subroutine saha_state_from_nh_p
158
159 !> Temperature inversion: given (nH, eint/nH) in CODE UNITS, find T in CODE UNITS,
160 !> by bisection (guaranteed convergence).
161 subroutine saha_t_from_nh_eint(nH_code, eint_nH_code, T_out, y_out)
162 double precision, intent(in) :: nh_code, eint_nh_code
163 double precision, intent(out) :: t_out, y_out
164
165 if (eos%method_id /= eos_analytic) call mpistop("saha_T_from_nH_eint requires eos_method=analytic")
166 call saha_t_bisection(nh_code, eint_nh_code, t_out, y_out)
167
168 end subroutine saha_t_from_nh_eint
169
170 !> Bisection solver for T(nH, eint/nH). Guaranteed convergence.
171 !> Brackets: T_lo from fully ionized (y=1), T_hi from neutral (y=0).
172 !> Handles the non-monotonic eint(T) when ionE is included by
173 !> checking the FI limit first: if y~1 at T_lo, use T_lo directly.
174 subroutine saha_t_bisection(nH_code, eint_nH_code, T_out, y_out)
175 double precision, intent(in) :: nh_code, eint_nh_code
176 double precision, intent(out) :: t_out, y_out
177
178 double precision :: nh_cgs, eint_nh_cgs, t_lo, t_hi, t_mid
179 double precision :: eint_mid, y_mid, y_lo
180 integer :: iter
181 integer, parameter :: max_iter = 30
182 double precision, parameter :: tol = 1.0d-8
183
184 ! Hoist CGS conversions out of loop
185 nh_cgs = nh_code * unit_numberdensity
186 eint_nh_cgs = eint_nh_code * unit_pressure / unit_numberdensity
187
188 ! Temperature bounds in CGS from energy equation limits
189 if (eos%ionE) then
190 ! T_lo: assume y=1 (fully ionized) -> eint = 3*kB*T + chi_H
191 t_lo = max((eint_nh_cgs - saha_chi_h_cgs) / (3.0d0 * saha_kb_cgs), 100.0d0)
192 ! T_hi: assume y=0 (neutral) -> eint = 1.5*kB*T
193 t_hi = eint_nh_cgs / (1.5d0 * saha_kb_cgs)
194 else
195 t_lo = eint_nh_cgs / (3.0d0 * saha_kb_cgs)
196 t_hi = eint_nh_cgs / (1.5d0 * saha_kb_cgs)
197 end if
198 t_lo = max(t_lo, 100.0d0)
199 t_hi = max(t_hi, t_lo + 1.0d0)
200
201 ! Guard against non-monotonic eint(T) when ionE is active:
202 ! At low nH, hydrogen is fully ionized (y=1) across a wide T range,
203 ! making the Saha equation degenerate. The eint function has two roots:
204 ! one at low T (ionized, eint ~ chi_H + small thermal) and one at
205 ! high T (neutral, eint ~ thermal only). The bisection must stay on
206 ! the correct branch.
207 ! Fix: check y at T_lo. If y~1, use the FI analytical formula directly.
208 if (eos%ionE) then
209 y_lo = saha_y_cgs(nh_cgs, t_lo)
210 if (y_lo > 0.999d0) then
211 ! Fully ionized: eint = 3*kB*T + chi_H => T = (eint - chi_H)/(3*kB)
212 t_out = max((eint_nh_cgs - saha_chi_h_cgs) &
213 / (3.0d0 * saha_kb_cgs), 100.0d0) / unit_temperature
214 y_out = 1.0d0
215 return
216 end if
217 end if
218
219 ! Bisect in CGS (no unit conversions inside loop)
220 do iter = 1, max_iter
221 t_mid = 0.5d0 * (t_lo + t_hi)
222 y_mid = saha_y_cgs(nh_cgs, t_mid)
223
224 eint_mid = 1.5d0 * (1.0d0 + y_mid) * saha_kb_cgs * t_mid
225 if (eos%ionE) eint_mid = eint_mid + y_mid * saha_chi_h_cgs
226
227 if (eint_mid < eint_nh_cgs) then
228 t_lo = t_mid
229 else
230 t_hi = t_mid
231 end if
232
233 if (dabs(t_hi - t_lo) < tol * t_mid) exit
234 end do
235
236 if (iter > max_iter .and. .not. saha_nonconv_warned .and. mype == 0) then
237 saha_nonconv_warned = .true.
238 write(*,*) "WARNING: saha_T_bisection reached max_iter without ", &
239 "converging; using last iterate. Not reported again."
240 end if
241
242 t_out = t_mid / unit_temperature
243 y_out = y_mid
244
245 end subroutine saha_t_bisection
246
247 !> Build 2D Gamma1(nH, T) table from analytical Saha for the 'analytic' EoS method.
248 !> Grid: uniform in (log10 nH, log10 T) with 256x256 points.
249 !> Gamma1 = 1 + (1+y)*kB / Cv, where Cv = d(eint)/d(T) at constant rho.
250 !> Stored in eos%gamma1_p for reuse by the existing csound2 routines
251 !> (re-indexed: axis 2 = log10(T) instead of log10(p/nH)).
252 subroutine build_gamma1_analytic_table()
253 integer, parameter :: ng = 256
254 integer :: i, j
255 double precision :: log_nh_min, log_nh_max, log_t_min, log_t_max
256 double precision :: dx1, dx2, log_nh, log_t
257 double precision :: nh_code, t_code, y, yp, ym
258 double precision :: cv, g1_val, g1_min, g1_max
259 double precision :: t_cgs, dt_cgs
260 double precision :: eint_p, eint_m
261
262 ! Range: log10(nH [CGS]) from 5 to 19, log10(T [K]) from 2.8 to 6.3
263 ! Convert to code-unit ranges
264 log_nh_min = 5.0d0 - dlog10(unit_numberdensity)
265 log_nh_max = 19.0d0 - dlog10(unit_numberdensity)
266 log_t_min = 2.8d0 - dlog10(unit_temperature)
267 log_t_max = 6.3d0 - dlog10(unit_temperature)
268
269 eos%gamma1_p%dim1 = ng
270 eos%gamma1_p%dim2 = ng
271 eos%gamma1_p%var1_min = log_nh_min
272 eos%gamma1_p%var1_max = log_nh_max
273 eos%gamma1_p%var2_min = log_t_min
274 eos%gamma1_p%var2_max = log_t_max
275 eos%gamma1_p%filename = 'computed_gamma1_analytic'
276
277 allocate(eos%gamma1_p%table(ng, ng))
278
279 dx1 = (log_nh_max - log_nh_min) / dble(ng - 1)
280 dx2 = (log_t_max - log_t_min) / dble(ng - 1)
281
282 g1_min = 1.0d30
283 g1_max = -1.0d30
284
285 do j = 1, ng
286 log_t = log_t_min + (j - 1) * dx2
287 t_code = 10.0d0**log_t
288 t_cgs = t_code * unit_temperature
289 ! Central difference step: 0.1% of T
290 dt_cgs = 1.0d-3 * t_cgs
291
292 do i = 1, ng
293 log_nh = log_nh_min + (i - 1) * dx1
294 nh_code = 10.0d0**log_nh
295
296 y = saha_y_from_nh_t(nh_code, t_code)
297
298 ! Compute Cv = d(eint/nH)/d(T) via central difference
299 yp = saha_y_from_nh_t(nh_code, (t_cgs + dt_cgs) / unit_temperature)
300 ym = saha_y_from_nh_t(nh_code, (t_cgs - dt_cgs) / unit_temperature)
301
302 eint_p = 1.5d0 * (1.0d0 + yp) * saha_kb_cgs * (t_cgs + dt_cgs)
303 eint_m = 1.5d0 * (1.0d0 + ym) * saha_kb_cgs * (t_cgs - dt_cgs)
304 if (eos%ionE) then
305 eint_p = eint_p + yp * saha_chi_h_cgs
306 eint_m = eint_m + ym * saha_chi_h_cgs
307 end if
308
309 cv = (eint_p - eint_m) / (2.0d0 * dt_cgs)
310
311 ! Gamma1 = 1 + (1+y)*kB / Cv
312 if (cv > 0.0d0) then
313 g1_val = 1.0d0 + (1.0d0 + y) * saha_kb_cgs / cv
314 else
315 g1_val = eos%gamma
316 end if
317
318 eos%gamma1_p%table(i, j) = g1_val
319 g1_min = min(g1_min, g1_val)
320 g1_max = max(g1_max, g1_val)
321 end do
322 end do
323
324 call precompute_step_inv(eos%gamma1_p)
325
326 if (mype == 0) then
327 write(*, '(A,F8.4,A,F8.4)') &
328 ' Gamma1 analytic table built (nH x T): min = ', g1_min, ', max = ', g1_max
329 end if
330
331 end subroutine build_gamma1_analytic_table
332
333 !> Look up Gamma1 from the analytical 2D table (nH, T axes in code units).
334 !> For use when eos%method == 'analytic' and gamma1_method == 'exact'.
335 double precision function saha_gamma1_from_nh_t(nH_code, T_code) result(g1)
336 double precision, intent(in) :: nh_code, t_code
337 double precision :: log_nh, log_t
338
339 if (eos%method_id /= eos_analytic) call mpistop("saha_gamma1_from_nH_T requires eos_method=analytic")
340 log_nh = dlog10(nh_code)
341 log_t = dlog10(t_code)
342
343 g1 = bicubic_lookup(log_nh, log_t, eos%gamma1_p)
344
345 end function saha_gamma1_from_nh_t
346
347end module mod_eos_lte_saha
348!> 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).
pure double precision function, public bicubic_lookup(var1, var2, tc)
Dispatch wrapper: bicubic lookup that branches on the table's is_uniform flag. Hides the uniform-vs-a...
subroutine, public precompute_step_inv(tc)
Analytic H-only Saha EoS (eos_method == 'analytic').
double precision function, public saha_gamma1_from_nh_t(nh_code, t_code)
Look up Gamma1 from the analytical 2D table (nH, T axes in code units). For use when eosmethod == 'an...
subroutine, public finalise_analytic_lte()
Analytic-method finalise (H-only Saha): no table unit conversion needed; build the Gamma1 table only ...
subroutine, public saha_t_from_nh_eint(nh_code, eint_nh_code, t_out, y_out)
Temperature inversion: given (nH, eint/nH) in CODE UNITS, find T in CODE UNITS, by bisection (guarant...
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)
Internal energy per nH from analytical Saha, in CODE UNITS. eint_nH = 1.5*(1+y)*kB*T [+ y*chi_H] in C...
subroutine, public saha_state_from_nh_p(nh_code, p_code, t_out, y_out, eint_nh_out)
Given (nH, p) in CODE UNITS, find T and y by solving p = nH*(1+y(T))*T_code. Uses bisection on T....
Shared LTE lookup-table infrastructure (build/IO; not on the hot path).
subroutine, public precompute_fi_bypass_constants()
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.