Skip to content

Commit d132c7e

Browse files
Added particle beds as a feature
1 parent c862070 commit d132c7e

9 files changed

Lines changed: 400 additions & 4 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import json
2+
import math
3+
4+
# 2D shock wave interacting with a bed of 20 free-floating circular particles.
5+
6+
gam_a = 1.4
7+
8+
# Shock parameters (Mach 1.5)
9+
mach_number = 1.5
10+
pre_shock_pressure = 1
11+
pre_shock_density = 1.4
12+
pre_shock_speed = 0.0
13+
post_shock_pressure = 2.4583
14+
post_shock_density = 2.6069
15+
post_shock_speed = 0.6944
16+
17+
domain_size = 4.0
18+
wave_front = -1.5
19+
20+
total_time = 1.5
21+
num_time_steps = 2000
22+
dt = float(total_time / num_time_steps)
23+
num_saves = 100
24+
steps_to_save = int(num_time_steps / num_saves)
25+
26+
# Soft-sphere collision parameters (from 3D_mibm_sphere_head_on_collision)
27+
collision_time = 20.0 * dt
28+
29+
# Particle bed parameters
30+
bed_x = -0.5
31+
bed_y = 0.0
32+
bed_lx = 1.
33+
bed_ly = 3.5
34+
particle_radius = 0.15
35+
particle_mass = 0.25
36+
particle_min_spacing = 0.02
37+
38+
print(
39+
json.dumps(
40+
{
41+
# Logistics
42+
"run_time_info": "T",
43+
# Computational Domain Parameters
44+
"x_domain%beg": -domain_size * 0.5,
45+
"x_domain%end": domain_size * 0.5,
46+
"y_domain%beg": -domain_size * 0.5,
47+
"y_domain%end": domain_size * 0.5,
48+
"cyl_coord": "F",
49+
"m": 512,
50+
"n": 512,
51+
"p": 0,
52+
"dt": dt,
53+
"t_step_start": 0,
54+
"t_step_stop": num_time_steps,
55+
"t_step_save": steps_to_save,
56+
# Simulation Algorithm Parameters
57+
"num_patches": 2,
58+
"model_eqns": 2,
59+
"alt_soundspeed": "F",
60+
"num_fluids": 1,
61+
"mpp_lim": "F",
62+
"mixture_err": "T",
63+
"time_stepper": 3,
64+
"weno_order": 5,
65+
"weno_eps": 1.0e-16,
66+
"weno_Re_flux": "T",
67+
"weno_avg": "T",
68+
"avg_state": 2,
69+
"mapped_weno": "T",
70+
"null_weights": "F",
71+
"mp_weno": "T",
72+
"riemann_solver": 2,
73+
"wave_speeds": 1,
74+
"bc_x%beg": -17,
75+
"bc_x%end": -8,
76+
"bc_y%beg": -15,
77+
"bc_y%end": -15,
78+
# Immersed boundaries — all circles come from the particle bed
79+
"ib": "T",
80+
"num_ibs": 0,
81+
"viscous": "T",
82+
# Collision model (soft-sphere, from 3D_mibm_sphere_head_on_collision)
83+
"collision_model": 1,
84+
"coefficient_of_restitution": 0.9,
85+
"collision_time": collision_time,
86+
"ib_coefficient_of_friction": 0.1,
87+
# Particle bed: 20 free-floating circles placed randomly in region
88+
"num_particle_beds": 1,
89+
"particle_bed(1)%x_centroid": bed_x,
90+
"particle_bed(1)%y_centroid": bed_y,
91+
"particle_bed(1)%z_centroid": 0.0,
92+
"particle_bed(1)%length_x": bed_lx,
93+
"particle_bed(1)%length_y": bed_ly,
94+
"particle_bed(1)%length_z": 0.0,
95+
"particle_bed(1)%num_particles": 20,
96+
"particle_bed(1)%radius": particle_radius,
97+
"particle_bed(1)%mass": particle_mass,
98+
"particle_bed(1)%min_spacing": particle_min_spacing,
99+
"particle_bed(1)%moving_ibm": 2,
100+
"particle_bed(1)%seed": 42,
101+
# Output
102+
"format": 1,
103+
"precision": 2,
104+
"prim_vars_wrt": "T",
105+
"E_wrt": "T",
106+
"ib_state_wrt": "F",
107+
"parallel_io": "T",
108+
# IC Patch 1: post-shock region (left of wave front)
109+
"patch_icpp(1)%geometry": 3,
110+
"patch_icpp(1)%x_centroid": 0.5 * wave_front - 0.25 * domain_size,
111+
"patch_icpp(1)%y_centroid": 0.0,
112+
"patch_icpp(1)%length_x": 0.5 * domain_size + wave_front,
113+
"patch_icpp(1)%length_y": domain_size,
114+
"patch_icpp(1)%vel(1)": post_shock_speed,
115+
"patch_icpp(1)%vel(2)": 0.0,
116+
"patch_icpp(1)%pres": post_shock_pressure,
117+
"patch_icpp(1)%alpha_rho(1)": post_shock_density,
118+
"patch_icpp(1)%alpha(1)": 1.0,
119+
# IC Patch 2: pre-shock region (right of wave front)
120+
"patch_icpp(2)%geometry": 3,
121+
"patch_icpp(2)%x_centroid": 0.5 * wave_front + 0.25 * domain_size,
122+
"patch_icpp(2)%y_centroid": 0.0,
123+
"patch_icpp(2)%length_x": 0.5 * domain_size - wave_front,
124+
"patch_icpp(2)%length_y": domain_size,
125+
"patch_icpp(2)%vel(1)": pre_shock_speed,
126+
"patch_icpp(2)%vel(2)": 0.0,
127+
"patch_icpp(2)%pres": pre_shock_pressure,
128+
"patch_icpp(2)%alpha_rho(1)": pre_shock_density,
129+
"patch_icpp(2)%alpha(1)": 1.0,
130+
# Fluid properties: air
131+
"fluid_pp(1)%gamma": 1.0 / (gam_a - 1.0),
132+
"fluid_pp(1)%pi_inf": 0,
133+
"fluid_pp(1)%Re(1)": 2500000,
134+
}
135+
)
136+
)

