MPI-AMRVAC 3.2
The MPI - Adaptive Mesh Refinement - Versatile Advection Code (development version)
Loading...
Searching...
No Matches
mod_eos_LTE_state.t
Go to the documentation of this file.
1!=============================================================================
2!> 'state' method (eos_method='state', legacy 'tables') of the LTE EoS family.
3!>
4!> The direct per-quantity tabulation of the Saha ionisation-equilibrium state:
5!> loads the T/neOnH (and ionE p2eint) tables, shifts them to code units, and
6!> builds the derived tables the runtime needs (gamma1, log_p, p_over_nH, the
7!> interleaved fast-path block, the pressure-indexed gamma1, and the bisected
8!> p2eint / eint-from-T inverses). The shared binary-file I/O, axis/guard/validate
9!> machinery and the FI-bypass constants live in mod_eos_LTE_tables (used by all
10!> LTE methods); this module owns only the state-method build + finalise. Runs
11!> once, during eos_init_LTE / eos_finalise_LTE.
12!=============================================================================
20 use mod_comm_lib, only: mpistop
21 implicit none
22 private
23
24 !> state arms of the eos_init / eos_finalise dispatchers (mod_eos_LTE)
26
27contains
28
29 !> 'tables' method load: T + neOnH, plus the ionE inverse/derived tables
30 !> (p2eint loaded; gamma1 / eint_from_T loaded if available, else built later).
31 !> Called from eos_init_LTE for eos_method='tables'.
32 subroutine load_state_lte()
33 call load_tables_lte("T")
34 call load_tables_lte("neOnH")
35 if (eos%ionE) then
36 call load_tables_lte("p2eint")
37 call try_load_tables_lte("gamma1")
38 call try_load_tables_lte("eint_from_T")
39 end if
40 end subroutine load_state_lte
41
42 !> Table-based method finalise: shift the loaded T/neOnH (and ionE-derived)
43 !> tables to code units, build the derived tables not present on disk, then
44 !> precompute step/guard arrays and validate. shift_table_to_code: axis2='eint'
45 !> uses the log(p/nH) axis-2 unit, value_shift subtracted from table VALUES
46 !> (T stores log T -> shift by log(unit_temperature); neOnH dimensionless -> 0);
47 !> it also ensures node arrays exist so the build_*_table routines see a single
48 !> var{1,2}_nodes path. Called from eos_finalise_LTE for eos_method='tables'.
49 subroutine finalise_state_lte()
50 call shift_table_to_code(eos%T, .false., dlog10(unit_temperature))
51 call shift_table_to_code(eos%neOnH, .false., 0.d0)
52
53 if (eos%ionE) then
54 if (allocated(eos%p2eint%table)) then
55 call shift_table_to_code(eos%p2eint, .false., 0.d0)
56 end if
57
58 !> Build derived tables from loaded T and neOnH. For any table that
59 !> has a loaded version on disk (uniform or adaptive), KEEP the loaded
60 !> one -- this enables the hybrid mode (T+neOnH uniform for the
61 !> interleaved fast path; gamma1/p2eint/eint_from_T adaptive for the
62 !> gamma_1 peak / tighter closure). Loaded versions came from the same
63 !> Saha solver as T/neOnH so they are physically consistent. Skipping
64 !> rebuild also avoids the one-time runtime cost but sacrifices accuracy
65 if (.not. allocated(eos%gamma1%table)) then
66 call build_gamma1_table()
67 else
68 if (mype == 0) write(*,'(A)') " Using loaded gamma1 table (skip rebuild)"
69 !> gamma1 on (log_nH, log_eint/nH) CGS log10; axes -> code units,
70 !> dimensionless values (no value shift).
71 call shift_table_to_code(eos%gamma1, .false., 0.d0)
72 end if
73
74 !> Order matters: build_gamma1_p_table reads eos%p2eint%var*_nodes,
75 !> so p2eint must be allocated first (loaded from disk above, or built
76 !> here for the entropy-sourced case).
77 if (.not. allocated(eos%p2eint%table)) then
78 call build_p2eint_table()
79 else
80 if (mype == 0) write(*,'(A)') " Using loaded p2eint table (skip rebuild)"
81 call ensure_axis_nodes(eos%p2eint)
82 end if
83
84 call build_gamma1_p_table()
85 call build_log_p_table()
86 call build_p_over_nh_table()
87
88 if (.not. allocated(eos%eint_from_T%table)) then
89 call build_eint_from_t_table()
90 else
91 if (mype == 0) write(*,'(A)') " Using loaded eint_from_T table (skip rebuild)"
92 !> eint_from_T on (log_nH, log_T) CGS log10, values log10(eint/nH)
93 !> CGS. axis2='T'; value shift log(p/nH).
94 call shift_table_to_code(eos%eint_from_T, .true., &
96 end if
97 endif
98
100 call precompute_step_inv(eos%neOnH)
101 call eos_build_guards(eos%T)
102 call eos_build_guards(eos%neOnH)
103 if (eos%ionE) then
104 call precompute_step_inv(eos%p2eint)
105 call precompute_step_inv(eos%gamma1)
106 call precompute_step_inv(eos%gamma1_p)
107 call precompute_step_inv(eos%eint_from_T)
108 call precompute_step_inv(eos%log_p)
109 call precompute_step_inv(eos%p_over_nH)
110 call eos_build_guards(eos%p2eint)
111 call eos_build_guards(eos%gamma1)
112 call eos_build_guards(eos%gamma1_p)
113 call eos_build_guards(eos%eint_from_T)
114 call eos_build_guards(eos%log_p)
115 call eos_build_guards(eos%p_over_nH)
116
117 ! Build interleaved table: T, neOnH, p_over_nH
118 call build_interleaved_eint_table()
119 end if
120
121 ! Defensive check: invariants on every populated table. Catches
122 ! silent-failure modes (forgotten guard, mismatched node-array length,
123 ! non-monotonic nodes) at startup rather than at first lookup.
124 call eos_validate_table(eos%T, "eos%T")
125 call eos_validate_table(eos%neOnH, "eos%neOnH")
126 if (eos%ionE) then
127 call eos_validate_table(eos%p2eint, "eos%p2eint")
128 call eos_validate_table(eos%gamma1, "eos%gamma1")
129 call eos_validate_table(eos%gamma1_p, "eos%gamma1_p")
130 call eos_validate_table(eos%eint_from_T, "eos%eint_from_T")
131 call eos_validate_table(eos%log_p, "eos%log_p")
132 call eos_validate_table(eos%p_over_nH, "eos%p_over_nH")
133 end if
134
136
137 if (eos%ionE) call verify_eos_round_trips()
138 !> Crucial to not end up with interpolation drift...
139 end subroutine finalise_state_lte
140
141 !> 'tables' method: shift one loaded table from CGS log10 storage to code
142 !> units and ensure its node arrays exist. axis2_kind selects the axis-2
143 !> unit ('T' -> log(unit_temperature); else -> log(unit_pressure/
144 !> unit_numberdensity)). value_shift is subtracted from the table VALUES
145 !> (0 = dimensionless table, no value shift; e.g. unit_temperature for the
146 !> T table, unit_pressure/unit_numberdensity for eint_from_T). Replaces the
147 !> per-table copy-pasted shift idiom (the subtle per-table differences here
148 !> were a past source of unit bugs).
149 subroutine shift_table_to_code(tc, axis2_is_T, value_shift)
150 type(eos_table_container), intent(inout) :: tc
151 logical, intent(in) :: axis2_is_t
152 double precision, intent(in) :: value_shift
153 double precision :: a1, a2
154 a1 = dlog10(unit_numberdensity)
155 if (axis2_is_t) then
156 a2 = dlog10(unit_temperature)
157 else
159 end if
160 tc%var1_min = tc%var1_min - a1
161 tc%var1_max = tc%var1_max - a1
162 tc%var2_min = tc%var2_min - a2
163 tc%var2_max = tc%var2_max - a2
164 if (value_shift /= 0.d0) tc%table = tc%table - value_shift
165 if (.not. tc%is_uniform) then
166 tc%var1_nodes = tc%var1_nodes - a1
167 tc%var2_nodes = tc%var2_nodes - a2
168 end if
169 call ensure_axis_nodes(tc)
170 end subroutine shift_table_to_code
171
172 !> Gamma_1 from (log nH, log eint/nH). Used only by build_gamma1_p_table,
173 !> i.e. the 'tables' method; the entropy method reads its own g1p table.
174 double precision function gamma1_from_nh_eint(log_nH, log_eint_nH) result(g1)
175 double precision, intent(in) :: log_nh, log_eint_nh
176 g1 = bicubic_lookup(log_nh, log_eint_nh, eos%gamma1)
177 end function gamma1_from_nh_eint
178
179 !> Build the first adiabatic index Gamma_1 table from T and neOnH tables.
180 !> Called during eos_finalise after unit conversion, when tables are in code units.
181 !>
182 !> Gamma_1 = (rho/p) * (dp/drho)_s = a + b * (p / eint_vol)
183 !> where a = d(log10 p)/d(log10 nH) at constant eint/nH
184 !> b = d(log10 p)/d(log10 eint/nH) at constant nH
185 !> and p = nH * T * (1 + He + y), eint_vol = nH * (eint/nH)
186 !>
187 !> Derivation: from the thermodynamic identity
188 !> a^2 = (dp/drho)_{eint} + ((eint+p)/rho) * (dp/deint)_rho
189 !> expressed in table coordinates (log10 nH, log10 eint/nH).
190 !> For ideal gas: a=1, b=1, p/eint=gamma-1, so Gamma_1 = 1+(gamma-1) = gamma.
191 subroutine build_gamma1_table()
192 integer :: n1, n2, i, j, im, ip, jm, jp
193 double precision :: x1, x2
194 double precision, allocatable :: log_p(:,:)
195 double precision :: t_val, y_val, p_val, eint_vol
196 double precision :: a_val, b_val, g1_val
197 double precision :: g1_min, g1_max
198
199 n1 = eos%T%dim1
200 n2 = eos%T%dim2
201
202 !> Gamma1 table shares the same grid as T and neOnH (inherits
203 !> uniform-or-adaptive node layout from the source).
204 eos%gamma1%dim1 = n1
205 eos%gamma1%dim2 = n2
206 eos%gamma1%var1_min = eos%T%var1_min
207 eos%gamma1%var1_max = eos%T%var1_max
208 eos%gamma1%var2_min = eos%T%var2_min
209 eos%gamma1%var2_max = eos%T%var2_max
210 eos%gamma1%filename = 'computed_gamma1'
211 eos%gamma1%is_uniform = eos%T%is_uniform
212 if (allocated(eos%gamma1%var1_nodes)) deallocate(eos%gamma1%var1_nodes)
213 if (allocated(eos%gamma1%var2_nodes)) deallocate(eos%gamma1%var2_nodes)
214 allocate(eos%gamma1%var1_nodes(n1)); eos%gamma1%var1_nodes = eos%T%var1_nodes
215 allocate(eos%gamma1%var2_nodes(n2)); eos%gamma1%var2_nodes = eos%T%var2_nodes
216
217 if (allocated(eos%gamma1%table)) deallocate(eos%gamma1%table)
218 allocate(eos%gamma1%table(n1, n2))
219 allocate(log_p(n1, n2))
220
221 !> Compute log10(p) at each grid point (using actual node positions)
222 !> In code units: p = nH * T * (1 + He + y), kB absorbed
223 do j = 1, n2
224 x2 = eos%T%var2_nodes(j)
225 do i = 1, n1
226 x1 = eos%T%var1_nodes(i)
227 t_val = 10.0d0**eos%T%table(i, j) !> T in code units
228 y_val = 10.0d0**eos%neOnH%table(i, j) !> Ne/nH (dimensionless)
229 !> log10(p) = log10(nH) + log10(T) + log10(1 + He + y)
230 log_p(i, j) = x1 + eos%T%table(i, j) + dlog10(1.0d0 + eos%He_abundance + y_val)
231 end do
232 end do
233
234 !> Compute Gamma_1 from finite differences of log_p using
235 !> actual node spacings (correct for both uniform and adaptive grids).
236 g1_min = 1.0d30
237 g1_max = -1.0d30
238
239 do j = 1, n2
240 x2 = eos%T%var2_nodes(j)
241 do i = 1, n1
242 x1 = eos%T%var1_nodes(i)
243
244 !> Centered finite differences with one-sided at boundaries
245 im = max(i - 1, 1)
246 ip = min(i + 1, n1)
247 jm = max(j - 1, 1)
248 jp = min(j + 1, n2)
249
250 !> p/eint_vol = T * (1+He+y) / (eint/nH)
251 t_val = 10.0d0**eos%T%table(i, j)
252 y_val = 10.0d0**eos%neOnH%table(i, j)
253 eint_vol = 10.0d0**x2 !> eint/nH in code units
254 p_val = t_val * (1.0d0 + eos%He_abundance + y_val)
255
256 if (eos%gamma1_method == 'effective') then
257 !> Approximate: gamma_eff = 1 + p/eint_vol
258 g1_val = 1.0d0 + p_val / eint_vol
259 else
260 !> Full formula: Gamma_1 = a + b * (p / eint_vol)
261 !> a = d(log10 p) / d(log10 nH) at constant eint/nH
262 a_val = (log_p(ip, j) - log_p(im, j)) &
263 / (eos%T%var1_nodes(ip) - eos%T%var1_nodes(im))
264 !> b = d(log10 p) / d(log10 eint/nH) at constant nH
265 b_val = (log_p(i, jp) - log_p(i, jm)) &
266 / (eos%T%var2_nodes(jp) - eos%T%var2_nodes(jm))
267 g1_val = a_val + b_val * (p_val / eint_vol)
268 endif
269
270 !> Clamp to physical range [1, 5/3] (monatomic H+He gas)
271 g1_val = max(1.001d0, min(g1_val, 5.0d0/3.0d0))
272
273 eos%gamma1%table(i, j) = g1_val
274
275 g1_min = min(g1_min, g1_val)
276 g1_max = max(g1_max, g1_val)
277 end do
278 end do
279
280 deallocate(log_p)
281
282 if (mype == 0) then
283 write(*, '(A,A,A,F8.4,A,F8.4)') &
284 ' Gamma1 table built (', trim(eos%gamma1_method), '): min = ', &
285 g1_min, ', max = ', g1_max
286 end if
287
288 end subroutine build_gamma1_table
289
290 !> Build merged log10(p/nH) table: log10(p/nH)(nH, eint/nH).
291 !> Same grid as T and neOnH tables. Each entry is:
292 !> log10(p/nH) = log10( 10^T * (1 + He + 10^y) )
293 !> where T and y are from the forward tables at the same grid point.
294 !> Used by the WB bisection to replace two separate PCHIP lookups
295 !> (T + neOnH) with a single lookup per iteration.
296 subroutine build_log_p_table()
297 integer :: n1, n2, i, j
298 double precision :: log_nh, log_eint_nh, t_val, y_val
299
300 n1 = eos%T%dim1
301 n2 = eos%T%dim2
302
303 eos%log_p%dim1 = n1
304 eos%log_p%dim2 = n2
305 eos%log_p%var1_min = eos%T%var1_min
306 eos%log_p%var1_max = eos%T%var1_max
307 eos%log_p%var2_min = eos%T%var2_min
308 eos%log_p%var2_max = eos%T%var2_max
309 eos%log_p%is_uniform = eos%T%is_uniform
310 if (allocated(eos%log_p%var1_nodes)) deallocate(eos%log_p%var1_nodes)
311 if (allocated(eos%log_p%var2_nodes)) deallocate(eos%log_p%var2_nodes)
312 allocate(eos%log_p%var1_nodes(n1)); eos%log_p%var1_nodes = eos%T%var1_nodes
313 allocate(eos%log_p%var2_nodes(n2)); eos%log_p%var2_nodes = eos%T%var2_nodes
314
315 if (allocated(eos%log_p%table)) deallocate(eos%log_p%table)
316 allocate(eos%log_p%table(n1, n2))
317
318 do j = 1, n2
319 do i = 1, n1
320 t_val = eos%T%table(i, j)
321 y_val = eos%neOnH%table(i, j)
322 eos%log_p%table(i, j) = dlog10(10.0d0**t_val &
323 * (1.0d0 + eos%He_abundance + 10.0d0**y_val))
324 end do
325 end do
326
327 if (mype == 0) then
328 write(*, '(A,I4,A,I4)') &
329 ' Merged log_p table built: ', n1, ' x ', n2
330 end if
331
332 end subroutine build_log_p_table
333
334 !> Build p/nH table: (1+He+y)*T at each (nH, eint/nH) grid point.
335 !> Shares axes with the T and neOnH tables.
336 !> Used by to_primitive_LTE for single-lookup pressure computation.
337 subroutine build_p_over_nh_table()
338 integer :: n1, n2, i, j
339 double precision :: t_val, y_val, p_nh_val
340 double precision :: p_min, p_max
341
342 n1 = eos%T%dim1
343 n2 = eos%T%dim2
344
345 eos%p_over_nH%dim1 = n1
346 eos%p_over_nH%dim2 = n2
347 eos%p_over_nH%var1_min = eos%T%var1_min
348 eos%p_over_nH%var1_max = eos%T%var1_max
349 eos%p_over_nH%var2_min = eos%T%var2_min
350 eos%p_over_nH%var2_max = eos%T%var2_max
351 eos%p_over_nH%filename = 'computed_p_over_nH'
352 eos%p_over_nH%is_uniform = eos%T%is_uniform
353 if (allocated(eos%p_over_nH%var1_nodes)) deallocate(eos%p_over_nH%var1_nodes)
354 if (allocated(eos%p_over_nH%var2_nodes)) deallocate(eos%p_over_nH%var2_nodes)
355 allocate(eos%p_over_nH%var1_nodes(n1)); eos%p_over_nH%var1_nodes = eos%T%var1_nodes
356 allocate(eos%p_over_nH%var2_nodes(n2)); eos%p_over_nH%var2_nodes = eos%T%var2_nodes
357
358 if (allocated(eos%p_over_nH%table)) deallocate(eos%p_over_nH%table)
359 allocate(eos%p_over_nH%table(n1, n2))
360
361 p_min = 1.0d30
362 p_max = -1.0d30
363
364 do j = 1, n2
365 do i = 1, n1
366 !> T table stores log10(T_code), neOnH stores log10(y)
367 t_val = 10.0d0**eos%T%table(i, j)
368 y_val = 10.0d0**eos%neOnH%table(i, j)
369 !> p/nH = (1 + He + y) * T in code units
370 p_nh_val = (1.0d0 + eos%He_abundance + y_val) * t_val
371 !> Store as log10 for PCHIP interpolation consistency
372 eos%p_over_nH%table(i, j) = dlog10(p_nh_val)
373 p_min = min(p_min, p_nh_val)
374 p_max = max(p_max, p_nh_val)
375 end do
376 end do
377
378 call precompute_step_inv(eos%p_over_nH)
379
380 if (mype == 0) then
381 write(*, '(A,ES10.3,A,ES10.3)') &
382 ' p/nH table built: min = ', p_min, ', max = ', p_max
383 end if
384
385 end subroutine build_p_over_nh_table
386
387 !> Build interleaved table from existing T, neOnH, p_over_nH tables.
388 !> Layout: table_eint_il(3, ny, nx) where:
389 !> slot 1 = T (log10), slot 2 = neOnH (log10), slot 3 = p_over_nH (log10)
390 !> All three are on the same (nH, eint/nH) axes.
391 subroutine build_interleaved_eint_table()
392 integer :: n1, n2, i, j
393
394 n1 = eos%T%dim1
395 n2 = eos%T%dim2
396
397 if (allocated(eos%table_eint_il)) deallocate(eos%table_eint_il)
398 allocate(eos%table_eint_il(3, n1, n2))
399
400 do j = 1, n2
401 do i = 1, n1
402 eos%table_eint_il(1, i, j) = eos%T%table(i, j)
403 eos%table_eint_il(2, i, j) = eos%neOnH%table(i, j)
404 eos%table_eint_il(3, i, j) = eos%p_over_nH%table(i, j)
405 end do
406 end do
407
408 if (mype == 0) then
409 write(*, '(A,I0,A,I0,A,I0,A,F6.1,A)') &
410 ' Interleaved table built: ', 3, ' x ', n1, ' x ', n2, &
411 ' (', 3.0d0*n1*n2*8.0d0/1024.0d0, ' KB)'
412 end if
413
414 end subroutine build_interleaved_eint_table
415
416 !> Build pressure-indexed Gamma_1 table: Gamma_1(nH, p/nH).
417 !> Re-indexes the eint-based gamma1 table into pressure space,
418 !> sharing the same grid axes as the p2eint table.
419 !> This eliminates the intermediate p2eint lookup from hd_get_csound2_LTE.
420 subroutine build_gamma1_p_table()
421 integer :: n1, n2, i, j
422 double precision :: log_nh, log_p_nh
423 double precision :: p2eint_ratio, log_eint_nh
424 double precision :: g1_val, g1_min, g1_max
425
426 n1 = eos%p2eint%dim1
427 n2 = eos%p2eint%dim2
428
429 ! gamma1_p shares the same axis layout as p2eint (inherits adaptive
430 ! v1 nodes from T via p2eint; v2 is uniform over the log_p/nH range).
431 eos%gamma1_p%dim1 = n1
432 eos%gamma1_p%dim2 = n2
433 eos%gamma1_p%var1_min = eos%p2eint%var1_min
434 eos%gamma1_p%var1_max = eos%p2eint%var1_max
435 eos%gamma1_p%var2_min = eos%p2eint%var2_min
436 eos%gamma1_p%var2_max = eos%p2eint%var2_max
437 eos%gamma1_p%filename = 'computed_gamma1_p'
438 eos%gamma1_p%is_uniform = eos%p2eint%is_uniform
439 if (allocated(eos%gamma1_p%var1_nodes)) deallocate(eos%gamma1_p%var1_nodes)
440 if (allocated(eos%gamma1_p%var2_nodes)) deallocate(eos%gamma1_p%var2_nodes)
441 allocate(eos%gamma1_p%var1_nodes(n1)); eos%gamma1_p%var1_nodes = eos%p2eint%var1_nodes
442 allocate(eos%gamma1_p%var2_nodes(n2)); eos%gamma1_p%var2_nodes = eos%p2eint%var2_nodes
443
444 if (allocated(eos%gamma1_p%table)) deallocate(eos%gamma1_p%table)
445 allocate(eos%gamma1_p%table(n1, n2))
446
447 g1_min = 1.0d30
448 g1_max = -1.0d30
449
450 do j = 1, n2
451 log_p_nh = eos%p2eint%var2_nodes(j)
452 do i = 1, n1
453 log_nh = eos%p2eint%var1_nodes(i)
454
455 ! Convert from (nH, p/nH) to (nH, eint/nH) via the p2eint table
456 ! p2eint table stores the ratio eint/(p), so:
457 ! log10(eint/nH) = log10(p/nH) + log10(p2eint_ratio)
458 p2eint_ratio = eos%p2eint%table(i, j)
459 log_eint_nh = log_p_nh + dlog10(p2eint_ratio)
460
461 ! Look up Gamma_1 from the eint-indexed table
462 g1_val = gamma1_from_nh_eint(log_nh, log_eint_nh)
463
464 eos%gamma1_p%table(i, j) = g1_val
465
466 g1_min = min(g1_min, g1_val)
467 g1_max = max(g1_max, g1_val)
468 end do
469 end do
470
471 if (mype == 0) then
472 write(*, '(A,F8.4,A,F8.4)') &
473 ' Gamma1_p table built (pressure-indexed): min = ', g1_min, ', max = ', g1_max
474 end if
475
476 end subroutine build_gamma1_p_table
477
478 !> Build p2eint table at runtime by inverting the forward (T, neOnH) tables.
479 !> For each (nH, p/nH) grid point, find eint/nH by bisection such that
480 !> the PCHIP-interpolated forward tables give p(nH, eint/nH) = p_target.
481 !> This guarantees exact round-trip: to_conserved(p) -> eint -> to_primitive -> p.
482 !>
483 !> The p/nH grid bounds are computed from the forward tables (not hardcoded),
484 !> ensuring coverage of reasonable pressures.
485 subroutine build_p2eint_table()
486 integer :: n1, n2_fwd, n2_p, i, j, iter
487 double precision :: log_nh_i, log_eint_lo, log_eint_hi, log_eint_mid
488 double precision :: log_p_target, log_p_eval
489 double precision :: t_val, y_val
490 double precision :: p_global_min, p_global_max
491 double precision :: dx_p, log_p_nh_ij
492 double precision :: max_err, err
493 double precision :: log_p_lo_i, log_p_hi_i
494 integer :: i_worst, j_worst
495
496 if (mype == 0) write(*,*) 'Building p2eint table from forward tables...'
497
498 n1 = eos%T%dim1
499 n2_fwd = eos%T%dim2
500
501 !> Compute p/nH at every forward grid node to find the pressure range
502 p_global_min = 1.0d30
503 p_global_max = -1.0d30
504 do j = 1, n2_fwd
505 do i = 1, n1
506 t_val = 10.0d0**eos%T%table(i, j)
507 y_val = 10.0d0**eos%neOnH%table(i, j)
508 log_p_nh_ij = dlog10(t_val * (1.0d0 + eos%He_abundance + y_val))
509 p_global_min = min(p_global_min, log_p_nh_ij)
510 p_global_max = max(p_global_max, log_p_nh_ij)
511 end do
512 end do
513
514 !> Pad pressure range to avoid edge clamping at off-grid PCHIP evaluations.
515 !> The PCHIP polynomial can overshoot grid-node extrema,
516 !> which for steep ionisation-zone gradients needs generous padding.
517 p_global_min = p_global_min - 0.2d0
518 p_global_max = p_global_max + 0.2d0
519
520 n2_p = n2_fwd !> Same resolution as forward table
521 if (allocated(eos%p2eint%table)) deallocate(eos%p2eint%table)
522 eos%p2eint%dim1 = n1
523 eos%p2eint%dim2 = n2_p
524 eos%p2eint%var1_min = eos%T%var1_min
525 eos%p2eint%var1_max = eos%T%var1_max
526 eos%p2eint%var2_min = p_global_min
527 eos%p2eint%var2_max = p_global_max
528 eos%p2eint%filename = 'computed_p2eint'
529 !> v1 axis inherited from T (adaptive if T is); v2 axis is uniform
530 !> over the (different) log_p/nH range. When T is adaptive, we still
531 !> store explicit var2_nodes so lookups dispatch via _nu uniformly.
532 eos%p2eint%is_uniform = eos%T%is_uniform
533 if (allocated(eos%p2eint%var1_nodes)) deallocate(eos%p2eint%var1_nodes)
534 if (allocated(eos%p2eint%var2_nodes)) deallocate(eos%p2eint%var2_nodes)
535 allocate(eos%p2eint%var1_nodes(n1)); eos%p2eint%var1_nodes = eos%T%var1_nodes
536 allocate(eos%p2eint%var2_nodes(n2_p))
537 do j = 1, n2_p
538 eos%p2eint%var2_nodes(j) = p_global_min &
539 + (j - 1) * (p_global_max - p_global_min) / dble(n2_p - 1)
540 end do
541 allocate(eos%p2eint%table(n1, n2_p))
542
543 dx_p = (p_global_max - p_global_min) / dble(n2_p - 1)
544 max_err = 0.0d0
545
546 !> For each (nH_i, p_j/nH), bisect on log10(eint/nH) to find the
547 !> value where the PCHIP-interpolated forward tables give this pressure.
548 i_worst = 1
549 j_worst = 1
550 do i = 1, n1
551 log_nh_i = eos%T%var1_nodes(i)
552
553 !> Compute achievable p range for this nH from forward table endpoints
554 t_val = bicubic_lookup(log_nh_i, eos%T%var2_min, eos%T)
555 y_val = bicubic_lookup(log_nh_i, eos%T%var2_min, eos%neOnH)
556 log_p_lo_i = dlog10(10.0d0**t_val &
557 * (1.0d0 + eos%He_abundance + 10.0d0**y_val))
558 t_val = bicubic_lookup(log_nh_i, eos%T%var2_max, eos%T)
559 y_val = bicubic_lookup(log_nh_i, eos%T%var2_max, eos%neOnH)
560 log_p_hi_i = dlog10(10.0d0**t_val &
561 * (1.0d0 + eos%He_abundance + 10.0d0**y_val))
562
563 do j = 1, n2_p
564 log_p_target = p_global_min + (j - 1) * dx_p
565
566 !> Clamp target to achievable range -- outside this range,
567 !> map to the table boundary, should be fine
568 if (log_p_target <= log_p_lo_i) then
569 eos%p2eint%table(i, j) = 10.0d0**(eos%T%var2_min - log_p_target)
570 cycle
571 else if (log_p_target >= log_p_hi_i) then
572 eos%p2eint%table(i, j) = 10.0d0**(eos%T%var2_max - log_p_target)
573 cycle
574 end if
575
576 !> Bisection on log10(eint/nH)
577 log_eint_lo = eos%T%var2_min
578 log_eint_hi = eos%T%var2_max
579
580 do iter = 1, 52 !> 52 bisections -> 2^-52 ~ 10^-15.7 precision
581 log_eint_mid = 0.5d0 * (log_eint_lo + log_eint_hi)
582
583 !> Evaluate p from forward tables at (nH_i, eint_mid)
584 t_val = bicubic_lookup(log_nh_i, log_eint_mid, eos%T)
585 y_val = bicubic_lookup(log_nh_i, log_eint_mid, eos%neOnH)
586 log_p_eval = dlog10(10.0d0**t_val &
587 * (1.0d0 + eos%He_abundance + 10.0d0**y_val))
588
589 if (log_p_eval < log_p_target) then
590 log_eint_lo = log_eint_mid
591 else
592 log_eint_hi = log_eint_mid
593 end if
594
595 if (dabs(log_eint_hi - log_eint_lo) < 1.0d-14) exit
596 end do
597
598 eos%p2eint%table(i, j) = 10.0d0**(log_eint_mid - log_p_target)
599
600 !> Track round-trip error (only for in-range points)
601 err = dabs(log_p_eval - log_p_target)
602 if (err > max_err) then
603 max_err = err
604 i_worst = i
605 j_worst = j
606 end if
607 end do
608 end do
609
610 if (mype == 0) then
611 write(*, '(A,ES10.3,A,I4,A,I4)') &
612 ' p2eint table built: max bisection err = ', max_err, &
613 ' at (i,j)=(', i_worst, ',', j_worst, ')'
614 write(*, '(A,F8.4,A,F8.4)') &
615 ' log10(p/nH) range = ', p_global_min, ' to ', p_global_max
616 end if
617
618 end subroutine build_p2eint_table
619
620 !> Build inverse T table: given (nH, T), return log10(eint/nH).
621 !> Inverts the forward T(nH, eint/nH) table by bisection using the SAME
622 !> PCHIP interpolation kernel as T_from_nH_eint. This guarantees that
623 !> T_from_nH_eint(nH, eint_from_T(nH, T)) = T to machine precision.
624 !> Called during eos_finalise after unit conversion.
625 subroutine build_eint_from_t_table()
626 integer :: n1, n2_fwd, n2_inv, i, j, iter
627 double precision :: dx2_inv
628 double precision :: t_global_min, t_global_max, t_fi_threshold
629 double precision :: log_nh_i, log_eint_lo, log_eint_hi, log_eint_mid
630 double precision :: t_target, t_eval, t_max_at_nh, eint_nh_fi
631 double precision :: eint_nh_min, eint_nh_max, max_err, err
632 integer :: n_fi_filled
633
634 n1 = eos%T%dim1
635 n2_fwd = eos%T%dim2
636
637 !> Find global T range from the forward T table
638 t_global_min = 1.0d30
639 t_global_max = -1.0d30
640 do j = 1, n2_fwd
641 do i = 1, n1
642 t_global_min = min(t_global_min, eos%T%table(i, j))
643 t_global_max = max(t_global_max, eos%T%table(i, j))
644 end do
645 end do
646
647 !> FI threshold temperature in code units (200,000 K)
648 t_fi_threshold = eos%p_rho_FI_threshold &
649 * (1.0d0 + 4.0d0*eos%He_abundance) / eos%n_per_nH_FI
650
651 !> Extend T axis to cover the FI regime up to 10 MK (or T_global_max if larger)
652 t_global_max = max(t_global_max, dlog10(1.0d7/unit_temperature))
653
654 !> Set up inverse table with same nH axis, extended T axis
655 n2_inv = n2_fwd !> Same resolution as forward table
656 if (allocated(eos%eint_from_T%table)) deallocate(eos%eint_from_T%table)
657 eos%eint_from_T%dim1 = n1
658 eos%eint_from_T%dim2 = n2_inv
659 eos%eint_from_T%var1_min = eos%T%var1_min
660 eos%eint_from_T%var1_max = eos%T%var1_max
661 eos%eint_from_T%var2_min = t_global_min
662 eos%eint_from_T%var2_max = t_global_max
663 eos%eint_from_T%filename = 'computed_eint_from_T'
664 !> Inherit v1 axis from T (adaptive if T is); uniform v2 axis covers
665 !> [T_global_min, T_global_max] with explicit nodes.
666 eos%eint_from_T%is_uniform = eos%T%is_uniform
667 if (allocated(eos%eint_from_T%var1_nodes)) deallocate(eos%eint_from_T%var1_nodes)
668 if (allocated(eos%eint_from_T%var2_nodes)) deallocate(eos%eint_from_T%var2_nodes)
669 allocate(eos%eint_from_T%var1_nodes(n1)); eos%eint_from_T%var1_nodes = eos%T%var1_nodes
670 allocate(eos%eint_from_T%var2_nodes(n2_inv))
671 do j = 1, n2_inv
672 eos%eint_from_T%var2_nodes(j) = t_global_min &
673 + (j - 1) * (t_global_max - t_global_min) / dble(n2_inv - 1)
674 end do
675
676 allocate(eos%eint_from_T%table(n1, n2_inv))
677
678 dx2_inv = (t_global_max - t_global_min) / dble(n2_inv - 1)
679
680 eint_nh_min = 1.0d30
681 eint_nh_max = -1.0d30
682 max_err = 0.0d0
683 n_fi_filled = 0
684
685 !> For each (nH_i, T_j): use bisection on the forward table where it
686 !> has coverage, and the analytic FI formula above the table range.
687 do i = 1, n1
688 log_nh_i = eos%T%var1_nodes(i)
689
690 !> Find the maximum T in the forward table at this nH
691 t_max_at_nh = eos%T%table(i, n2_fwd)
692
693 do j = 1, n2_inv
694 t_target = t_global_min + (j - 1) * dx2_inv
695
696 if (10.0d0**t_target > t_fi_threshold .or. &
697 t_target > t_max_at_nh) then
698 !> Above table coverage or FI regime: use analytic formula
699 !> eint/nH = n_per_nH_FI / (gamma-1) * T + neOnH_FI * eion_per_nH
700 eint_nh_fi = eos%n_per_nH_FI * eos%inv_gamma_minus_1 &
701 * 10.0d0**t_target
702 if (eos%ionE) eint_nh_fi = eint_nh_fi &
703 + eos%neOnH_FI * eos%eion_per_nH
704 eos%eint_from_T%table(i, j) = dlog10(eint_nh_fi)
705 n_fi_filled = n_fi_filled + 1
706 else
707 !> Bisection on log10(eint/nH) using the forward T table
708 log_eint_lo = eos%T%var2_min
709 log_eint_hi = eos%T%var2_max
710
711 do iter = 1, 52
712 log_eint_mid = 0.5d0 * (log_eint_lo + log_eint_hi)
713
714 t_eval = bicubic_lookup(log_nh_i, log_eint_mid, eos%T)
715
716 if (t_eval < t_target) then
717 log_eint_lo = log_eint_mid
718 else
719 log_eint_hi = log_eint_mid
720 end if
721
722 if (dabs(log_eint_hi - log_eint_lo) < 1.0d-14) exit
723 end do
724
725 eos%eint_from_T%table(i, j) = log_eint_mid
726
727 err = dabs(t_eval - t_target)
728 max_err = max(max_err, err)
729 end if
730
731 eint_nh_min = min(eint_nh_min, eos%eint_from_T%table(i, j))
732 eint_nh_max = max(eint_nh_max, eos%eint_from_T%table(i, j))
733 end do
734 end do
735
736 if (mype == 0) then
737 write(*, '(A,ES10.3,A,I0,A,I0)') &
738 ' Inverse T table built: max bisection err = ', max_err, &
739 ', FI-filled = ', n_fi_filled, ' / ', n1*n2_inv
740 write(*, '(A,F8.4,A,F8.4)') &
741 ' log10(T) range = ', t_global_min, ' to ', t_global_max
742 write(*, '(A,F8.4,A,F8.4)') &
743 ' log10(eint/nH) range = ', eint_nh_min, ' to ', eint_nh_max
744 end if
745
746 end subroutine build_eint_from_t_table
747
748 !> Verify round-trip consistency of ALL EoS table pathways.
749 !> Tests at off-grid random points to measure real interpolation error.
750 !> Called after all tables are built.
751 subroutine verify_eos_round_trips()
752 integer :: n1, n2, i, j, n_tested
753 double precision :: log_nh, log_eint_nh
754 double precision :: dx1, dx2
755 double precision :: t_val, y_val, log_p_nh, p2eint_val, log_eint_recovered
756 double precision :: t_recovered, p_from_ty, log_p_recovered
757 double precision :: eint_from_t_val, log_eint_from_t_recovered
758 double precision :: err_p, err_eint_t
759 double precision :: max_err_p, max_err_eint_t, mean_err_p, mean_err_eint_t
760 double precision :: log_nh_worst_p, log_eint_worst_p
761
762 n1 = eos%T%dim1
763 n2 = eos%T%dim2
764 dx1 = (eos%T%var1_max - eos%T%var1_min) / dble(n1 - 1)
765 dx2 = (eos%T%var2_max - eos%T%var2_min) / dble(n2 - 1)
766
767 max_err_p = 0.0d0
768 max_err_eint_t = 0.0d0
769 mean_err_p = 0.0d0
770 mean_err_eint_t = 0.0d0
771 n_tested = 0
772
773 !> Test at cell-centre-like points (offset by 0.5*dx from grid nodes)
774 !> to exercise the PCHIP interpolation between nodes.
775 do i = 2, n1 - 1
776 log_nh = eos%T%var1_min + (dble(i) - 0.5d0) * dx1
777 do j = 2, n2 - 1
778 log_eint_nh = eos%T%var2_min + (dble(j) - 0.5d0) * dx2
779 n_tested = n_tested + 1
780
781 !> === Round-trip 1: eint -> T,y -> p -> p2eint -> eint' ===
782 !> Forward: get T and y from eint
783 t_val = bicubic_lookup(log_nh, log_eint_nh, eos%T)
784 y_val = bicubic_lookup(log_nh, log_eint_nh, eos%neOnH)
785 !> Compute p/nH
786 log_p_nh = dlog10(10.0d0**t_val &
787 * (1.0d0 + eos%He_abundance + 10.0d0**y_val))
788 !> Inverse: get eint from p via p2eint table
789 p2eint_val = bicubic_lookup(log_nh, log_p_nh, eos%p2eint)
790 log_eint_recovered = log_p_nh + dlog10(p2eint_val)
791
792 err_p = dabs(log_eint_recovered - log_eint_nh)
793 mean_err_p = mean_err_p + err_p
794 if (err_p > max_err_p) then
795 max_err_p = err_p
796 log_nh_worst_p = log_nh
797 log_eint_worst_p = log_eint_nh
798 end if
799
800 !> === Round-trip 2: eint -> T -> eint_from_T -> eint' ===
801 eint_from_t_val = bicubic_lookup(log_nh, t_val, eos%eint_from_T)
802
803 err_eint_t = dabs(eint_from_t_val - log_eint_nh)
804 mean_err_eint_t = mean_err_eint_t + err_eint_t
805 end do
806 end do
807
808 mean_err_p = mean_err_p / dble(max(n_tested, 1))
809 mean_err_eint_t = mean_err_eint_t / dble(max(n_tested, 1))
810
811 if (mype == 0) then
812 write(*, '(A)') ' === EoS round-trip verification (off-grid points) ==='
813 write(*, '(A,I6,A)') ' Tested ', n_tested, ' points (half-cell offsets)'
814 write(*, '(A,ES10.3,A,ES10.3)') &
815 ' p round-trip: max err = ', max_err_p, &
816 ' mean err = ', mean_err_p
817 write(*, '(A,F8.4,A,F8.4)') &
818 ' worst at log_nH = ', log_nh_worst_p, &
819 ', log_eint = ', log_eint_worst_p
820 write(*, '(A,ES10.3,A,ES10.3)') &
821 ' T round-trip: max err = ', max_err_eint_t, &
822 ' mean err = ', mean_err_eint_t
823 write(*, '(A)') ' ====================================================='
824 end if
825
826 end subroutine verify_eos_round_trips
827
828end module mod_eos_lte_state
829!> 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.
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)
'state' method (eos_method='state', legacy 'tables') of the LTE EoS family.
subroutine, public finalise_state_lte()
Table-based method finalise: shift the loaded T/neOnH (and ionE-derived) tables to code units,...
subroutine, public load_state_lte()
state arms of the eos_init / eos_finalise dispatchers (mod_eos_LTE)
Shared LTE lookup-table infrastructure (build/IO; not on the hot path).
subroutine, public try_load_tables_lte(fieldname)
Attempt to load pre-computed table from binary file. If the file does not exist, silently skip and th...
subroutine, public eos_validate_table(tc, name)
Defensive consistency check for one table after init. Aborts with a diagnostic message if any silent-...
subroutine, public load_tables_lte(fieldname)
Read one named table file into its eos% container. The filename encodes the composition (H or HHe) an...
subroutine, public ensure_axis_nodes(tc)
Ensure tcvar1_nodes / tcvar2_nodes are allocated and populated so that build_*_table routines and oth...
subroutine, public eos_build_guards(tc)
Build guard (bucket) arrays so that adaptive index lookup is O(1). No-op for uniform tables....
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 mype
The rank of the current MPI task.
double precision, dimension(:), allocatable, parameter d
double precision unit_temperature
Physical scaling factor for temperature.