Skip to content

Commit b09d279

Browse files
authored
Regular low-speed collision substep in stost (#615)
## Cause In `collis_alp::stost` the speed p is advanced by Euler-Maruyama with the substep bounded by `fraction*p/|fpeff|` and `fraction**2*p**2/(6*dpp)`. Near p = 0 the speed process is a 3-D Bessel-type process (`fpeff ~ 2*dpp/p`). In a cold plasma and from a subthermal start, about one call in 1e6 to 1e7 lets the discrete walk descend geometrically to p ~ 1e-10 within one call. The substep then falls below ulp of the remaining time and `stost` returns `ierr = 4`, so the caller rejects the step. Below p ~ 1e-8 the `plim` floor in `coleff_local` also weakens the repulsive drift. ## Fix For `iswmode` 1 and 2, below `p_switch = 1/max(velrat)` over the field species with `efcolf > 0` (thermal speed of the slowest species over v0), the substep is taken in Cartesian velocity space. This is the same generator, but regular at p = 0: `v_new = v + A*vhat*h + sqrt(2*Dpar*h)*xi1*vhat + sqrt(2*Dperp*h)*(xi2*e2 + xi3*e3)` with `Dpar = dpp`, `Dperp = dhh*p**2` and `A = fpeff - 2*Dperp/p`, evaluated without the 1/p cancellation in the new helper `coleff_cartesian` (next to `coleff_local`/`onseff`). Ito check: `dp = (A + 2*Dperp/p) dt + sqrt(2*Dpar) dW = fpeff dt + ...` and the pitch generator is `Dperp/p**2 d/dlambda (1-lambda**2) d/dlambda`. Above `p_switch`, and whenever `velrat` is zero (synthetic tests), the existing scheme is unchanged. ## Tests New `test_collision_low_speed` (labels `unit;collisions;physics`), cold H plasma at 20 eV with 2.5% C6+, n_e = 2e19 m^-3: - Start at p = 1e-9 and 1e-12 must return `ierr = 0` (the old code returns `ierr = 4` immediately, checked once by hand). - First moments of one substep on both sides of `p_switch` match `fpeff*h` and `-2*dhh*lambda*h` within 5 sigma. - 4096 markers from p = 1e-6 thermalize to the Maxwellian `<p**2>`, `<lambda> = 0`, `<lambda**2> = 1/3` within 5 sigma, and subthermal markers never error over 2000 seeds. Verified on macOS arm64 (gfortran 16, Release): `ctest -L collisions` passes (12/12, including `test_collision_substeps` and the new test). `golden_record_collision` could not run there, because `golden_record.sh` uses bash 4 `exec {fd}` redirection. Chris&AI
1 parent 8263951 commit b09d279

3 files changed

Lines changed: 231 additions & 2 deletions

File tree

‎src/collis_alphas.f90‎

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,38 @@ subroutine onseff(v, dp, dh, dpd)
118118
end if
119119
end subroutine onseff
120120

121+
!> Cartesian-velocity coefficients of the same collision operator, regular
122+
!> at p = 0: parallel diffusion dpar, perpendicular diffusion dperp and the
123+
!> drift a along the velocity, with fpeff = a + 2*dperp/p.
124+
subroutine coleff_cartesian(p, efcolf_loc, velrat_loc, enrat_loc, &
125+
dpar, dperp, a)
126+
real(wp), intent(in) :: p
127+
real(wp), intent(in) :: efcolf_loc(nsorts), velrat_loc(nsorts), enrat_loc(nsorts)
128+
real(wp), intent(out) :: dpar, dperp, a
129+
130+
real(wp), parameter :: cons = 0.75225278d0
131+
integer :: i
132+
real(wp) :: x, dp, dh, dpd, g
133+
134+
dpar = 0.0_wp
135+
dperp = 0.0_wp
136+
a = 0.0_wp
137+
do i = 1, nsorts
138+
x = p*velrat_loc(i)
139+
call onseff(x, dp, dh, dpd)
140+
if (x < 0.01d0) then
141+
g = -2.0d0*cons*x
142+
else if (x > 6.0d0) then
143+
g = -2.0d0/x**2
144+
else
145+
g = (dpd - 2.0d0*dh)/x
146+
end if
147+
dpar = dpar + dp*efcolf_loc(i)
148+
dperp = dperp + dh*efcolf_loc(i)
149+
a = a + efcolf_loc(i)*(velrat_loc(i)*g - 2.0d0*dp*p*enrat_loc(i))
150+
end do
151+
end subroutine coleff_cartesian
152+
121153
subroutine loacol_alpha(am1, am2, Z1, Z2, densi1, densi2, tempi1, tempi2, &
122154
tempe, ealpha, v0, dchichi, slowrate, &
123155
dchichi_norm, slowrate_norm, test_mass, test_charge)
@@ -288,10 +320,11 @@ subroutine stost(z, dtauc, iswmode, ierr, max_substep_fraction, s_profile)
288320
integer, parameter :: max_substeps = 100000
289321
real(wp) :: initial(5), fraction, remaining, step, p, dpp, dhh, fpeff
290322
real(wp) :: alam, angle, azimuth
323+
real(wp) :: p_switch, dpar, dperp, adrift, vhat(2), vnew(3), xi(3)
291324
real(wp) :: efcolf_loc(nsorts), velrat_loc(nsorts), enrat_loc(nsorts)
292325
real :: ur
293-
integer :: substep
294-
logical :: pitch, energy, diffusion
326+
integer :: substep, i
327+
logical :: pitch, energy, diffusion, cartesian_ok
295328

296329
initial = z
297330
ierr = 0
@@ -324,9 +357,56 @@ subroutine stost(z, dtauc, iswmode, ierr, max_substep_fraction, s_profile)
324357
energy = iswmode /= 4
325358
diffusion = iswmode < 3
326359
remaining = dtauc
360+
! Below the thermal speed of the slowest field species the radial
361+
! process is Bessel-like and the Euler walk in p can collapse towards
362+
! p = 0. There the substep is taken in Cartesian velocity space.
363+
cartesian_ok = iswmode <= 2 .and. any(efcolf_loc > 0.0_wp .and. &
364+
velrat_loc > 0.0_wp)
365+
p_switch = 0.0_wp
366+
if (cartesian_ok) p_switch = 1.0_wp/maxval(velrat_loc, &
367+
mask=efcolf_loc > 0.0_wp)
327368

328369
do substep = 1, max_substeps
329370
p = z(4)
371+
if (cartesian_ok .and. p < p_switch) then
372+
call coleff_cartesian(p, efcolf_loc, velrat_loc, enrat_loc, &
373+
dpar, dperp, adrift)
374+
if (.not. all(is_finite([dpar, dperp, adrift])) .or. &
375+
dpar < 0.0_wp .or. dperp < 0.0_wp) then
376+
ierr = 1
377+
exit
378+
end if
379+
step = remaining
380+
if (max(dpar, dperp) > 0.0_wp) step = min(step, &
381+
fraction**2*p_switch**2/(6.0_wp*max(dpar, dperp)))
382+
if (abs(adrift) > 0.0_wp) &
383+
step = min(step, fraction*p_switch/abs(adrift))
384+
if (step <= 0.0_wp .or. remaining - step == remaining) then
385+
ierr = 4 ! Cannot represent further time progress.
386+
exit
387+
end if
388+
alam = z(5)
389+
vhat = [alam, sqrt(1.0_wp - alam**2)]
390+
do i = 1, 3
391+
call getran(0, ur)
392+
xi(i) = real(ur, wp)
393+
end do
394+
vnew(1:2) = p*vhat + (adrift*step + &
395+
sqrt(2.0_wp*dpar*step)*xi(1))*vhat + &
396+
sqrt(2.0_wp*dperp*step)*xi(2)*[-vhat(2), vhat(1)]
397+
vnew(3) = sqrt(2.0_wp*dperp*step)*xi(3)
398+
z(4) = sqrt(sum(vnew**2))
399+
if (pitch .and. is_finite(z(4)) .and. z(4) > 0.0_wp) &
400+
z(5) = max(-1.0_wp, min(1.0_wp, vnew(1)/z(4)))
401+
if (.not. all(is_finite(z)) .or. z(4) <= 0.0_wp .or. &
402+
abs(z(5)) > 1.0_wp) then
403+
ierr = 1
404+
exit
405+
end if
406+
remaining = remaining - step
407+
if (remaining == 0.0_wp) return
408+
cycle
409+
end if
330410
call coleff_local(p, efcolf_loc, velrat_loc, enrat_loc, dpp, dhh, fpeff)
331411
if (.not. all(is_finite([dpp, dhh, fpeff])) .or. &
332412
dpp < 0.0_wp .or. dhh < 0.0_wp) then

‎test/tests/CMakeLists.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,12 @@ add_test(NAME test_collision_substeps COMMAND test_collision_substeps.x)
535535
set_tests_properties(test_collision_substeps PROPERTIES
536536
LABELS "unit;collisions;physics" TIMEOUT 180)
537537

538+
add_executable(test_collision_low_speed.x test_collision_low_speed.f90)
539+
target_link_libraries(test_collision_low_speed.x simple)
540+
add_test(NAME test_collision_low_speed COMMAND test_collision_low_speed.x)
541+
set_tests_properties(test_collision_low_speed PROPERTIES
542+
LABELS "unit;collisions;physics" TIMEOUT 180)
543+
538544
add_executable (test_profiles.x test_profiles.f90)
539545
target_link_libraries(test_profiles.x simple)
540546
add_test(NAME test_profiles COMMAND test_profiles.x)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
!> Low-speed behaviour of collis_alp::stost in a cold plasma. Below the thermal
2+
!> speed of the slowest field species the radial speed process is Bessel-like,
3+
!> and the Euler walk in p can collapse to p ~ 1e-10, where it cannot represent
4+
!> further time progress (ierr = 4). The Cartesian substep is regular there.
5+
program test_collision_low_speed
6+
use, intrinsic :: iso_fortran_env, only: dp => real64
7+
use collis_alp, only: stost, coleff, efcolf, velrat, enrat, efcolf_grid, &
8+
velrat_grid, enrat_grid, N_S_GRID, &
9+
compute_collision_coeffs
10+
implicit none
11+
12+
real(dp), parameter :: ealpha = 55.0e3_dp, temp = 20.0_dp
13+
real(dp) :: p_switch, rate, v0
14+
integer :: nseed
15+
integer, allocatable :: seed(:)
16+
17+
call random_seed(size=nseed)
18+
allocate (seed(nseed))
19+
call setup_cold_plasma()
20+
p_switch = 1.0_dp/maxval(velrat)
21+
rate = sum(efcolf*enrat)*2.0_dp*0.75225278_dp
22+
print '(a,es10.3,a,es10.3)', 'p_switch = ', p_switch, ', rate = ', rate
23+
24+
call check_tiny_speed_start()
25+
call check_generator_moments()
26+
call check_maxwellian()
27+
print *, 'PASS: low-speed collision substep'
28+
contains
29+
30+
subroutine setup_cold_plasma()
31+
! H test ion in a 20 eV H plasma with 2.5% C6+, n_e = 2e19 m^-3.
32+
real(dp) :: ni1, ni2
33+
integer :: node
34+
ni1 = 2.0e13_dp/(1.0_dp + 6.0_dp*0.025_dp)
35+
ni2 = 0.025_dp*ni1
36+
call compute_collision_coeffs(1.0_dp, 12.0_dp, 1.0_dp, 6.0_dp, ni1, ni2, &
37+
temp, temp, temp, ealpha, v0, &
38+
efcolf, velrat, enrat)
39+
do node = 1, N_S_GRID
40+
efcolf_grid(:, node) = efcolf
41+
velrat_grid(:, node) = velrat
42+
enrat_grid(:, node) = enrat
43+
end do
44+
end subroutine setup_cold_plasma
45+
46+
subroutine check_tiny_speed_start()
47+
! Before the Cartesian substep, a start at p = 1e-9 or 1e-12 ended
48+
! immediately with ierr = 4 (substep ~ p**2 below ulp of the interval).
49+
real(dp), parameter :: starts(2) = [1.0e-9_dp, 1.0e-12_dp]
50+
integer :: k, mode, ierr
51+
real(dp) :: z(5)
52+
do mode = 1, 2
53+
do k = 1, 2
54+
z = [0.5_dp, 0.2_dp, 0.3_dp, starts(k), 0.4_dp]
55+
call stost(z, 6.76_dp, mode, ierr)
56+
if (ierr /= 0) error stop 'Tiny-speed start returned an error'
57+
if (.not. all(z == z) .or. z(4) <= 0.0_dp .or. &
58+
abs(z(5)) > 1.0_dp) error stop 'Tiny-speed start left bad state'
59+
end do
60+
end do
61+
end subroutine check_tiny_speed_start
62+
63+
subroutine check_generator_moments()
64+
! One substep from either side of p_switch: first moments of p and
65+
! lambda must match fpeff*h and -2*dhh*lambda*h.
66+
integer, parameter :: n = 400000
67+
real(dp), parameter :: lam = 0.5_dp
68+
real(dp) :: fac(3) = [0.5_dp, 0.98_dp, 1.02_dp]
69+
real(dp) :: p, h, dpp, dhh, fpeff, z(5), dp1, dl1, m1, m2, s1, s2
70+
integer :: k, i, ierr
71+
seed = 77
72+
call random_seed(put=seed)
73+
do k = 1, size(fac)
74+
p = fac(k)*p_switch
75+
if (k == 3) p = 1.02_dp*p_switch
76+
call coleff(p, dpp, dhh, fpeff)
77+
h = 0.5_dp*min(0.01_dp/dhh, 0.1_dp*p/abs(fpeff), &
78+
0.01_dp*p**2/(6.0_dp*dpp))
79+
m1 = 0.0_dp
80+
m2 = 0.0_dp
81+
s1 = 0.0_dp
82+
s2 = 0.0_dp
83+
do i = 1, n
84+
z = [0.5_dp, 0.2_dp, 0.3_dp, p, lam]
85+
call stost(z, h, 1, ierr)
86+
if (ierr /= 0) error stop 'Generator substep failed'
87+
dp1 = z(4) - p
88+
dl1 = z(5) - lam
89+
m1 = m1 + dp1
90+
s1 = s1 + dp1**2
91+
m2 = m2 + dl1
92+
s2 = s2 + dl1**2
93+
end do
94+
m1 = m1/n
95+
m2 = m2/n
96+
s1 = sqrt((s1/n - m1**2)/n)
97+
s2 = sqrt((s2/n - m2**2)/n)
98+
print '(a,f6.3,4es12.4)', 'moments p/p_switch, dp: ', p/p_switch, &
99+
m1, fpeff*h, m2, -2.0_dp*dhh*lam*h
100+
if (abs(m1 - fpeff*h) > 5.0_dp*s1) &
101+
error stop 'Speed drift disagrees with fpeff'
102+
if (abs(m2 + 2.0_dp*dhh*lam*h) > 5.0_dp*s2) &
103+
error stop 'Pitch drift disagrees with -2*dhh*lambda'
104+
end do
105+
end subroutine check_generator_moments
106+
107+
subroutine check_maxwellian()
108+
integer, parameter :: n = 4096
109+
real(dp) :: z(5), sp2, sl, sl2, target, sig, duration
110+
integer :: i, ierr
111+
duration = 6.0_dp/rate
112+
seed = 20261023
113+
call random_seed(put=seed)
114+
sp2 = 0.0_dp
115+
sl = 0.0_dp
116+
sl2 = 0.0_dp
117+
do i = 1, n
118+
z = [0.5_dp, 0.2_dp, 0.3_dp, 1.0e-6_dp, 0.0_dp]
119+
call stost(z, duration, 1, ierr)
120+
if (ierr /= 0) error stop 'Thermalization ended in an error'
121+
sp2 = sp2 + z(4)**2/n
122+
sl = sl + z(5)/n
123+
sl2 = sl2 + z(5)**2/n
124+
end do
125+
target = 1.5_dp*temp/ealpha
126+
! Maxwellian: var(E) = 1.5*T**2, var(lambda) = 1/3, var(lambda**2) = 4/45.
127+
sig = sqrt(1.5_dp)*temp/ealpha/sqrt(real(n, dp))
128+
print '(a,3es12.4)', '<p^2>, target, sigma: ', sp2, target, sig
129+
print '(a,2es12.4)', '<lambda>, <lambda^2>: ', sl, sl2
130+
if (abs(sp2 - target) > 5.0_dp*sig) error stop '<p^2> off Maxwellian'
131+
if (abs(sl) > 5.0_dp*sqrt(1.0_dp/3.0_dp/n)) error stop '<lambda> not 0'
132+
if (abs(sl2 - 1.0_dp/3.0_dp) > 5.0_dp*sqrt(4.0_dp/45.0_dp/n)) &
133+
error stop '<lambda^2> not 1/3'
134+
! Subthermal starts never error, from many seeds.
135+
do i = 1, 2000
136+
seed = 1000 + 7*i
137+
call random_seed(put=seed)
138+
z = [0.5_dp, 0.2_dp, 0.3_dp, 0.5_dp*p_switch, 0.3_dp]
139+
call stost(z, 6.76_dp, 1, ierr)
140+
if (ierr /= 0) error stop 'Subthermal marker errored'
141+
end do
142+
end subroutine check_maxwellian
143+
end program test_collision_low_speed

0 commit comments

Comments
 (0)