forked from trixi-framework/TrixiParticles.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinitial_condition.jl
More file actions
449 lines (360 loc) · 19.3 KB
/
initial_condition.jl
File metadata and controls
449 lines (360 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
@doc raw"""
InitialCondition(; coordinates, density, velocity=zeros(size(coordinates, 1)),
mass=nothing, pressure=0.0, particle_spacing=-1.0)
Struct to hold the initial configuration of the particles.
The following setups return `InitialCondition`s for commonly used setups:
- [`RectangularShape`](@ref)
- [`SphereShape`](@ref)
- [`RectangularTank`](@ref)
- [`ComplexShape`](@ref)
- [`extrude_geometry`](@ref)
`InitialCondition`s support the set operations `union`, `setdiff` and `intersect` in order
to build more complex geometries.
`InitialCondition`s also support the set operations `setdiff` and `intersect` together
with `TrixiParticles.TriangleMesh` and `TrixiParticles.Polygon` returned by [`load_geometry`](@ref).
# Arguments
- `coordinates`: An array where the $i$-th column holds the coordinates of particle $i$.
- `density`: Either a vector holding the density of each particle,
or a function mapping each particle's coordinates to its density,
or a scalar for a constant density over all particles.
# Keywords
- `velocity`: Either an array where the $i$-th column holds the velocity of particle $i$,
or a function mapping each particle's coordinates to its velocity,
or, for a constant fluid velocity, a vector holding this velocity.
Velocity is constant zero by default.
- `mass`: Either `nothing` (default) to automatically compute particle mass from particle
density and spacing, or a vector holding the mass of each particle,
or a function mapping each particle's coordinates to its mass,
or a scalar for a constant mass over all particles.
- `pressure`: Either a vector holding the pressure of each particle,
or a function mapping each particle's coordinates to its pressure,
or a scalar for a constant pressure over all particles. This is optional and
only needed when using the [`EntropicallyDampedSPHSystem`](@ref).
- `particle_spacing`: The spacing between the particles. This is a scalar, as the spacing
is assumed to be uniform. This is only needed when using
set operations on the `InitialCondition` or for automatic mass calculation.
# Examples
```jldoctest; output = false
# Rectangle filled with particles
initial_condition = RectangularShape(0.1, (3, 4), (-1.0, 1.0), density=1.0)
# Two spheres in one initial condition
initial_condition = union(SphereShape(0.15, 0.5, (-1.0, 1.0), 1.0),
SphereShape(0.15, 0.2, (0.0, 1.0), 1.0))
# Rectangle with a spherical hole
shape1 = RectangularShape(0.1, (16, 13), (-0.8, 0.0), density=1.0)
shape2 = SphereShape(0.1, 0.35, (0.0, 0.6), 1.0, sphere_type=RoundSphere())
initial_condition = setdiff(shape1, shape2)
# Intersect of a rectangle with a sphere. Note that this keeps the particles of the
# rectangle that are in the intersect, while `intersect(shape2, shape1)` would consist of
# the particles of the sphere that are in the intersect.
shape1 = RectangularShape(0.1, (16, 13), (-0.8, 0.0), density=1.0)
shape2 = SphereShape(0.1, 0.35, (0.0, 0.6), 1.0, sphere_type=RoundSphere())
initial_condition = intersect(shape1, shape2)
# Set operations with geometries loaded from files
shape = RectangularShape(0.05, (20, 20), (0.0, 0.0), density=1.0)
file = pkgdir(TrixiParticles, "examples", "preprocessing", "data", "circle.asc")
geometry = load_geometry(file)
initial_condition_1 = intersect(shape, geometry)
initial_condition_2 = setdiff(shape, geometry)
# Build `InitialCondition` manually
coordinates = [0.0 1.0 1.0
0.0 0.0 1.0]
velocity = zero(coordinates)
mass = ones(3)
density = 1000 * ones(3)
initial_condition = InitialCondition(; coordinates, velocity, mass, density)
# With functions
initial_condition = InitialCondition(; coordinates, velocity=x -> 2x, mass=1.0, density=1000.0)
# output
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ InitialCondition{Float64} │
│ ═════════════════════════ │
│ #dimensions: ……………………………………………… 2 │
│ #particles: ………………………………………………… 3 │
│ particle spacing: ………………………………… -1.0 │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘
```
"""
struct InitialCondition{ELTYPE, MATRIX, VECTOR}
particle_spacing :: ELTYPE
coordinates :: MATRIX # Array{ELTYPE, 2}
velocity :: MATRIX # Array{ELTYPE, 2}
mass :: VECTOR # Array{ELTYPE, 1}
density :: VECTOR # Array{ELTYPE, 1}
pressure :: VECTOR # Array{ELTYPE, 1}
end
# The default constructor needs to be accessible for Adapt.jl to work with this struct.
# See the comments in general/gpu.jl for more details.
function InitialCondition(; coordinates, density, velocity=zeros(size(coordinates, 1)),
mass=nothing, pressure=0.0, particle_spacing=-1.0)
NDIMS = size(coordinates, 1)
return InitialCondition{NDIMS}(coordinates, velocity, mass, density,
pressure, particle_spacing)
end
# Function barrier to make `NDIMS` static and therefore SVectors type-stable
function InitialCondition{NDIMS}(coordinates, velocity, mass, density,
pressure, particle_spacing) where {NDIMS}
ELTYPE = eltype(coordinates)
n_particles = size(coordinates, 2)
if n_particles == 0
return InitialCondition(particle_spacing, coordinates, zeros(ELTYPE, NDIMS, 0),
zeros(ELTYPE, 0), zeros(ELTYPE, 0), zeros(ELTYPE, 0))
end
# SVector of coordinates to pass to functions.
# This will return a vector of SVectors in 2D and 3D, but an 1×n matrix in 1D.
coordinates_svector_ = reinterpret(reshape, SVector{NDIMS, ELTYPE}, coordinates)
# In 1D, this will reshape the 1×n matrix to a vector, in 2D/3D it will do nothing
coordinates_svector = reshape(coordinates_svector_, length(coordinates_svector_))
if velocity isa AbstractMatrix
velocities = velocity
else
# Assuming `velocity` is a scalar or a function
velocity_fun = wrap_function(velocity, Val(NDIMS))
if length(velocity_fun(coordinates_svector[1])) != NDIMS
throw(ArgumentError("`velocity` must be $NDIMS-dimensional " *
"for $NDIMS-dimensional `coordinates`"))
end
velocities_svector = velocity_fun.(coordinates_svector)
velocities = stack(velocities_svector)
end
if size(coordinates) != size(velocities)
throw(ArgumentError("`coordinates` and `velocities` must be of the same size"))
end
if density isa AbstractVector
if length(density) != n_particles
throw(ArgumentError("Expected: length(density) == size(coordinates, 2)\n" *
"Got: size(coordinates, 2) = $(size(coordinates, 2)), " *
"length(density) = $(length(density))"))
end
densities = density
else
density_fun = wrap_function(density, Val(NDIMS))
densities = density_fun.(coordinates_svector)
end
if any(densities .< eps())
throw(ArgumentError("density must be positive and larger than `eps()`"))
end
if pressure isa AbstractVector
if length(pressure) != n_particles
throw(ArgumentError("Expected: length(pressure) == size(coordinates, 2)\n" *
"Got: size(coordinates, 2) = $(size(coordinates, 2)), " *
"length(pressure) = $(length(pressure))"))
end
pressures = pressure
else
pressure_fun = wrap_function(pressure, Val(NDIMS))
pressures = pressure_fun.(coordinates_svector)
end
if mass isa AbstractVector
if length(mass) != n_particles
throw(ArgumentError("Expected: length(mass) == size(coordinates, 2)\n" *
"Got: size(coordinates, 2) = $(size(coordinates, 2)), " *
"length(mass) = $(length(mass))"))
end
masses = mass
elseif mass === nothing
if particle_spacing < 0
throw(ArgumentError("`mass` must be specified when not using `particle_spacing`"))
end
particle_volume = particle_spacing^NDIMS
masses = particle_volume * densities
else
mass_fun = wrap_function(mass, Val(NDIMS))
masses = mass_fun.(coordinates_svector)
end
return InitialCondition(particle_spacing, coordinates, ELTYPE.(velocities),
ELTYPE.(masses), ELTYPE.(densities), ELTYPE.(pressures))
end
function Base.show(io::IO, ic::InitialCondition)
@nospecialize ic # reduce precompilation time
print(io, "InitialCondition{$(eltype(ic))}()")
end
function Base.show(io::IO, ::MIME"text/plain", ic::InitialCondition)
@nospecialize ic # reduce precompilation time
if get(io, :compact, false)
show(io, system)
else
summary_header(io, "InitialCondition{$(eltype(ic))}")
summary_line(io, "#dimensions", "$(ndims(ic))")
summary_line(io, "#particles", "$(nparticles(ic))")
summary_line(io, "particle spacing", "$(first(ic.particle_spacing))")
summary_footer(io)
end
end
function wrap_function(function_::Function, ::Val)
# Already a function
return function_
end
function wrap_function(constant_scalar::Number, ::Val)
return coords -> constant_scalar
end
# For vectors and tuples
function wrap_function(constant_vector, ::Val{NDIMS}) where {NDIMS}
return coords -> SVector{NDIMS}(constant_vector)
end
@inline function Base.ndims(initial_condition::InitialCondition)
return size(initial_condition.coordinates, 1)
end
@inline function Base.eltype(initial_condition::InitialCondition)
return eltype(initial_condition.coordinates)
end
@inline nparticles(initial_condition::InitialCondition) = length(initial_condition.mass)
function Base.union(initial_condition::InitialCondition, initial_conditions...)
particle_spacing = initial_condition.particle_spacing
ic = first(initial_conditions)
if initial_condition.particle_spacing isa Vector || ic.particle_spacing isa Vector
throw(ArgumentError("`union` operation does not support non-uniform particle spacings. " *
"Please ensure all `InitialCondition`s use a scalar particle spacing."))
end
if ndims(ic) != ndims(initial_condition)
throw(ArgumentError("all passed initial conditions must have the same dimensionality"))
end
if particle_spacing < eps()
throw(ArgumentError("all passed initial conditions must store a particle spacing"))
end
if !isapprox(ic.particle_spacing, particle_spacing)
throw(ArgumentError("all passed initial conditions must have the same particle spacing"))
end
too_close = find_too_close_particles(ic.coordinates, initial_condition.coordinates,
0.75particle_spacing)
valid_particles = setdiff(eachparticle(ic), too_close)
coordinates = hcat(initial_condition.coordinates, ic.coordinates[:, valid_particles])
velocity = hcat(initial_condition.velocity, ic.velocity[:, valid_particles])
mass = vcat(initial_condition.mass, ic.mass[valid_particles])
density = vcat(initial_condition.density, ic.density[valid_particles])
pressure = vcat(initial_condition.pressure, ic.pressure[valid_particles])
result = InitialCondition{ndims(ic)}(coordinates, velocity, mass, density, pressure,
particle_spacing)
return union(result, Base.tail(initial_conditions)...)
end
Base.union(initial_condition::InitialCondition) = initial_condition
function Base.setdiff(initial_condition::InitialCondition, initial_conditions...)
ic = first(initial_conditions)
if initial_condition.particle_spacing isa Vector || ic.particle_spacing isa Vector
throw(ArgumentError("`setdiff` operation does not support non-uniform particle spacings. " *
"Please ensure all `InitialCondition`s use a scalar particle spacing."))
end
if ndims(ic) != ndims(initial_condition)
throw(ArgumentError("all passed initial conditions must have the same dimensionality"))
end
particle_spacing = initial_condition.particle_spacing
if particle_spacing < eps()
throw(ArgumentError("the initial condition in the first argument must store a particle spacing"))
end
too_close = find_too_close_particles(initial_condition.coordinates, ic.coordinates,
0.75particle_spacing)
valid_particles = setdiff(eachparticle(initial_condition), too_close)
coordinates = initial_condition.coordinates[:, valid_particles]
velocity = initial_condition.velocity[:, valid_particles]
mass = initial_condition.mass[valid_particles]
density = initial_condition.density[valid_particles]
pressure = initial_condition.pressure[valid_particles]
result = InitialCondition{ndims(ic)}(coordinates, velocity, mass, density, pressure,
particle_spacing)
return setdiff(result, Base.tail(initial_conditions)...)
end
Base.setdiff(initial_condition::InitialCondition) = initial_condition
function Base.intersect(initial_condition::InitialCondition, initial_conditions...)
ic = first(initial_conditions)
if initial_condition.particle_spacing isa Vector || ic.particle_spacing isa Vector
throw(ArgumentError("`intersect` operation does not support non-uniform particle spacings. " *
"Please ensure all `InitialCondition`s use a scalar particle spacing."))
end
if ndims(ic) != ndims(initial_condition)
throw(ArgumentError("all passed initial conditions must have the same dimensionality"))
end
particle_spacing = initial_condition.particle_spacing
if particle_spacing < eps()
throw(ArgumentError("the initial condition in the first argument must store a particle spacing"))
end
too_close = find_too_close_particles(initial_condition.coordinates, ic.coordinates,
0.75particle_spacing)
coordinates = initial_condition.coordinates[:, too_close]
velocity = initial_condition.velocity[:, too_close]
mass = initial_condition.mass[too_close]
density = initial_condition.density[too_close]
pressure = initial_condition.pressure[too_close]
result = InitialCondition{ndims(ic)}(coordinates, velocity, mass, density, pressure,
particle_spacing)
return intersect(result, Base.tail(initial_conditions)...)
end
Base.intersect(initial_condition::InitialCondition) = initial_condition
function InitialCondition(sol::ODESolution, system, semi; use_final_velocity=false,
min_particle_distance=(system.initial_condition.particle_spacing /
4))
ic = system.initial_condition
v_ode, u_ode = sol.u[end].x
u = wrap_u(u_ode, system, semi)
v = wrap_u(v_ode, system, semi)
# Check if particles come too close especially when the surface exhibits large curvature
too_close = find_too_close_particles(u, min_particle_distance)
velocity_ = use_final_velocity ? view(v, 1:ndims(system), :) : ic.velocity
not_too_close = setdiff(eachparticle(system), too_close)
coordinates = u[:, not_too_close]
velocity = velocity_[:, not_too_close]
mass = ic.mass[not_too_close]
density = ic.density[not_too_close]
pressure = ic.pressure[not_too_close]
if length(too_close) > 0
@info "Removed $(length(too_close)) particles that are too close together"
end
return InitialCondition{ndims(ic)}(coordinates, velocity, mass, density, pressure,
ic.particle_spacing)
end
# Find particles in `coords1` that are closer than `max_distance` to any particle in `coords2`
function find_too_close_particles(coords1, coords2, max_distance)
NDIMS = size(coords1, 1)
result = Int[]
nhs = GridNeighborhoodSearch{NDIMS}(search_radius=max_distance,
n_points=size(coords2, 2))
PointNeighbors.initialize!(nhs, coords1, coords2)
# We are modifying the vector `result`, so this cannot be parallel
foreach_point_neighbor(coords1, coords2, nhs;
parallelization_backend=SerialBackend()) do particle, _, _, _
if !(particle in result)
push!(result, particle)
end
end
return result
end
# Find particles in `coords` that are closer than `min_distance` to any other particle in `coords`
function find_too_close_particles(coords, min_distance)
NDIMS = size(coords, 1)
result = Int[]
nhs = GridNeighborhoodSearch{NDIMS}(search_radius=min_distance,
n_points=size(coords, 2))
TrixiParticles.initialize!(nhs, coords)
# We are modifying the vector `result`, so this cannot be parallel
foreach_point_neighbor(coords, coords, nhs;
parallelization_backend=SerialBackend()) do particle, neighbor,
_, _
# Only consider particles with neighbors that are not to be removed
if particle != neighbor && !(particle in result) && !(neighbor in result)
push!(result, particle)
end
end
return result
end
function move_particles_to_end!(ic::InitialCondition, particle_ids_to_move::Vector)
invalid_id = findfirst(i -> i <= 0 || i > nparticles(ic), particle_ids_to_move)
isnothing(invalid_id) || throw(BoundsError(ic, invalid_id))
sort_key = [i in particle_ids_to_move ? 1 : 0 for i in eachparticle(ic)]
# Determine a permutation that sorts 'sort_key' in ascending order
permutation = sortperm(sort_key)
ic.coordinates .= ic.coordinates[:, permutation]
ic.velocity .= ic.velocity[:, permutation]
ic.mass .= ic.mass[permutation]
ic.density .= ic.density[permutation]
ic.pressure .= ic.pressure[permutation]
return ic
end
function move_particles_to_end!(a::AbstractVector, particle_ids_to_move::Vector)
invalid_id = findfirst(i -> i <= 0 || i > length(a), particle_ids_to_move)
isnothing(invalid_id) || throw(BoundsError(a, invalid_id))
sort_key = [i in particle_ids_to_move ? 1 : 0 for i in eachindex(a)]
# determine a permutation that sorts 'sort_key' in ascending order
permutation = sortperm(sort_key)
a .= a[permutation]
return a
end
move_particles_to_end!(a::Real, particle_ids_to_move::Vector) = a