src/common/m_constants.fpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module m_constants
2626
integer, parameter :: num_patches_max = 10 !< Maximum number of IC patches
2727
integer, parameter :: num_ib_patches_max = 2050000 !< Maximum number of immersed boundary patches (patch_ib)
2828
integer, parameter :: num_local_ibs_max = 2000 !< Maximum number of immersed boundary patches (patch_ib)
29+
integer, parameter :: num_particle_beds_max = 10 !< Maximum number of particle bed patch specifications
2930
integer, parameter :: num_bc_patches_max = 10 !< Maximum number of boundary condition patches
3031
integer, parameter :: max_2d_fourier_modes = 10 !< Max Fourier mode index for 2D modal patch (geometry 13)
3132
integer, parameter :: max_sph_harm_degree = 5 !< Max degree L for 3D spherical harmonic patch (geometry 14)

src/simulation/m_global_parameters.fpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,20 @@ module m_global_parameters
353353
logical :: ib_state_wrt
354354
type(ib_patch_parameters), allocatable, dimension(:) :: patch_ib !< Immersed boundary patch parameters
355355
integer, dimension(num_local_ibs_max) :: local_ib_patch_ids !< lookup table of IBs in the local compute domain
356+
357+
type particle_bed_parameters
358+
real(wp) :: x_centroid, y_centroid, z_centroid !< Center of the particle bed region
359+
real(wp) :: length_x, length_y, length_z !< Dimensions of the particle bed region
360+
integer :: num_particles !< Number of particles to generate
361+
real(wp) :: radius !< Particle radius
362+
real(wp) :: mass !< Particle mass
363+
real(wp) :: min_spacing !< Minimum surface-to-surface gap (particle centers are 2*radius + min_spacing apart)
364+
integer :: moving_ibm !< Motion flag: 0=static, 1=moving (forces), 2=forced path
365+
integer :: seed !< Random seed for reproducible placement
366+
end type particle_bed_parameters
367+
368+
integer :: num_particle_beds !< Number of particle bed specifications
369+
type(particle_bed_parameters), dimension(num_particle_beds_max) :: particle_bed !< Particle bed specifications
356370
integer, allocatable, dimension(:,:,:) :: ib_neighbor_ranks !< MPI ranks of neighborhood domains, indexed (-N:N,-N:N,-N:N)
357371
type(vec3_dt), allocatable, dimension(:) :: airfoil_grid_u, airfoil_grid_l
358372
integer :: Np
@@ -799,6 +813,22 @@ contains
799813
relativity = .false.
800814
#:endif
801815
816+
num_particle_beds = 0
817+
do i = 1, num_particle_beds_max
818+
particle_bed(i)%x_centroid = 0._wp
819+
particle_bed(i)%y_centroid = 0._wp
820+
particle_bed(i)%z_centroid = 0._wp
821+
particle_bed(i)%length_x = dflt_real
822+
particle_bed(i)%length_y = dflt_real
823+
particle_bed(i)%length_z = dflt_real
824+
particle_bed(i)%num_particles = 0
825+
particle_bed(i)%radius = dflt_real
826+
particle_bed(i)%mass = dflt_real
827+
particle_bed(i)%min_spacing = 0._wp
828+
particle_bed(i)%moving_ibm = 0
829+
particle_bed(i)%seed = 0
830+
end do
831+
802832
allocate (patch_ib(num_ib_patches_max))
803833
do i = 1, num_ib_patches_max
804834
patch_ib(i)%gbl_patch_id = i

