MFC
Exascale flow solver
Loading...
Searching...
No Matches
m_particle_cloud.fpp.f90
Go to the documentation of this file.
1# 1 "/home/runner/work/MFC/MFC/src/simulation/m_particle_cloud.fpp"
2!>
3!! @file m_particle_cloud.fpp
4!! @brief Generates particle beds: converts particle_cloud specifications into
5!! individual sphere/circle particle_cloud_ibs entries before reduction.
6
7!> @brief Generates particle beds by converting particle_cloud patch specifications into individual immersed boundary patches before
8!! domain reduction. Each rank runs the same deterministic placement so no MPI broadcast of particle positions is needed.
10
12 use m_constants
13 use m_mpi_common
14
15 implicit none
16
17 private
18
20
21contains
22
23 !> Generate all particle beds and fill particle_cloud_ibs. Called on all ranks before s_reduce_ib_patch_array.
24 impure subroutine s_generate_particle_clouds(particle_cloud_ibs)
25
26 type(ib_patch_parameters), allocatable, intent(out), dimension(:) :: particle_cloud_ibs
27 integer :: cloud_idx, ib_idx, n_total_particles
28 real(wp) :: t_start, t_end
29
30 if (num_particle_clouds == 0) then
31 allocate (particle_cloud_ibs(0))
32 return
33 end if
34
35 call cpu_time(t_start)
36
37 ! Pre-count total particles across all beds so particle_cloud_ibs can be allocated exactly once.
38 n_total_particles = 0
39 do cloud_idx = 1, num_particle_clouds
40 n_total_particles = n_total_particles + particle_cloud(cloud_idx)%num_particles
41 end do
42 allocate (particle_cloud_ibs(n_total_particles))
43
44 ib_idx = 0 ! index into particle_cloud_ibs
45
46 do cloud_idx = 1, num_particle_clouds
47 select case (particle_cloud(cloud_idx)%packing_method)
48 case (1) ! random box packing method
49 call s_particle_cloud_random_box(cloud_idx, ib_idx, particle_cloud_ibs)
50 case (2) ! lattice packing method
51 call s_particle_cloud_lattice(cloud_idx, ib_idx, particle_cloud_ibs)
52 end select
53 end do
54
55 call cpu_time(t_end)
56 if (proc_rank == 0) print '(a,i0,a,f0.3,a)', 'Particle beds placed ', ib_idx, ' particles in ', t_end - t_start, ' seconds.'
57
58 end subroutine s_generate_particle_clouds
59
60 !> Generates a random distributions of particles in a box with a minimum spacing
61 subroutine s_particle_cloud_random_box(cloud_idx, ib_idx, particle_cloud_ibs)
62
63 integer, intent(in) :: cloud_idx
64 integer, intent(inout) :: ib_idx
65 type(ib_patch_parameters), intent(inout), dimension(:) :: particle_cloud_ibs
66 integer :: n_placed, geom, seed
67 integer(8) :: n_attempts, max_attempts
68 real(wp) :: xmin, xmax, ymin, ymax, zmin, zmax, min_dist
69 real(wp) :: rx, ry, rz, dist
70 logical :: overlaps
71 real(wp), allocatable :: placed(:,:)
72 integer :: hash_size, slot
73 integer :: bx, by, bz, nbx, nby, nbz
74 integer :: dx_b, dy_b, dz_b, dz_lo, dz_hi, j
75 integer, allocatable :: hash_head(:), chain_next(:)
76
77 xmin = particle_cloud(cloud_idx)%x_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_x
78 xmax = particle_cloud(cloud_idx)%x_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_x
79 ymin = particle_cloud(cloud_idx)%y_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_y
80 ymax = particle_cloud(cloud_idx)%y_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_y
81 zmin = particle_cloud(cloud_idx)%z_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_z
82 zmax = particle_cloud(cloud_idx)%z_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_z
83
84 min_dist = 2._wp*particle_cloud(cloud_idx)%radius + particle_cloud(cloud_idx)%min_spacing
85
86 if (p == 0) then
87 geom = 2 ! circle for 2D
88 dz_lo = 0
89 dz_hi = 0
90 else
91 geom = 8 ! sphere for 3D
92 dz_lo = -1
93 dz_hi = 1
94 end if
95
96 max_attempts = int(particle_cloud(cloud_idx)%num_particles, 8)*1000_8
97 n_placed = 0
98 n_attempts = 0
99 seed = particle_cloud(cloud_idx)%seed
100 if (seed == 0) seed = 1 + cloud_idx*1013904223
101
102 allocate (placed(3, particle_cloud(cloud_idx)%num_particles))
103
104 ! Hash table: 4x overprovisioned for ~25% load factor, minimum 16 buckets. chain_next(i) links placed particle i to the
105 ! previous occupant of its bucket.
106 hash_size = max(16, 4*particle_cloud(cloud_idx)%num_particles)
107 allocate (hash_head(hash_size))
108 allocate (chain_next(particle_cloud(cloud_idx)%num_particles))
109 hash_head = -1
110 chain_next = -1
111
112 do while (n_placed < particle_cloud(cloud_idx)%num_particles .and. n_attempts < max_attempts)
113 n_attempts = n_attempts + 1
114
115 rx = xmin + f_xorshift(seed)*(xmax - xmin)
116 ry = ymin + f_xorshift(seed)*(ymax - ymin)
117 if (p == 0) then
118 rz = particle_cloud(cloud_idx)%z_centroid
119 else
120 rz = zmin + f_xorshift(seed)*(zmax - zmin)
121 end if
122
123 bx = int(floor(rx/min_dist))
124 by = int(floor(ry/min_dist))
125 bz = 0
126 if (p /= 0) bz = int(floor(rz/min_dist))
127
128 ! Check 3x3(x3) neighboring bins - O(1) average via hash lookup
129 overlaps = .false.
130 outer: do dx_b = -1, 1
131 do dy_b = -1, 1
132 do dz_b = dz_lo, dz_hi
133 nbx = bx + dx_b
134 nby = by + dy_b
135 nbz = bz + dz_b
136 slot = f_bin_hash(nbx, nby, nbz, hash_size)
137 j = hash_head(slot)
138 do while (j > 0)
139 if (p == 0) then
140 dist = sqrt((rx - placed(1, j))**2 + (ry - placed(2, j))**2)
141 else
142 dist = sqrt((rx - placed(1, j))**2 + (ry - placed(2, j))**2 + (rz - placed(3, j))**2)
143 end if
144 if (dist < min_dist) then
145 overlaps = .true.
146 exit outer
147 end if
148 j = chain_next(j)
149 end do
150 end do
151 end do
152 end do outer
153
154 if (.not. overlaps) then
155 n_placed = n_placed + 1
156 placed(1, n_placed) = rx
157 placed(2, n_placed) = ry
158 placed(3, n_placed) = rz
159
160 ! Insert into hash grid as head of bucket chain
161 slot = f_bin_hash(bx, by, bz, hash_size)
162 chain_next(n_placed) = hash_head(slot)
163 hash_head(slot) = n_placed
164
165 call s_add_cloud_particle(cloud_idx, ib_idx, geom, rx, ry, rz, particle_cloud_ibs)
166 end if
167 end do
168
169 if (n_placed < particle_cloud(cloud_idx)%num_particles) then
170 call s_mpi_abort("Error :: Failed to place all particles in particle bed")
171 end if
172
173 deallocate (placed, hash_head, chain_next)
174
175 end subroutine s_particle_cloud_random_box
176
177 !> Places particles on the optimally dense lattice for the cloud region: a triangular lattice in 2D, a face-centered cubic
178 !! lattice in 3D. The lattice spacing is set by the particle density (num_particles over the region area/volume); if that
179 !! spacing falls below the required centre-to-centre distance (2*radius + min_spacing), the region is too dense and the run is
180 !! aborted.
181 subroutine s_particle_cloud_lattice(cloud_idx, ib_idx, particle_cloud_ibs)
182
183 integer, intent(in) :: cloud_idx
184 integer, intent(inout) :: ib_idx
185 type(ib_patch_parameters), intent(inout), dimension(:) :: particle_cloud_ibs
186 integer :: n_placed, n_target, geom
187 integer :: row, col, ncx, ncy, ix, jy, kz, b
188 real(wp) :: xmin, xmax, ymin, ymax, zmin, zmax, min_dist
189 real(wp) :: spacing, row_dy, cell, x0, px, py
190 real(wp), dimension(4) :: bx_off, by_off, bz_off
191
192 xmin = particle_cloud(cloud_idx)%x_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_x
193 xmax = particle_cloud(cloud_idx)%x_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_x
194 ymin = particle_cloud(cloud_idx)%y_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_y
195 ymax = particle_cloud(cloud_idx)%y_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_y
196 zmin = particle_cloud(cloud_idx)%z_centroid - 0.5_wp*particle_cloud(cloud_idx)%length_z
197 zmax = particle_cloud(cloud_idx)%z_centroid + 0.5_wp*particle_cloud(cloud_idx)%length_z
198
199 min_dist = 2._wp*particle_cloud(cloud_idx)%radius + particle_cloud(cloud_idx)%min_spacing
200 n_target = particle_cloud(cloud_idx)%num_particles
201 n_placed = 0
202
203 if (p == 0) then
204 geom = 2 ! circle for 2D
205 ! Triangular lattice: area per particle = (sqrt(3)/2)*spacing**2.
206 spacing = sqrt(2._wp*(xmax - xmin)*(ymax - ymin)/(sqrt(3._wp)*real(n_target, wp)))
207 else
208 geom = 8 ! sphere for 3D
209 ! Face-centered cubic lattice: volume per particle = spacing**3/sqrt(2).
210 spacing = (sqrt(2._wp)*(xmax - xmin)*(ymax - ymin)*(zmax - zmin)/real(n_target, wp))**(1._wp/3._wp)
211 end if
212
213 if (spacing < min_dist) then
214 call s_mpi_abort("Error :: Particle cloud is too dense for lattice packing; " &
215 & // "reduce num_particles or min_spacing, or enlarge the cloud region")
216 end if
217
218 if (p == 0) then
219 ! Triangular lattice: rows pitched by spacing*sqrt(3)/2, odd rows shifted by half a spacing.
220 row_dy = spacing*sqrt(3._wp)/2._wp
221 row = 0
222 do while (n_placed < n_target)
223 py = ymin + real(row, wp)*row_dy
224 x0 = xmin
225 if (mod(row, 2) == 1) x0 = xmin + 0.5_wp*spacing
226 col = 0
227 px = x0
228 do while (px <= xmax .and. n_placed < n_target)
229 call s_add_cloud_particle(cloud_idx, ib_idx, geom, px, py, particle_cloud(cloud_idx)%z_centroid, &
230 & particle_cloud_ibs)
231 n_placed = n_placed + 1
232 col = col + 1
233 px = x0 + real(col, wp)*spacing
234 end do
235 row = row + 1
236 end do
237 else
238 ! Face-centered cubic lattice via the conventional cubic cell (side = spacing*sqrt(2)) and its four basis points.
239 cell = spacing*sqrt(2._wp)
240 bx_off = [0._wp, 0.5_wp, 0.5_wp, 0._wp]*cell
241 by_off = [0._wp, 0.5_wp, 0._wp, 0.5_wp]*cell
242 bz_off = [0._wp, 0._wp, 0.5_wp, 0.5_wp]*cell
243 ncx = max(1, ceiling((xmax - xmin)/cell))
244 ncy = max(1, ceiling((ymax - ymin)/cell))
245 kz = 0
246 do while (n_placed < n_target)
247 do jy = 0, ncy - 1
248 do ix = 0, ncx - 1
249 do b = 1, 4
250 if (n_placed >= n_target) exit
251 call s_add_cloud_particle(cloud_idx, ib_idx, geom, xmin + real(ix, wp)*cell + bx_off(b), &
252 & ymin + real(jy, wp)*cell + by_off(b), zmin + real(kz, &
253 & wp)*cell + bz_off(b), particle_cloud_ibs)
254 n_placed = n_placed + 1
255 end do
256 end do
257 end do
258 kz = kz + 1
259 end do
260 end if
261
262 end subroutine s_particle_cloud_lattice
263
264 !> Writes a single placed particle into particle_cloud_ibs at the next free slot, advancing ib_idx. Shared by all packing
265 !! methods so the per-particle ib_patch_parameters setup stays in one place.
266 subroutine s_add_cloud_particle(cloud_idx, ib_idx, geom, px, py, pz, particle_cloud_ibs)
267
268 integer, intent(in) :: cloud_idx, geom
269 integer, intent(inout) :: ib_idx
270 real(wp), intent(in) :: px, py, pz
271 type(ib_patch_parameters), intent(inout), dimension(:) :: particle_cloud_ibs
272
273 ib_idx = ib_idx + 1
274
275 ! gbl_patch_id is relative within particle_cloud_ibs here; s_reduce_ib_patch_array adjusts to global indexing.
276 particle_cloud_ibs(ib_idx)%gbl_patch_id = ib_idx
277 particle_cloud_ibs(ib_idx)%geometry = geom
278 particle_cloud_ibs(ib_idx)%x_centroid = px
279 particle_cloud_ibs(ib_idx)%y_centroid = py
280 particle_cloud_ibs(ib_idx)%z_centroid = pz
281 particle_cloud_ibs(ib_idx)%step_x_centroid = px
282 particle_cloud_ibs(ib_idx)%step_y_centroid = py
283 particle_cloud_ibs(ib_idx)%step_z_centroid = pz
284 particle_cloud_ibs(ib_idx)%angles(:) = 0._wp
285 particle_cloud_ibs(ib_idx)%step_angles(:) = 0._wp
286 particle_cloud_ibs(ib_idx)%vel(:) = 0._wp
287 particle_cloud_ibs(ib_idx)%step_vel(:) = 0._wp
288 particle_cloud_ibs(ib_idx)%angular_vel(:) = 0._wp
289 particle_cloud_ibs(ib_idx)%step_angular_vel(:) = 0._wp
290 particle_cloud_ibs(ib_idx)%force(:) = 0._wp
291 particle_cloud_ibs(ib_idx)%torque(:) = 0._wp
292 particle_cloud_ibs(ib_idx)%centroid_offset(:) = 0._wp
293 particle_cloud_ibs(ib_idx)%rotation_matrix = 0._wp
294 particle_cloud_ibs(ib_idx)%rotation_matrix(1, 1) = 1._wp
295 particle_cloud_ibs(ib_idx)%rotation_matrix(2, 2) = 1._wp
296 particle_cloud_ibs(ib_idx)%rotation_matrix(3, 3) = 1._wp
297 particle_cloud_ibs(ib_idx)%rotation_matrix_inverse = particle_cloud_ibs(ib_idx)%rotation_matrix
298 particle_cloud_ibs(ib_idx)%radius = particle_cloud(cloud_idx)%radius
299 particle_cloud_ibs(ib_idx)%mass = particle_cloud(cloud_idx)%mass
300 particle_cloud_ibs(ib_idx)%moment = dflt_real
301 particle_cloud_ibs(ib_idx)%moving_ibm = particle_cloud(cloud_idx)%moving_ibm
302 particle_cloud_ibs(ib_idx)%slip = .false.
303
304 end subroutine s_add_cloud_particle
305
306 !> Xorshift PRNG. Advances seed in-place and returns a value in [0, 1).
307 function f_xorshift(seed) result(rval)
308
309 integer, intent(inout) :: seed
310 real(wp) :: rval
311
312 seed = ieor(seed, ishft(seed, 13))
313 seed = ieor(seed, ishft(seed, -17))
314 seed = ieor(seed, ishft(seed, 5))
315
316 rval = abs(real(seed, wp))/real(huge(seed), wp)
317
318 end function f_xorshift
319
320 !> Hash bin coordinates to a 1-indexed slot in [1, hash_size]. Uses large prime multipliers to spread bins across buckets. Hash
321 !! collisions are benign: the distance check catches false neighbours.
322 function f_bin_hash(bx, by, bz, hash_size) result(slot)
323
324 integer, intent(in) :: bx, by, bz, hash_size
325 integer :: slot
326 integer(8) :: key
327
328 key = ieor(ieor(int(bx, 8)*73856093_8, int(by, 8)*19349663_8), int(bz, 8)*83492791_8)
329 slot = int(mod(abs(key), int(hash_size, 8))) + 1
330
331 end function f_bin_hash
332
333end module m_particle_cloud
Compile-time constant parameters: default values, tolerances, and physical constants.
real(wp), parameter dflt_real
Default real value.
Global parameters for the computational domain, fluid properties, and simulation algorithm configurat...
integer proc_rank
Rank of the local processor.
MPI communication layer: domain decomposition, halo exchange, reductions, and parallel I/O setup.
impure subroutine s_mpi_abort(prnt, code)
The subroutine terminates the MPI execution environment.
Generates particle beds by converting particle_cloud patch specifications into individual immersed bo...
impure subroutine, public s_generate_particle_clouds(particle_cloud_ibs)
Generate all particle beds and fill particle_cloud_ibs. Called on all ranks before s_reduce_ib_patch_...
subroutine s_add_cloud_particle(cloud_idx, ib_idx, geom, px, py, pz, particle_cloud_ibs)
Writes a single placed particle into particle_cloud_ibs at the next free slot, advancing ib_idx....
real(wp) function f_xorshift(seed)
Xorshift PRNG. Advances seed in-place and returns a value in [0, 1).
integer function f_bin_hash(bx, by, bz, hash_size)
Hash bin coordinates to a 1-indexed slot in [1, hash_size]. Uses large prime multipliers to spread bi...
subroutine s_particle_cloud_lattice(cloud_idx, ib_idx, particle_cloud_ibs)
Places particles on the optimally dense lattice for the cloud region: a triangular lattice in 2D,...
subroutine s_particle_cloud_random_box(cloud_idx, ib_idx, particle_cloud_ibs)
Generates a random distributions of particles in a box with a minimum spacing.