src/simulation/m_mpi_proxy.fpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ contains
7474
& 'wave_speeds', 'avg_state', 'precision', 'bc_x%beg', 'bc_x%end', &
7575
& 'bc_y%beg', 'bc_y%end', 'bc_z%beg', 'bc_z%end', 'fd_order', &
7676
& 'num_probes', 'num_integrals', 'bubble_model', 'thermal', &
77-
& 'num_source', 'relax_model', 'num_ibs', 'n_start', &
77+
& 'num_source', 'relax_model', 'num_ibs', 'num_particle_beds', 'n_start', &
7878
& 'num_bc_patches', 'num_igr_iters', 'num_igr_warm_start_iters', &
7979
& 'adap_dt_max_iters', 'collision_model', 'ib_neighborhood_radius', &
8080
& 'int_comp' ]
@@ -207,6 +207,16 @@ contains
207207
call MPI_BCAST(patch_ib(i)%model_filepath, len(patch_ib(i)%model_filepath), MPI_CHARACTER, 0, MPI_COMM_WORLD, ierr)
208208
end do
209209

210+
do i = 1, num_particle_beds
211+
#:for VAR in ['x_centroid', 'y_centroid', 'z_centroid', 'length_x', 'length_y', 'length_z', &
212+
& 'radius', 'mass', 'min_spacing']
213+
call MPI_BCAST(particle_bed(i)%${VAR}$, 1, mpi_p, 0, MPI_COMM_WORLD, ierr)
214+
#:endfor
215+
call MPI_BCAST(particle_bed(i)%num_particles, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
216+
call MPI_BCAST(particle_bed(i)%moving_ibm, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
217+
call MPI_BCAST(particle_bed(i)%seed, 1, MPI_INTEGER, 0, MPI_COMM_WORLD, ierr)
218+
end do
219+
210220
do j = 1, num_probes_max
211221
do i = 1, 3
212222
call MPI_BCAST(acoustic(j)%loc(i), 1, mpi_p, 0, MPI_COMM_WORLD, ierr)

src/simulation/m_particle_bed.fpp

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
!>
2+
!! @file m_particle_bed.fpp
3+
!! @brief Generates particle beds: converts particle_bed specifications into
4+
!! individual sphere/circle patch_ib entries before MPI broadcast.
5+
6+
module m_particle_bed
7+
8+
use m_global_parameters
9+
use m_constants
10+
11+
implicit none
12+
13+
private
14+
15+
public :: s_generate_particle_beds
16+
17+
contains
18+
19+
!> Generate all particle beds and append the resulting particles to patch_ib.
20+
!! Called on rank 0 only, before s_mpi_bcast_user_inputs.
21+
!! Uses a spatial hash grid (cell size = min_dist) so each candidate requires
22+
!! only 3^dim distance checks on average instead of O(n).
23+
impure subroutine s_generate_particle_beds()
24+
25+
integer :: b, ib_idx, geom
26+
integer :: n_placed, n_attempts, max_attempts
27+
real(wp) :: xmin, xmax, ymin, ymax, zmin, zmax, min_dist
28+
real(wp) :: rx, ry, rz, dist
29+
integer :: seed
30+
logical :: overlaps
31+
real(wp), allocatable :: placed(:, :)
32+
33+
! Spatial hash grid
34+
integer :: hash_size, slot
35+
integer :: bx, by, bz, nbx, nby, nbz
36+
integer :: dx_b, dy_b, dz_b, dz_lo, dz_hi, j
37+
integer, allocatable :: hash_head(:), chain_next(:)
38+
39+
if (num_particle_beds == 0) return
40+
41+
do b = 1, num_particle_beds
42+
xmin = particle_bed(b)%x_centroid - 0.5_wp*particle_bed(b)%length_x
43+
xmax = particle_bed(b)%x_centroid + 0.5_wp*particle_bed(b)%length_x
44+
ymin = particle_bed(b)%y_centroid - 0.5_wp*particle_bed(b)%length_y
45+
ymax = particle_bed(b)%y_centroid + 0.5_wp*particle_bed(b)%length_y
46+
zmin = particle_bed(b)%z_centroid - 0.5_wp*particle_bed(b)%length_z
47+
zmax = particle_bed(b)%z_centroid + 0.5_wp*particle_bed(b)%length_z
48+
49+
min_dist = 2._wp*particle_bed(b)%radius + particle_bed(b)%min_spacing
50+
51+
if (p == 0) then
52+
geom = 2 ! circle for 2D
53+
dz_lo = 0
54+
dz_hi = 0
55+
else
56+
geom = 8 ! sphere for 3D
57+
dz_lo = -1
58+
dz_hi = 1
59+
end if
60+
61+
max_attempts = particle_bed(b)%num_particles*10000
62+
n_placed = 0
63+
n_attempts = 0
64+
seed = particle_bed(b)%seed
65+
if (seed == 0) seed = 1 + b*1013904223
66+
67+
allocate (placed(3, particle_bed(b)%num_particles))
68+
69+
! Hash table: 4x overprovisioned for ~25% load factor, minimum 16 buckets.
70+
! chain_next(i) links placed particle i to the previous occupant of its bucket.
71+
hash_size = max(16, 4*particle_bed(b)%num_particles)
72+
allocate (hash_head(hash_size))
73+
allocate (chain_next(particle_bed(b)%num_particles))
74+
hash_head = -1
75+
chain_next = -1
76+
77+
do while (n_placed < particle_bed(b)%num_particles .and. n_attempts < max_attempts)
78+
n_attempts = n_attempts + 1
79+
80+
rx = xmin + f_xorshift(seed)*(xmax - xmin)
81+
ry = ymin + f_xorshift(seed)*(ymax - ymin)
82+
if (p == 0) then
83+
rz = particle_bed(b)%z_centroid
84+
else
85+
rz = zmin + f_xorshift(seed)*(zmax - zmin)
86+
end if
87+
88+
bx = int(floor(rx/min_dist))
89+
by = int(floor(ry/min_dist))
90+
bz = 0
91+
if (p /= 0) bz = int(floor(rz/min_dist))
92+
93+
! Check 3x3(x3) neighboring bins — O(1) average via hash lookup
94+
overlaps = .false.
95+
outer: do dx_b = -1, 1
96+
do dy_b = -1, 1
97+
do dz_b = dz_lo, dz_hi
98+
nbx = bx + dx_b
99+
nby = by + dy_b
100+
nbz = bz + dz_b
101+
slot = f_bin_hash(nbx, nby, nbz, hash_size)
102+
j = hash_head(slot)
103+
do while (j > 0)
104+
if (p == 0) then
105+
dist = sqrt((rx - placed(1, j))**2 + (ry - placed(2, j))**2)
106+
else
107+
dist = sqrt((rx - placed(1, j))**2 + (ry - placed(2, j))**2 &
108+
+ (rz - placed(3, j))**2)
109+
end if
110+
if (dist < min_dist) then
111+
overlaps = .true.
112+
exit outer
113+
end if
114+
j = chain_next(j)
115+
end do
116+
end do
117+
end do
118+
end do outer
119+
120+
if (.not. overlaps) then
121+
n_placed = n_placed + 1
122+
placed(1, n_placed) = rx
123+
placed(2, n_placed) = ry
124+
placed(3, n_placed) = rz
125+
126+
! Insert into hash grid as head of bucket chain
127+
slot = f_bin_hash(bx, by, bz, hash_size)
128+
chain_next(n_placed) = hash_head(slot)
129+
hash_head(slot) = n_placed
130+
131+
num_ibs = num_ibs + 1
132+
ib_idx = num_ibs
133+
134+
patch_ib(ib_idx)%gbl_patch_id = ib_idx
135+
patch_ib(ib_idx)%geometry = geom
136+
patch_ib(ib_idx)%x_centroid = rx
137+
patch_ib(ib_idx)%y_centroid = ry
138+
patch_ib(ib_idx)%z_centroid = rz
139+
patch_ib(ib_idx)%radius = particle_bed(b)%radius
140+
patch_ib(ib_idx)%mass = particle_bed(b)%mass
141+
patch_ib(ib_idx)%moving_ibm = particle_bed(b)%moving_ibm
142+
end if
143+
end do
144+
145+
if (n_placed < particle_bed(b)%num_particles) then
146+
print '("WARNING: particle_bed(",I0,"): placed ",I0," of ",I0," particles after ",I0," attempts")', &
147+
b, n_placed, particle_bed(b)%num_particles, n_attempts
148+
end if
149+
150+
deallocate (placed, hash_head, chain_next)
151+
end do
152+
153+
end subroutine s_generate_particle_beds
154+
155+
!> Xorshift PRNG. Advances seed in-place and returns a value in [0, 1).
156+
function f_xorshift(seed) result(rval)
157+
158+
integer, intent(inout) :: seed
159+
real(wp) :: rval
160+
161+
seed = ieor(seed, ishft(seed, 13))
162+
seed = ieor(seed, ishft(seed, -17))
163+
seed = ieor(seed, ishft(seed, 5))
164+
165+
rval = abs(real(seed, wp))/real(huge(seed), wp)
166+
167+
end function f_xorshift
168+
169+
!> Hash bin coordinates to a 1-indexed slot in [1, hash_size].
170+
!! Uses large prime multipliers to spread bins across buckets.
171+
!! Hash collisions are benign: the distance check catches false neighbours.
172+
function f_bin_hash(bx, by, bz, hash_size) result(slot)
173+
174+
integer, intent(in) :: bx, by, bz, hash_size
175+
integer :: slot
176+
integer(8) :: key
177+
178+
key = ieor(ieor(int(bx, 8)*73856093_8, int(by, 8)*19349663_8), int(bz, 8)*83492791_8)
179+
slot = int(mod(abs(key), int(hash_size, 8))) + 1
180+
181+
end function f_bin_hash
182+
183+
end module m_particle_bed

0 commit comments

Comments
 (0)