From 732acefacfd3623c7d88d009824d71fc14a92b4e Mon Sep 17 00:00:00 2001 From: Sven Berger Date: Fri, 13 Jun 2025 15:52:21 +0200 Subject: [PATCH 01/15] Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> --- examples/dem/collapsing_sand_pile_3d.jl | 3 +- examples/fsi/dam_break_gate_2d.jl | 8 +-- examples/fsi/dam_break_plate_2d.jl | 8 +-- examples/preprocessing/packing_2d.jl | 9 +-- examples/preprocessing/packing_3d.jl | 2 +- examples/solid/oscillating_beam_2d.jl | 6 +- src/general/interpolation.jl | 5 +- .../particle_packing/signed_distance.jl | 2 +- src/preprocessing/particle_packing/system.jl | 35 ++++++----- src/setups/complex_shape.jl | 10 ++-- src/setups/extrude_geometry.jl | 60 +++++++++++-------- src/setups/rectangular_shape.jl | 19 +++--- src/setups/sphere_shape.jl | 45 +++++++------- test/setups/extrude_geometry.jl | 7 ++- test/setups/rectangular_shape.jl | 5 +- test/setups/sphere_shape.jl | 8 +-- test/systems/packing_system.jl | 6 +- 17 files changed, 128 insertions(+), 110 deletions(-) diff --git a/examples/dem/collapsing_sand_pile_3d.jl b/examples/dem/collapsing_sand_pile_3d.jl index aa9ba7a56b..f463ac2417 100644 --- a/examples/dem/collapsing_sand_pile_3d.jl +++ b/examples/dem/collapsing_sand_pile_3d.jl @@ -55,7 +55,8 @@ min_coords_floor = (min_boundary[1] - boundary_thickness, floor_particles = RectangularShape(particle_spacing, (n_particles_floor_x, n_particles_floor_y, n_particles_floor_z), - min_coords_floor; density=boundary_density, tlsph=true) + min_coords_floor; density=boundary_density, + place_on_shell=true) boundary_particles = floor_particles # ========================================================================================== diff --git a/examples/fsi/dam_break_gate_2d.jl b/examples/fsi/dam_break_gate_2d.jl index 529fd56522..44adf7bc59 100644 --- a/examples/fsi/dam_break_gate_2d.jl +++ b/examples/fsi/dam_break_gate_2d.jl @@ -83,18 +83,18 @@ solid_particle_spacing = thickness / (n_particles_x - 1) n_particles_y = round(Int, length_beam / solid_particle_spacing) + 1 # The bottom layer is sampled separately below. Note that the `RectangularShape` puts the -# first particle half a particle spacing away from the boundary, which is correct for fluids, -# but not for solids. We therefore need to pass `tlsph=true`. +# first particle half a particle spacing away from the shell of the shape, which is +# correct for fluids, but not for solids. We therefore need to pass `place_on_shell=true`. # # The right end of the plate is 0.2 from the right end of the tank. plate_position = 0.6 - n_particles_x * solid_particle_spacing plate = RectangularShape(solid_particle_spacing, (n_particles_x, n_particles_y - 1), (plate_position, solid_particle_spacing), - density=solid_density, tlsph=true) + density=solid_density, place_on_shell=true) fixed_particles = RectangularShape(solid_particle_spacing, (n_particles_x, 1), (plate_position, 0.0), - density=solid_density, tlsph=true) + density=solid_density, place_on_shell=true) solid = union(plate, fixed_particles) diff --git a/examples/fsi/dam_break_plate_2d.jl b/examples/fsi/dam_break_plate_2d.jl index f5b42ecaa0..d4f8b206fb 100644 --- a/examples/fsi/dam_break_plate_2d.jl +++ b/examples/fsi/dam_break_plate_2d.jl @@ -57,15 +57,15 @@ solid_particle_spacing = thickness / (n_particles_x - 1) n_particles_y = round(Int, length_beam / solid_particle_spacing) + 1 # The bottom layer is sampled separately below. Note that the `RectangularShape` puts the -# first particle half a particle spacing away from the boundary, which is correct for fluids, -# but not for solids. We therefore need to pass `tlsph=true`. +# first particle half a particle spacing away from the shell of the shape, which is +# correct for fluids, but not for solids. We therefore need to pass `place_on_shell=true`. plate = RectangularShape(solid_particle_spacing, (n_particles_x, n_particles_y - 1), (2initial_fluid_size[1], solid_particle_spacing), - density=solid_density, tlsph=true) + density=solid_density, place_on_shell=true) fixed_particles = RectangularShape(solid_particle_spacing, (n_particles_x, 1), (2initial_fluid_size[1], 0.0), - density=solid_density, tlsph=true) + density=solid_density, place_on_shell=true) solid = union(plate, fixed_particles) diff --git a/examples/preprocessing/packing_2d.jl b/examples/preprocessing/packing_2d.jl index fc8b330269..0975b6e2ad 100644 --- a/examples/preprocessing/packing_2d.jl +++ b/examples/preprocessing/packing_2d.jl @@ -20,7 +20,7 @@ file = pkgdir(TrixiParticles, "examples", "preprocessing", "data", filename * ". # ========================================================================================== # ==== Packing parameters -tlsph = false +place_on_shell = false # ========================================================================================== # ==== Resolution @@ -50,7 +50,7 @@ shape_sampled = ComplexShape(geometry; particle_spacing, density, # Returns `InitialCondition` boundary_sampled = sample_boundary(signed_distance_field; boundary_density=density, - boundary_thickness, tlsph=tlsph) + boundary_thickness, place_on_shell=place_on_shell) trixi2vtk(shape_sampled) trixi2vtk(boundary_sampled, filename="boundary") @@ -66,12 +66,13 @@ background_pressure = 1.0 smoothing_length = 0.8 * particle_spacing packing_system = ParticlePackingSystem(shape_sampled; smoothing_length=smoothing_length, - signed_distance_field, tlsph=tlsph, + signed_distance_field, place_on_shell=place_on_shell, background_pressure) boundary_system = ParticlePackingSystem(boundary_sampled; smoothing_length=smoothing_length, is_boundary=true, signed_distance_field, - tlsph=tlsph, boundary_compress_factor=0.8, + place_on_shell=place_on_shell, + boundary_compress_factor=0.8, background_pressure) # ========================================================================================== diff --git a/examples/preprocessing/packing_3d.jl b/examples/preprocessing/packing_3d.jl index cb15a255b2..ede3433b38 100644 --- a/examples/preprocessing/packing_3d.jl +++ b/examples/preprocessing/packing_3d.jl @@ -24,5 +24,5 @@ boundary_thickness = 8 * particle_spacing trixi_include(joinpath(examples_dir(), "preprocessing", "packing_2d.jl"), density=1000.0, particle_spacing=particle_spacing, file=file, - boundary_thickness=boundary_thickness, tlsph=true, + boundary_thickness=boundary_thickness, place_on_shell=true, save_intervals=false) diff --git a/examples/solid/oscillating_beam_2d.jl b/examples/solid/oscillating_beam_2d.jl index e2bb13a300..8deadc23db 100644 --- a/examples/solid/oscillating_beam_2d.jl +++ b/examples/solid/oscillating_beam_2d.jl @@ -38,7 +38,7 @@ fixed_particles = SphereShape(particle_spacing, clamp_radius + particle_spacing (0.0, elastic_beam.thickness / 2), material.density, cutout_min=(0.0, 0.0), cutout_max=(clamp_radius, elastic_beam.thickness), - tlsph=true) + place_on_shell=true) n_particles_clamp_x = round(Int, clamp_radius / particle_spacing) @@ -48,9 +48,9 @@ n_particles_per_dimension = (round(Int, elastic_beam.length / particle_spacing) # Note that the `RectangularShape` puts the first particle half a particle spacing away # from the boundary, which is correct for fluids, but not for solids. -# We therefore need to pass `tlsph=true`. +# We therefore need to pass `place_on_shell=true`. beam = RectangularShape(particle_spacing, n_particles_per_dimension, - (0.0, 0.0), density=material.density, tlsph=true) + (0.0, 0.0), density=material.density, place_on_shell=true) solid = union(beam, fixed_particles) diff --git a/src/general/interpolation.jl b/src/general/interpolation.jl index afee4de5b1..cc246111c9 100644 --- a/src/general/interpolation.jl +++ b/src/general/interpolation.jl @@ -191,9 +191,10 @@ function interpolate_plane_2d(min_corner, max_corner, resolution, semi, ref_syst x_range = range(min_corner[1], max_corner[1], length=n_points_per_dimension[1]) y_range = range(min_corner[2], max_corner[2], length=n_points_per_dimension[2]) - # Generate points within the plane. Use `tlsph=true` to generate points on the boundary + # Generate points within the plane. Use `place_on_shell=true` to generate points + # on the shell of the geometry. point_coords = rectangular_shape_coords(resolution, n_points_per_dimension, min_corner, - tlsph=true) + place_on_shell=true) results = interpolate_points(point_coords, semi, ref_system, v_ode, u_ode, smoothing_length=smoothing_length, diff --git a/src/preprocessing/particle_packing/signed_distance.jl b/src/preprocessing/particle_packing/signed_distance.jl index 7e957e124d..082deced5a 100644 --- a/src/preprocessing/particle_packing/signed_distance.jl +++ b/src/preprocessing/particle_packing/signed_distance.jl @@ -59,7 +59,7 @@ function SignedDistanceField(geometry, particle_spacing; particle_spacing)) grid = rectangular_shape_coords(particle_spacing, n_particles_per_dimension, - min_corner; tlsph=true) + min_corner; place_on_shell=true) points = reinterpret(reshape, SVector{NDIMS, ELTYPE}, grid) end diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl index f0772589ff..6f73fb2c68 100644 --- a/src/preprocessing/particle_packing/system.jl +++ b/src/preprocessing/particle_packing/system.jl @@ -6,7 +6,7 @@ smoothing_length_interpolation=smoothing_length, is_boundary=false, boundary_compress_factor=1, neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(), - background_pressure, tlsph=false, fixed_system=false) + background_pressure, place_on_shell=false, fixed_system=false) System to generate body-fitted particles for complex shapes. For more information on the methods, see [particle packing](@ref particle_packing). @@ -18,10 +18,11 @@ For more information on the methods, see [particle packing](@ref particle_packin - `background_pressure`: Constant background pressure to physically pack the particles. A large `background_pressure` can cause high accelerations which requires a properly adjusted time step. -- `tlsph`: With the [`TotalLagrangianSPHSystem`](@ref), particles need to be placed - on the boundary of the shape and not half a particle spacing away, - as for fluids. When `tlsph=true`, particles will be placed - on the boundary of the shape. +- `place_on_shell`: If `place_on_shell=true`, particles will be placed + on the shell of the geometry. For example, + the [`TotalLagrangianSPHSystem`](@ref) requires particles to be placed + on the shell of the geometry and not half a particle spacing away, + as for fluids. - `is_boundary`: When `shape` is inside the geometry that was used to create `signed_distance_field`, set `is_boundary=false`. Otherwise (`shape` is the sampled boundary), set `is_boundary=true`. @@ -64,7 +65,7 @@ struct ParticlePackingSystem{S, F, NDIMS, ELTYPE <: Real, PR, C, AV, smoothing_kernel :: K smoothing_length_interpolation :: ELTYPE background_pressure :: ELTYPE - tlsph :: Bool + place_on_shell :: Bool signed_distance_field :: S is_boundary :: Bool shift_length :: ELTYPE @@ -78,7 +79,8 @@ struct ParticlePackingSystem{S, F, NDIMS, ELTYPE <: Real, PR, C, AV, # See the comments in general/gpu.jl for more details. function ParticlePackingSystem(initial_condition, mass, density, particle_spacing, smoothing_kernel, smoothing_length_interpolation, - background_pressure, tlsph, signed_distance_field, + background_pressure, place_on_shell, + signed_distance_field, is_boundary, shift_length, neighborhood_search, signed_distances, particle_refinement, buffer, fixed_system, cache, advection_velocity) @@ -90,7 +92,7 @@ struct ParticlePackingSystem{S, F, NDIMS, ELTYPE <: Real, PR, C, AV, mass, density, particle_spacing, smoothing_kernel, smoothing_length_interpolation, - background_pressure, tlsph, + background_pressure, place_on_shell, signed_distance_field, is_boundary, shift_length, neighborhood_search, signed_distances, particle_refinement, @@ -105,7 +107,8 @@ function ParticlePackingSystem(shape::InitialCondition; smoothing_length_interpolation=smoothing_length, is_boundary=false, boundary_compress_factor=1, neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(), - background_pressure, tlsph=false, fixed_system=false) + background_pressure, place_on_shell=false, + fixed_system=false) NDIMS = ndims(shape) ELTYPE = eltype(shape) mass = copy(shape.mass) @@ -144,12 +147,12 @@ function ParticlePackingSystem(shape::InitialCondition; # Its value is negative if the particle is inside the geometry. # Otherwise (if outside), the value is positive. if is_boundary - offset = tlsph ? shape.particle_spacing : shape.particle_spacing / 2 + offset = place_on_shell ? shape.particle_spacing : shape.particle_spacing / 2 shift_length = -boundary_compress_factor * signed_distance_field.max_signed_distance - offset else - shift_length = tlsph ? zero(ELTYPE) : shape.particle_spacing / 2 + shift_length = place_on_shell ? zero(ELTYPE) : shape.particle_spacing / 2 end cache = (; create_cache_refinement(shape, particle_refinement, smoothing_length)...) @@ -158,7 +161,7 @@ function ParticlePackingSystem(shape::InitialCondition; return ParticlePackingSystem(shape, mass, density, shape.particle_spacing, smoothing_kernel, smoothing_length_interpolation, - background_pressure, tlsph, signed_distance_field, + background_pressure, place_on_shell, signed_distance_field, is_boundary, shift_length, nhs, fill(zero(ELTYPE), nparticles(shape)), particle_refinement, nothing, fixed_system, cache, advection_velocity) @@ -183,7 +186,7 @@ function Base.show(io::IO, ::MIME"text/plain", system::ParticlePackingSystem) system.neighborhood_search |> typeof |> nameof) summary_line(io, "#particles", nparticles(system)) summary_line(io, "smoothing kernel", system.smoothing_kernel |> typeof |> nameof) - summary_line(io, "tlsph", system.tlsph ? "yes" : "no") + summary_line(io, "place_on_shell", system.place_on_shell ? "yes" : "no") summary_line(io, "boundary", system.is_boundary ? "yes" : "no") summary_footer(io) end @@ -330,8 +333,8 @@ function constrain_particle!(u, system, particle, distance_signed, normal_vector (; shift_length) = system # For fluid particles: - # - `tlsph = true`: `shift_length = 0` - # - `tlsph = false`: `shift_length = particle_spacing / 2` + # - `place_on_shell = true`: `shift_length = 0` + # - `place_on_shell = false`: `shift_length = particle_spacing / 2` # For boundary particles: # `shift_length` is the thickness of the boundary. if distance_signed >= -shift_length @@ -346,7 +349,7 @@ function constrain_particle!(u, system, particle, distance_signed, normal_vector system.is_boundary || return u particle_spacing = system.initial_condition.particle_spacing - shift_length_inner = system.tlsph ? particle_spacing : particle_spacing / 2 + shift_length_inner = system.place_on_shell ? particle_spacing : particle_spacing / 2 if distance_signed < shift_length_inner shift = (distance_signed - shift_length_inner) * normal_vector diff --git a/src/setups/complex_shape.jl b/src/setups/complex_shape.jl index cdb8e146b6..032250ce56 100644 --- a/src/setups/complex_shape.jl +++ b/src/setups/complex_shape.jl @@ -79,7 +79,7 @@ end """ sample_boundary(signed_distance_field; - boundary_density, boundary_thickness, tlsph=true) + boundary_density, boundary_thickness, place_on_shell=true) Sample boundary particles of a complex geometry by using the [`SignedDistanceField`](@ref) of the geometry. @@ -90,9 +90,9 @@ of the geometry. # Keywords - `boundary_thickness`: Thickness of the boundary - `boundary_density`: Density of each boundary particle. -- `tlsph` : When `tlsph=true`, boundary particles will be placed +- `place_on_shell`: When `place_on_shell=true`, boundary particles will be placed one particle spacing from the surface of the geometry. - Otherwise when `tlsph=true` (simulating fluid particles), + Otherwise when `place_on_shell=true` (simulating fluid particles), boundary particles will be placed half particle spacing away from the surface. @@ -118,7 +118,7 @@ boundary_sampled = sample_boundary(signed_distance_field; boundary_density=1.0, ``` """ function sample_boundary(signed_distance_field; - boundary_density, boundary_thickness, tlsph=true) + boundary_density, boundary_thickness, place_on_shell=true) (; max_signed_distance, boundary_packing, positions, distances, particle_spacing) = signed_distance_field @@ -158,6 +158,6 @@ function particle_grid(geometry, particle_spacing; end grid = rectangular_shape_coords(particle_spacing, n_particles_per_dimension, - min_corner; tlsph=true) + min_corner; place_on_shell=true) return reinterpret(reshape, SVector{ndims(geometry), eltype(geometry)}, grid) end diff --git a/src/setups/extrude_geometry.jl b/src/setups/extrude_geometry.jl index 498266ecd0..6d169762c7 100644 --- a/src/setups/extrude_geometry.jl +++ b/src/setups/extrude_geometry.jl @@ -30,9 +30,11 @@ Returns an [`InitialCondition`](@ref). - `pressure`: Scalar to set the pressure of all particles to this value. This is only used by the [`EntropicallyDampedSPHSystem`](@ref) and will be overwritten when using an initial pressure function in the system. -- `tlsph`: With the [`TotalLagrangianSPHSystem`](@ref), particles need to be placed - on the boundary of the shape and not one particle radius away, as for fluids. - When `tlsph=true`, particles will be placed on the boundary of the shape. +- `place_on_shell`: If `place_on_shell=true`, particles will be placed + on the shell of the geometry. For example, + the [`TotalLagrangianSPHSystem`](@ref) requires particles to be placed + on the shell of the geometry and not half a particle spacing away, + as for fluids. # Examples ```jldoctest; output = false @@ -79,7 +81,7 @@ shape = extrude_geometry(shape; direction, particle_spacing=0.1, n_extrude=4, de This is an experimental feature and may change in any future releases. """ function extrude_geometry(geometry; particle_spacing=-1, direction, n_extrude::Integer, - velocity=zeros(length(direction)), tlsph=false, + velocity=zeros(length(direction)), place_on_shell=false, mass=nothing, density=nothing, pressure=0.0) direction_ = normalize(direction) NDIMS = length(direction_) @@ -95,9 +97,11 @@ function extrude_geometry(geometry; particle_spacing=-1, direction, n_extrude::I throw(ArgumentError("`particle_spacing` must be specified when not extruding an `InitialCondition`")) end - geometry = shift_plane_corners(geometry, direction_, particle_spacing, tlsph) + geometry = shift_plane_corners(geometry, direction_, particle_spacing, place_on_shell) - face_coords, particle_spacing_ = sample_plane(geometry, particle_spacing; tlsph=tlsph) + face_coords, + particle_spacing_ = sample_plane(geometry, particle_spacing; + place_on_shell=place_on_shell) if !isapprox(particle_spacing, particle_spacing_, rtol=5e-2) @info "The desired size is not a multiple of the particle spacing $particle_spacing." * @@ -119,12 +123,13 @@ end # For corners/endpoints of a plane/line, sample the plane/line with particles. # For 2D coordinates or an `InitialCondition`, add a third dimension. -function sample_plane(geometry::AbstractMatrix, particle_spacing; tlsph) +function sample_plane(geometry::AbstractMatrix, particle_spacing; place_on_shell) if size(geometry, 1) == 2 # Extruding a 2D shape results in a 3D shape - # When `tlsph=true`, particles will be placed on the x-y plane - coords = vcat(geometry, fill(tlsph ? 0 : particle_spacing / 2, size(geometry, 2))') + # When `place_on_shell=true`, particles will be placed on the x-y plane + coords = vcat(geometry, + fill(place_on_shell ? 0 : particle_spacing / 2, size(geometry, 2))') # TODO: 2D shapes not only in x-y plane but in any user-defined plane return coords, particle_spacing @@ -133,13 +138,14 @@ function sample_plane(geometry::AbstractMatrix, particle_spacing; tlsph) return geometry, particle_spacing end -function sample_plane(shape::InitialCondition, particle_spacing; tlsph) +function sample_plane(shape::InitialCondition, particle_spacing; place_on_shell) if ndims(shape) == 2 # Extruding a 2D shape results in a 3D shape - # When `tlsph=true`, particles will be placed on the x-y plane + # When `place_on_shell=true`, particles will be placed on the x-y plane coords = vcat(shape.coordinates, - fill(tlsph ? 0 : particle_spacing / 2, size(shape.coordinates, 2))') + fill(place_on_shell ? 0 : particle_spacing / 2, + size(shape.coordinates, 2))') # TODO: 2D shapes not only in x-y plane but in any user-defined plane return coords, particle_spacing @@ -148,13 +154,13 @@ function sample_plane(shape::InitialCondition, particle_spacing; tlsph) return shape.coordinates, particle_spacing end -function sample_plane(plane_points, particle_spacing; tlsph=nothing) +function sample_plane(plane_points, particle_spacing; place_on_shell=nothing) # Convert to tuple - return sample_plane(tuple(plane_points...), particle_spacing; tlsph=nothing) + return sample_plane(tuple(plane_points...), particle_spacing; place_on_shell=nothing) end -function sample_plane(plane_points::NTuple{2}, particle_spacing; tlsph=nothing) +function sample_plane(plane_points::NTuple{2}, particle_spacing; place_on_shell=nothing) # Verify that points are in 2D space if any(length.(plane_points) .!= 2) throw(ArgumentError("all points must be 2D coordinates")) @@ -168,7 +174,7 @@ function sample_plane(plane_points::NTuple{2}, particle_spacing; tlsph=nothing) return coords, particle_spacing_new end -function sample_plane(plane_points::NTuple{3}, particle_spacing; tlsph=nothing) +function sample_plane(plane_points::NTuple{3}, particle_spacing; place_on_shell=nothing) # Verify that points are in 3D space if any(length.(plane_points) .!= 3) throw(ArgumentError("all points must be 3D coordinates")) @@ -209,21 +215,22 @@ function sample_plane(plane_points::NTuple{3}, particle_spacing; tlsph=nothing) return coords, particle_spacing_new end -# Shift corners of the plane/line inwards by half a particle spacing with `tlsph=false` +# Shift corners of the plane/line inwards by half a particle spacing with `place_on_shell=false` # because fluid particles need to be half a particle spacing away from the boundary of the shape. function shift_plane_corners(geometry::Union{AbstractMatrix, InitialCondition}, - direction, particle_spacing, tlsph) + direction, particle_spacing, place_on_shell) return geometry end -function shift_plane_corners(plane_points, direction, particle_spacing, tlsph) - shift_plane_corners(tuple(plane_points...), direction, particle_spacing, tlsph) +function shift_plane_corners(plane_points, direction, particle_spacing, place_on_shell) + shift_plane_corners(tuple(plane_points...), direction, particle_spacing, place_on_shell) end -function shift_plane_corners(plane_points::NTuple{2}, direction, particle_spacing, tlsph) - # With TLSPH, particles need to be AT the min coordinates and not half a particle +function shift_plane_corners(plane_points::NTuple{2}, direction, particle_spacing, + place_on_shell) + # With `place_on_shell`, particles need to be AT the min coordinates and not half a particle # spacing away from it. - (tlsph) && (return plane_points) + (place_on_shell) && (return plane_points) plane_point1 = copy(plane_points[1]) plane_point2 = copy(plane_points[2]) @@ -238,10 +245,11 @@ function shift_plane_corners(plane_points::NTuple{2}, direction, particle_spacin return (plane_point1, plane_point2) end -function shift_plane_corners(plane_points::NTuple{3}, direction, particle_spacing, tlsph) - # With TLSPH, particles need to be AT the min coordinates and not half a particle +function shift_plane_corners(plane_points::NTuple{3}, direction, particle_spacing, + place_on_shell) + # With `place_on_shell`, particles need to be AT the min coordinates and not half a particle # spacing away from it. - (tlsph) && (return plane_points) + (place_on_shell) && (return plane_points) plane_point1 = copy(plane_points[1]) plane_point2 = copy(plane_points[2]) diff --git a/src/setups/rectangular_shape.jl b/src/setups/rectangular_shape.jl index 98160bcac9..1c30ef5c1b 100644 --- a/src/setups/rectangular_shape.jl +++ b/src/setups/rectangular_shape.jl @@ -3,7 +3,7 @@ velocity=zeros(length(n_particles_per_dimension)), mass=nothing, density=nothing, pressure=0.0, acceleration=nothing, state_equation=nothing, - tlsph=false, loop_order=nothing) + place_on_shell=false, loop_order=nothing) Rectangular shape filled with particles. Returns an [`InitialCondition`](@ref). @@ -40,9 +40,10 @@ Rectangular shape filled with particles. Returns an [`InitialCondition`](@ref). - `state_equation`: When calculating a hydrostatic pressure gradient by setting `acceleration`, the `state_equation` will be used to set the corresponding density. Cannot be used together with `density`. -- `tlsph`: With the [`TotalLagrangianSPHSystem`](@ref), particles need to be placed - on the boundary of the shape and not one particle radius away, as for fluids. - When `tlsph=true`, particles will be placed on the boundary of the shape. +- `place_on_shell`: If `place_on_shell=true`, particles will be placed on the shell of the shape. + For example, the [`TotalLagrangianSPHSystem`](@ref) requires particles + to be placed on the shell of the shape and not half a particle spacing away, + as for fluids. - `coordinates_perturbation`: Add a small random displacement to the particle positions, where the amplitude is `coordinates_perturbation * particle_spacing`. @@ -75,7 +76,7 @@ function RectangularShape(particle_spacing, n_particles_per_dimension, min_coord coordinates_perturbation=nothing, mass=nothing, density=nothing, pressure=0.0, acceleration=nothing, state_equation=nothing, - tlsph=false, loop_order=nothing) + place_on_shell=false, loop_order=nothing) if particle_spacing < eps() throw(ArgumentError("`particle_spacing` needs to be positive and larger than $(eps())")) end @@ -95,7 +96,7 @@ function RectangularShape(particle_spacing, n_particles_per_dimension, min_coord n_particles = prod(n_particles_per_dimension) coordinates = rectangular_shape_coords(particle_spacing, n_particles_per_dimension, - min_coordinates, tlsph=tlsph, + min_coordinates, place_on_shell=place_on_shell, loop_order=loop_order) if !isnothing(coordinates_perturbation) @@ -190,15 +191,15 @@ function loop_permutation(loop_order, NDIMS::Val{3}) end function rectangular_shape_coords(particle_spacing, n_particles_per_dimension, - min_coordinates; tlsph=false, loop_order=nothing) + min_coordinates; place_on_shell=false, loop_order=nothing) ELTYPE = eltype(particle_spacing) NDIMS = length(n_particles_per_dimension) coordinates = Array{ELTYPE, 2}(undef, NDIMS, prod(n_particles_per_dimension)) - # With TLSPH, particles need to be AT the min coordinates and not half a particle + # With place_on_shell, particles need to be AT the min coordinates and not half a particle # spacing away from it. - if tlsph + if place_on_shell min_coordinates = min_coordinates .- 0.5particle_spacing end diff --git a/src/setups/sphere_shape.jl b/src/setups/sphere_shape.jl index 0233fc8995..3c6a7466d5 100644 --- a/src/setups/sphere_shape.jl +++ b/src/setups/sphere_shape.jl @@ -1,7 +1,7 @@ """ SphereShape(particle_spacing, radius, center_position, density; sphere_type=VoxelSphere(), n_layers=-1, layer_outwards=false, - cutout_min=(0.0, 0.0), cutout_max=(0.0, 0.0), tlsph=false, + cutout_min=(0.0, 0.0), cutout_max=(0.0, 0.0), place_on_shell=false, velocity=zeros(length(center_position)), mass=nothing, pressure=0.0) Generate a sphere that is either completely filled (by default) @@ -35,18 +35,19 @@ coordinate directions as `cutout_min` and `cutout_max`. cut out of the sphere. - `cutout_max`: Corner in positive coordinate directions of a cuboid that is to be cut out of the sphere. -- `tlsph`: With the [`TotalLagrangianSPHSystem`](@ref), particles need to be placed - on the boundary of the shape and not one particle radius away, as for fluids. - When `tlsph=true`, particles will be placed on the boundary of the shape. -- `velocity`: Either 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 function mapping each particle's coordinates to its mass, - or a scalar for a constant mass over all particles. -- `pressure`: Either 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). +- `place_on_shell`: If `place_on_shell=true`, particles will be placed on the shell of the shape. + For example, the [`TotalLagrangianSPHSystem`](@ref) requires particles + to be placed on the shell of the shape and not half a particle spacing away, + as for fluids. +- `velocity`: Either 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 function mapping each particle's coordinates to its mass, + or a scalar for a constant mass over all particles. +- `pressure`: Either 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). # Examples ```jldoctest; output = false @@ -89,7 +90,7 @@ SphereShape(0.1, 0.5, (0.2, 0.4, 0.3), 1000.0, sphere_type=RoundSphere()) """ function SphereShape(particle_spacing, radius, center_position, density; sphere_type=VoxelSphere(), n_layers=-1, layer_outwards=false, - cutout_min=(0.0, 0.0), cutout_max=(0.0, 0.0), tlsph=false, + cutout_min=(0.0, 0.0), cutout_max=(0.0, 0.0), place_on_shell=false, velocity=zeros(length(center_position)), mass=nothing, pressure=0) if particle_spacing < eps() throw(ArgumentError("`particle_spacing` needs to be positive and larger than $(eps())")) @@ -99,7 +100,7 @@ function SphereShape(particle_spacing, radius, center_position, density; coordinates = sphere_shape_coords(sphere_type, particle_spacing, radius, SVector{NDIMS}(center_position), - n_layers, layer_outwards, tlsph) + n_layers, layer_outwards, place_on_shell) # Convert tuples to vectors cutout_min_ = collect(cutout_min) @@ -169,13 +170,13 @@ struct RoundSphere{AR} end function sphere_shape_coords(::VoxelSphere, particle_spacing, radius, center_position, - n_layers, layer_outwards, tlsph) + n_layers, layer_outwards, place_on_shell) if n_layers > 0 if layer_outwards inner_radius = radius outer_radius = radius + n_layers * particle_spacing - if !tlsph + if !place_on_shell # Put first layer of particles half a particle spacing outside of `radius` inner_radius += particle_spacing / 2 outer_radius += particle_spacing / 2 @@ -184,7 +185,7 @@ function sphere_shape_coords(::VoxelSphere, particle_spacing, radius, center_pos inner_radius = radius - n_layers * particle_spacing outer_radius = radius - if !tlsph + if !place_on_shell # Put first layer of particles half a particle spacing inside of `radius` inner_radius -= particle_spacing / 2 outer_radius -= particle_spacing / 2 @@ -194,7 +195,7 @@ function sphere_shape_coords(::VoxelSphere, particle_spacing, radius, center_pos outer_radius = radius inner_radius = -1 - if !tlsph + if !place_on_shell # Put first layer of particles half a particle spacing inside of `radius` outer_radius -= particle_spacing / 2 end @@ -225,7 +226,7 @@ function sphere_shape_coords(::VoxelSphere, particle_spacing, radius, center_pos end function sphere_shape_coords(sphere::RoundSphere, particle_spacing, radius, center, - n_layers, layer_outwards, tlsph) + n_layers, layer_outwards, place_on_shell) if n_layers > 0 if layer_outwards inner_radius = radius @@ -233,12 +234,12 @@ function sphere_shape_coords(sphere::RoundSphere, particle_spacing, radius, cent inner_radius = radius - n_layers * particle_spacing end - if !tlsph + if !place_on_shell # Put first layer of particles half a particle spacing outside of inner radius inner_radius += particle_spacing / 2 end else - if tlsph + if place_on_shell # Just create a sphere that is 0.5 particle spacing larger radius += particle_spacing / 2 end diff --git a/test/setups/extrude_geometry.jl b/test/setups/extrude_geometry.jl index e695f2e170..5a927dbee4 100644 --- a/test/setups/extrude_geometry.jl +++ b/test/setups/extrude_geometry.jl @@ -28,7 +28,8 @@ ] @testset "Direction $i" for i in eachindex(directions) - shape = extrude_geometry((point1, point2); direction=directions[i], tlsph=true, + shape = extrude_geometry((point1, point2); direction=directions[i], + place_on_shell=true, particle_spacing=0.15, n_extrude=5, density=1.0) @test shape.coordinates ≈ expected_coords[i] @@ -68,7 +69,7 @@ end @testset "Direction $i" for i in eachindex(directions) shape = extrude_geometry(geometry; direction=directions[i], particle_spacing, - n_extrude=5, tlsph=true, density=1.0) + n_extrude=5, place_on_shell=true, density=1.0) @test shape.coordinates ≈ expected_coords[i] end @@ -86,7 +87,7 @@ end 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.06666666666666667 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.13333333333333333 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.26666666666666666 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.3333333333333333 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.4666666666666667 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.5333333333333333 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.6666666666666666 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.7333333333333333 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.8666666666666667 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 0.9333333333333333 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.092709445701687 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.15937611236835367 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.22604277903502035 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.292709445701687 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.35937611236835365 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.4260427790350203 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.492709445701687 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.5593761123683537 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.6260427790350204 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.692709445701687 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.7593761123683537 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8260427790350203 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.8927094457016871 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 0.9593761123683537 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.0260427790350204 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 1.092709445701687 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.185418891403374 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.2520855580700407 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.31875222473670733 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.38541889140337404 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.4520855580700407 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.5187522247367073 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.585418891403374 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.6520855580700406 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.7187522247367073 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.785418891403374 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.8520855580700406 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.9187522247367073 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 0.985418891403374 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.0520855580700408 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.1187522247367074 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 1.185418891403374 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.278128337105061 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.34479500377172767 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.4114616704383943 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.47812833710506103 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.5447950037717277 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.6114616704383944 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.678128337105061 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.7447950037717277 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.8114616704383943 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.878128337105061 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 0.9447950037717276 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0114616704383943 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.0781283371050612 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.1447950037717276 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.2114616704383945 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061 1.278128337105061] shape = extrude_geometry((p1, p2, p3); direction, particle_spacing=0.1, n_extrude=4, - density=1000.0, tlsph=true) + density=1000.0, place_on_shell=true) @test shape.coordinates ≈ expected_coords end diff --git a/test/setups/rectangular_shape.jl b/test/setups/rectangular_shape.jl index f3e77043b0..7c2fd6875b 100644 --- a/test/setups/rectangular_shape.jl +++ b/test/setups/rectangular_shape.jl @@ -41,7 +41,8 @@ ] @testset "$(loop_orders[i])" for i in eachindex(loop_orders) - shape = RectangularShape(1.0, (2, 2), (0.0, 0.0), density=1.0, tlsph=true, + shape = RectangularShape(1.0, (2, 2), (0.0, 0.0), density=1.0, + place_on_shell=true, loop_order=loop_orders[i]) @test shape.coordinates == expected_coords[i] @@ -242,7 +243,7 @@ end @testset "$(loop_orders[i])" for i in eachindex(loop_orders) shape = RectangularShape(1.0, (2, 2, 2), (0.0, 0.0, 0.0), density=1.0, - tlsph=true, loop_order=loop_orders[i]) + place_on_shell=true, loop_order=loop_orders[i]) @test shape.coordinates == expected_coords[i] end diff --git a/test/setups/sphere_shape.jl b/test/setups/sphere_shape.jl index 57d904a376..c8b87d76db 100644 --- a/test/setups/sphere_shape.jl +++ b/test/setups/sphere_shape.jl @@ -106,14 +106,14 @@ SphereShape(1.0, 1.1, (0.2, -1.0, 0.3), 1000.0, sphere_type=RoundSphere()), SphereShape(1.0, 1.2, (-0.3, 0.1, 0.8), 1000.0, sphere_type=RoundSphere()), SphereShape(0.1, 0.5, (0.3, 0.4, 0.5), 1000.0, cutout_min=(0.18, 0.4, 0.5), - cutout_max=(0.42, 10.0, 1.0), tlsph=true), - SphereShape(0.1, 0.5, (0.3, 0.4, 0.5), 1000.0, n_layers=2, tlsph=true), + cutout_max=(0.42, 10.0, 1.0), place_on_shell=true), + SphereShape(0.1, 0.5, (0.3, 0.4, 0.5), 1000.0, n_layers=2, place_on_shell=true), SphereShape(0.1, 0.5, (0.3, 0.4, 0.5), 1000.0, n_layers=2, - layer_outwards=true, tlsph=true), + layer_outwards=true, place_on_shell=true), SphereShape(0.1, 0.5, (0.3, 0.4, 0.5), 1000.0, n_layers=2, sphere_type=RoundSphere()), SphereShape(0.1, 0.55, (0.3, 0.4, 0.5), 1000.0, n_layers=2, layer_outwards=true, - sphere_type=RoundSphere(), tlsph=true) + sphere_type=RoundSphere(), place_on_shell=true) ] expected_coords = [ diff --git a/test/systems/packing_system.jl b/test/systems/packing_system.jl index d55afd559a..b42e534275 100644 --- a/test/systems/packing_system.jl +++ b/test/systems/packing_system.jl @@ -19,7 +19,7 @@ │ neighborhood search: ………………………… GridNeighborhoodSearch │ │ #particles: ………………………………………………… 307 │ │ smoothing kernel: ………………………………… SchoenbergQuinticSplineKernel │ - │ tlsph: ……………………………………………………………… no │ + │ place_on_shell: ……………………………………… no │ │ boundary: ……………………………………………………… no │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" @test repr("text/plain", system) == show_box @@ -36,7 +36,7 @@ │ neighborhood search: ………………………… GridNeighborhoodSearch │ │ #particles: ………………………………………………… 307 │ │ smoothing kernel: ………………………………… SchoenbergQuinticSplineKernel │ - │ tlsph: ……………………………………………………………… no │ + │ place_on_shell: ……………………………………… no │ │ boundary: ……………………………………………………… yes │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" @test repr("text/plain", system) == show_box @@ -52,7 +52,7 @@ │ neighborhood search: ………………………… Nothing │ │ #particles: ………………………………………………… 307 │ │ smoothing kernel: ………………………………… SchoenbergQuinticSplineKernel │ - │ tlsph: ……………………………………………………………… no │ + │ place_on_shell: ……………………………………… no │ │ boundary: ……………………………………………………… no │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" end From 0597721d47cfb09b9d5293a54db1d42c608b5590 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:27:56 +0200 Subject: [PATCH 02/15] Combine PST and TVF into a unified framework (#884) * Combine PST and TVF into a unified framework * Require update callback for PST * Fix WCSPH * Update PST only in callback * Fix EDAC * Update docs * Fix alle example files * Fix tests * Fix periodic channel * Fix docs * Update news * Fix tests * Fix example file --- NEWS.md | 25 +- docs/src/systems/entropically_damped_sph.md | 61 ---- docs/src/systems/weakly_compressible_sph.md | 70 ++++- examples/fluid/lid_driven_cavity_2d.jl | 4 +- .../fluid/periodic_array_of_cylinders_2d.jl | 2 +- examples/fluid/periodic_channel_2d.jl | 3 +- examples/fluid/pipe_flow_2d.jl | 6 +- examples/fluid/taylor_green_vortex_2d.jl | 4 +- src/TrixiParticles.jl | 5 +- src/callbacks/callbacks.jl | 1 - src/callbacks/particle_shifting.jl | 148 --------- src/callbacks/update.jl | 42 ++- src/general/semidiscretization.jl | 2 +- src/general/system.jl | 3 - src/io/write_vtk.jl | 3 - .../fluid/entropically_damped_sph/rhs.jl | 13 +- .../fluid/entropically_damped_sph/system.jl | 28 +- src/schemes/fluid/fluid.jl | 2 +- src/schemes/fluid/shifting_techniques.jl | 291 ++++++++++++++++++ src/schemes/fluid/transport_velocity.jl | 158 ---------- .../fluid/weakly_compressible_sph/rhs.jl | 13 +- .../fluid/weakly_compressible_sph/system.jl | 25 +- test/examples/examples_fluid.jl | 19 +- test/systems/edac_system.jl | 8 +- test/systems/wcsph_system.jl | 4 +- 25 files changed, 479 insertions(+), 461 deletions(-) delete mode 100644 src/callbacks/particle_shifting.jl create mode 100644 src/schemes/fluid/shifting_techniques.jl delete mode 100644 src/schemes/fluid/transport_velocity.jl diff --git a/NEWS.md b/NEWS.md index f1940cbc46..d4e2ccdd7d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,14 +4,27 @@ TrixiParticles.jl follows the interpretation of [semantic versioning (semver)](https://julialang.github.io/Pkg.jl/dev/compatibility/#Version-specifier-format-1) used in the Julia ecosystem. Notable changes will be documented in this file for human readability. +## Version 0.4 + +### API Changes + +- Combined transport velocity formulation (TVF) and particle shifting technique (PST) into + one unified framework. + The keyword argument `transport_velocity` now changed to `shifting_technique`. + The `ParticleShiftingCallback` has been removed. To use PST, use the `UpdateCallback` + instead, and pass `shifting_technique=ParticleShiftingTechnique()` to the system. + +- Renamed the keyword argument `tlsph` to `place_on_shell` for `ParticlePackingSystem`, + `sample_boundary`, `extrude_geometry`, `RectangularShape`, and `SphereShape`. + ## Version 0.3.1 ### Features -- **Simplified SGS Viscosity Models**: Added ViscosityMorrisSGS and ViscosityAdamiSGS, +- **Simplified SGS Viscosity Models**: Added ViscosityMorrisSGS and ViscosityAdamiSGS, which implement a simplified Smagorinsky-type sub-grid-scale viscosity. (#753) -- **Multithreaded Integration Array**: Introduced a new array type for CPU backends +- **Multithreaded Integration Array**: Introduced a new array type for CPU backends that enables multithreaded broadcasting, delivering speed-ups of up to 5× on systems with many threads when combined with thread pinning. (#722) @@ -21,17 +34,17 @@ used in the Julia ecosystem. Notable changes will be documented in this file for - **DXF file format support**: Import complex geometries using the DXF file format. (#821) - **Improved Plane interpolation**: Massively improved interpolation performance for planes (#763). - + ### GPU - Make PST GPU-compatible (#813). - + - Make open boundaries GPU-compatible (#773). - + - Make interpolation GPU-compatible (#812). ### Important Bugfixes - + - Fix validation setups (#801). - Calculate interpolated density instead of computed density when using interpolation (#808). diff --git a/docs/src/systems/entropically_damped_sph.md b/docs/src/systems/entropically_damped_sph.md index 0db8d275e2..d035fb6fc1 100644 --- a/docs/src/systems/entropically_damped_sph.md +++ b/docs/src/systems/entropically_damped_sph.md @@ -45,64 +45,3 @@ is a good choice for a wide range of Reynolds numbers (0.0125 to 10000). Modules = [TrixiParticles] Pages = [joinpath("schemes", "fluid", "entropically_damped_sph", "system.jl")] ``` - -## [Transport Velocity Formulation (TVF)](@id transport_velocity_formulation) -Standard SPH suffers from problems like tensile instability or the creation of void regions in the flow. -To address these problems, [Adami (2013)](@cite Adami2013) modified the advection velocity and added an extra term to the momentum equation. -The authors introduced the so-called Transport Velocity Formulation (TVF) for WCSPH. [Ramachandran (2019)](@cite Ramachandran2019) applied the TVF -also for the [EDAC](@ref edac) scheme. - -The transport velocity ``\tilde{v}_a`` of particle ``a`` is used to evolve the position of the particle ``r_a`` from one time step to the next by - -```math -\frac{\mathrm{d} r_a}{\mathrm{d}t} = \tilde{v}_a -``` - -and is obtained at every time-step ``\Delta t`` from - -```math -\tilde{v}_a (t + \Delta t) = v_a (t) + \Delta t \left(\frac{\tilde{\mathrm{d}} v_a}{\mathrm{d}t} - \frac{1}{\rho_a} \nabla p_{\text{background}} \right), -``` - -where ``\rho_a`` is the density of particle ``a`` and ``p_{\text{background}}`` is a constant background pressure field. -The tilde in the second term of the right hand side indicates that the material derivative has an advection part. - -The discretized form of the last term is - -```math - -\frac{1}{\rho_a} \nabla p_{\text{background}} \approx -\frac{p_{\text{background}}}{m_a} \sum_b \left(V_a^2 + V_b^2 \right) \nabla_a W_{ab}, -``` - -where ``V_a``, ``V_b`` denote the volume of particles ``a`` and ``b`` respectively. -Note that although in the continuous case ``\nabla p_{\text{background}} = 0``, the discretization is not 0th-order consistent for **non**-uniform particle distribution, -which means that there is a non-vanishing contribution only when particles are disordered. -That also means that ``p_{\text{background}}`` occurs as prefactor to correct the trajectory of a particle resulting in uniform pressure distributions. -Suggested is a background pressure which is in the order of the reference pressure but can be chosen arbitrarily large when the time-step criterion is adjusted. - -The inviscid momentum equation with an additional convection term for a particle moving with ``\tilde{v}`` is - -```math -\frac{\tilde{\mathrm{d}} \left( \rho v \right)}{\mathrm{d}t} = -\nabla p + \nabla \cdot \bm{A}, -``` - - where the tensor ``\bm{A} = \rho v\left(\tilde{v}-v\right)^T`` is a consequence of the modified - advection velocity and can be interpreted as the convection of momentum with the relative velocity ``\tilde{v}-v``. - -The discretized form of the momentum equation for a particle ``a`` reads as - -```math -\frac{\tilde{\mathrm{d}} v_a}{\mathrm{d}t} = \frac{1}{m_a} \sum_b \left(V_a^2 + V_b^2 \right) \left[ -\tilde{p}_{ab} \nabla_a W_{ab} + \frac{1}{2} \left(\bm{A}_a + \bm{A}_b \right) \cdot \nabla_a W_{ab} \right]. -``` - -Here, ``\tilde{p}_{ab}`` is the density-weighted pressure - -```math -\tilde{p}_{ab} = \frac{\rho_b p_a + \rho_a p_b}{\rho_a + \rho_b}, -``` - -with the density ``\rho_a``, ``\rho_b`` and the pressure ``p_a``, ``p_b`` of particles ``a`` and ``b`` respectively. ``\bm{A}_a`` and ``\bm{A}_b`` are the convection tensors for particle ``a`` and ``b`` respectively and are given, e.g. for particle ``a``, as ``\bm{A}_a = \rho v_a\left(\tilde{v}_a-v_a\right)^T``. - -```@autodocs -Modules = [TrixiParticles] -Pages = [joinpath("schemes", "fluid", "transport_velocity.jl")] -``` diff --git a/docs/src/systems/weakly_compressible_sph.md b/docs/src/systems/weakly_compressible_sph.md index 51355961f6..62a1bc8d0c 100644 --- a/docs/src/systems/weakly_compressible_sph.md +++ b/docs/src/systems/weakly_compressible_sph.md @@ -152,8 +152,74 @@ as explained in [Sun2018](@cite Sun2018) on page 29, right above Equation 9. The ``\delta``-SPH method (WCSPH with density diffusion) together with this formulation of PST is commonly referred to as ``\delta^+``-SPH. -The Particle Shifting Technique can be applied in form -of the [`ParticleShiftingCallback`](@ref). +To apply particle shifting, use the keyword argument `shifting_technique` in the constructor +of a system that supports it. + + +## [Transport Velocity Formulation (TVF)](@id transport_velocity_formulation) + +An alternative formulation is the so-called Transport Velocity Formulation (TVF) +by [Adami (2013)](@cite Adami2013). +[Ramachandran (2019)](@cite Ramachandran2019) applied the TVF also for the [EDAC](@ref edac) +scheme. + +The transport velocity ``\tilde{v}_a`` of particle ``a`` is used to evolve the position +of the particle ``r_a`` from one time step to the next by +```math +\frac{\mathrm{d} r_a}{\mathrm{d}t} = \tilde{v}_a +``` +and is obtained at every time step ``\Delta t`` from +```math +\tilde{v}_a (t + \Delta t) = v_a (t) + \Delta t \left(\frac{\tilde{\mathrm{d}} v_a}{\mathrm{d}t} - \frac{1}{\rho_a} \nabla p_{\text{background}} \right), +``` +where ``\rho_a`` is the density of particle ``a`` and ``p_{\text{background}}`` +is a constant background pressure field. +The tilde in the second term of the right-hand side indicates that the material derivative +has an advection part. + +The discretized form of the last term is +```math + -\frac{1}{\rho_a} \nabla p_{\text{background}} \approx -\frac{p_{\text{background}}}{m_a} \sum_b \left(V_a^2 + V_b^2 \right) \nabla_a W_{ab}, +``` +where ``V_a``, ``V_b`` denote the volume of particles ``a`` and ``b`` respectively. +Note that although in the continuous case ``\nabla p_{\text{background}} = 0``, +the discretization is not 0th-order consistent for **non**-uniform particle distribution, +which means that there is a non-vanishing contribution only when particles are disordered. +That also means that ``p_{\text{background}}`` occurs as pre-factor to correct +the trajectory of a particle resulting in uniform pressure distributions. +Suggested is a background pressure which is in the order of the reference pressure, +but it can be chosen arbitrarily large when the time-step criterion is adjusted. + +The inviscid momentum equation with an additional convection term for a particle +moving with ``\tilde{v}`` is +```math +\frac{\tilde{\mathrm{d}} \left( \rho v \right)}{\mathrm{d}t} = -\nabla p + \nabla \cdot \bm{A}, +``` +where the tensor ``\bm{A} = \rho v\left(\tilde{v}-v\right)^T`` is a consequence +of the modified advection velocity and can be interpreted as the convection of momentum +with the relative velocity ``\tilde{v}-v``. + +The discretized form of the momentum equation for a particle ``a`` reads as +```math +\frac{\tilde{\mathrm{d}} v_a}{\mathrm{d}t} = \frac{1}{m_a} \sum_b \left(V_a^2 + V_b^2 \right) \left[ -\tilde{p}_{ab} \nabla_a W_{ab} + \frac{1}{2} \left(\bm{A}_a + \bm{A}_b \right) \cdot \nabla_a W_{ab} \right]. +``` +Here, ``\tilde{p}_{ab}`` is the density-weighted pressure +```math +\tilde{p}_{ab} = \frac{\rho_b p_a + \rho_a p_b}{\rho_a + \rho_b}, +``` +with the density ``\rho_a``, ``\rho_b`` and the pressure ``p_a``, ``p_b`` of particles ``a`` +and ``b``, respectively. ``\bm{A}_a`` and ``\bm{A}_b`` are the convection tensors +for particle ``a`` and ``b``, respectively, and are given, e.g., for particle ``a``, +as ``\bm{A}_a = \rho v_a\left(\tilde{v}_a-v_a\right)^T``. + +To apply the TVF, use the keyword argument `shifting_technique` in the constructor +of a system that supports it. + +```@autodocs +Modules = [TrixiParticles] +Pages = [joinpath("schemes", "fluid", "shifting_techniques.jl")] +``` + ## [Tensile Instability Control](@id tic) diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl index b6582a1701..1b8b15cda4 100644 --- a/examples/fluid/lid_driven_cavity_2d.jl +++ b/examples/fluid/lid_driven_cavity_2d.jl @@ -65,7 +65,7 @@ if wcsph state_equation, smoothing_kernel, pressure_acceleration=TrixiParticles.inter_particle_averaged_pressure, smoothing_length, viscosity=viscosity, - transport_velocity=TransportVelocityAdami(pressure)) + shifting_technique=TransportVelocityAdami(pressure)) else state_equation = nothing density_calculator = ContinuityDensity() @@ -73,7 +73,7 @@ else smoothing_length, density_calculator=density_calculator, sound_speed, viscosity=viscosity, - transport_velocity=TransportVelocityAdami(pressure)) + shifting_technique=TransportVelocityAdami(pressure)) end # ========================================================================================== diff --git a/examples/fluid/periodic_array_of_cylinders_2d.jl b/examples/fluid/periodic_array_of_cylinders_2d.jl index 850c9f2ff9..648a98be11 100644 --- a/examples/fluid/periodic_array_of_cylinders_2d.jl +++ b/examples/fluid/periodic_array_of_cylinders_2d.jl @@ -60,7 +60,7 @@ smoothing_length = 1.2 * particle_spacing smoothing_kernel = SchoenbergQuarticSplineKernel{2}() fluid_system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, smoothing_length, sound_speed, viscosity=ViscosityAdami(; nu), - transport_velocity=TransportVelocityAdami(pressure), + shifting_technique=TransportVelocityAdami(pressure), acceleration=(acceleration_x, 0.0)) # ========================================================================================== diff --git a/examples/fluid/periodic_channel_2d.jl b/examples/fluid/periodic_channel_2d.jl index 74770c318b..9cd4bd95a5 100644 --- a/examples/fluid/periodic_channel_2d.jl +++ b/examples/fluid/periodic_channel_2d.jl @@ -28,7 +28,7 @@ initial_fluid_size = tank_size initial_velocity = (1.0, 0.0) fluid_density = 1000.0 -sound_speed = initial_velocity[1] +sound_speed = 10 * initial_velocity[1] state_equation = StateEquationCole(; sound_speed, reference_density=fluid_density, exponent=7) @@ -48,6 +48,7 @@ viscosity = ArtificialViscosityMonaghan(alpha=0.02, beta=0.0) fluid_system = WeaklyCompressibleSPHSystem(tank.fluid, fluid_density_calculator, state_equation, smoothing_kernel, smoothing_length, viscosity=viscosity, + shifting_technique=nothing, pressure_acceleration=nothing) # ========================================================================================== diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl index 3544ac7a8d..85e3461ccc 100644 --- a/examples/fluid/pipe_flow_2d.jl +++ b/examples/fluid/pipe_flow_2d.jl @@ -74,6 +74,7 @@ viscosity = ViscosityAdami(nu=kinematic_viscosity) fluid_system = EntropicallyDampedSPHSystem(pipe.fluid, smoothing_kernel, smoothing_length, sound_speed, viscosity=viscosity, density_calculator=fluid_density_calculator, + shifting_technique=ParticleShiftingTechnique(), buffer_size=n_buffer_particles) # Alternatively the WCSPH scheme can be used @@ -86,6 +87,7 @@ if wcsph fluid_system = WeaklyCompressibleSPHSystem(pipe.fluid, fluid_density_calculator, state_equation, smoothing_kernel, smoothing_length, viscosity=viscosity, + shifting_technique=ParticleShiftingTechnique(), buffer_size=n_buffer_particles) end @@ -159,12 +161,10 @@ ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=100) saving_callback = SolutionSavingCallback(dt=0.02, prefix="") -particle_shifting = ParticleShiftingCallback() extra_callback = nothing -callbacks = CallbackSet(info_callback, saving_callback, UpdateCallback(), - particle_shifting, extra_callback) +callbacks = CallbackSet(info_callback, saving_callback, UpdateCallback(), extra_callback) sol = solve(ode, RDPK3SpFSAL35(), abstol=1e-5, # Default abstol is 1e-6 (may need to be tuned to prevent boundary penetration) diff --git a/examples/fluid/taylor_green_vortex_2d.jl b/examples/fluid/taylor_green_vortex_2d.jl index e6e74fbc4b..8a99c455c6 100644 --- a/examples/fluid/taylor_green_vortex_2d.jl +++ b/examples/fluid/taylor_green_vortex_2d.jl @@ -86,13 +86,13 @@ if wcsph pressure_acceleration=TrixiParticles.inter_particle_averaged_pressure, smoothing_length, viscosity=ViscosityAdami(; nu), - transport_velocity=TransportVelocityAdami(background_pressure)) + shifting_technique=TransportVelocityAdami(background_pressure)) else density_calculator = SummationDensity() fluid_system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, smoothing_length, sound_speed, density_calculator=density_calculator, - transport_velocity=TransportVelocityAdami(background_pressure), + shifting_technique=TransportVelocityAdami(background_pressure), viscosity=ViscosityAdami(; nu)) end diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 987e208750..2a53025d46 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -65,10 +65,9 @@ export WeaklyCompressibleSPHSystem, EntropicallyDampedSPHSystem, TotalLagrangian BoundarySPHSystem, DEMSystem, BoundaryDEMSystem, OpenBoundarySPHSystem export BoundaryZone, InFlow, OutFlow, BidirectionalFlow export InfoCallback, SolutionSavingCallback, DensityReinitializationCallback, - PostprocessCallback, StepsizeCallback, UpdateCallback, SteadyStateReachedCallback, - ParticleShiftingCallback + PostprocessCallback, StepsizeCallback, UpdateCallback, SteadyStateReachedCallback export ContinuityDensity, SummationDensity -export PenaltyForceGanzenmueller, TransportVelocityAdami +export PenaltyForceGanzenmueller, TransportVelocityAdami, ParticleShiftingTechnique export SchoenbergCubicSplineKernel, SchoenbergQuarticSplineKernel, SchoenbergQuinticSplineKernel, GaussianKernel, WendlandC2Kernel, WendlandC4Kernel, WendlandC6Kernel, SpikyKernel, Poly6Kernel diff --git a/src/callbacks/callbacks.jl b/src/callbacks/callbacks.jl index 062a16e3a9..e9eb048c3f 100644 --- a/src/callbacks/callbacks.jl +++ b/src/callbacks/callbacks.jl @@ -32,4 +32,3 @@ include("post_process.jl") include("stepsize.jl") include("update.jl") include("steady_state_reached.jl") -include("particle_shifting.jl") diff --git a/src/callbacks/particle_shifting.jl b/src/callbacks/particle_shifting.jl deleted file mode 100644 index 921370f3b1..0000000000 --- a/src/callbacks/particle_shifting.jl +++ /dev/null @@ -1,148 +0,0 @@ -@doc raw""" - ParticleShiftingCallback() - -Callback to apply the Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017). -Following the original paper, the callback is applied in every time step and not -in every stage of a multi-stage time integration method to reduce the computational -cost and improve the stability of the scheme. - -See [Callbacks](@ref Callbacks) for more information on how to use this callback. -See [Particle Shifting Technique](@ref shifting) for more information on the method itself. - -!!! warning - The Particle Shifting Technique needs to be disabled close to the free surface - and therefore requires a free surface detection method. This is not yet implemented. - **This callback cannot be used in a free surface simulation.** -""" -function ParticleShiftingCallback() - # The first one is the `condition`, the second the `affect!` - return DiscreteCallback((particle_shifting_condition), particle_shifting!, - save_positions=(false, false)) -end - -# `condition` -function particle_shifting_condition(u, t, integrator) - return true -end - -# `affect!` -function particle_shifting!(integrator) - t = integrator.t - semi = integrator.p - v_ode, u_ode = integrator.u.x - dt = integrator.dt - # Internal cache vector, which is safe to use as temporary array - vu_cache = first(get_tmp_cache(integrator)) - - @trixi_timeit timer() "particle shifting callback" begin - # Update quantities that are stored in the systems. These quantities (e.g. pressure) - # still have the values from the last stage of the previous step if not updated here. - @trixi_timeit timer() "update systems and nhs" begin - # Don't create sub-timers here to avoid cluttering the timer output - @notimeit timer() update_systems_and_nhs(v_ode, u_ode, semi, t) - end - - @trixi_timeit timer() "particle shifting" foreach_system(semi) do system - u = wrap_u(u_ode, system, semi) - v = wrap_v(v_ode, system, semi) - particle_shifting!(u, v, system, v_ode, u_ode, semi, vu_cache, dt) - end - end - - # Tell OrdinaryDiffEq that `u` has been modified - u_modified!(integrator, true) - - return integrator -end - -function particle_shifting!(u, v, system, v_ode, u_ode, semi, u_cache, dt) - return u -end - -function particle_shifting!(u, v, system::FluidSystem, v_ode, u_ode, semi, - vu_cache, dt) - # Wrap the cache vector to an NDIMS x NPARTICLES matrix. - # We need this buffer because we cannot safely update `u` while iterating over it. - _, u_cache = vu_cache.x - delta_r = wrap_u(u_cache, system, semi) - set_zero!(delta_r) - - # This has similar performance to `maximum(..., eachparticle(system))`, - # but is GPU-compatible. - v_max = maximum(x -> sqrt(dot(x, x)), - reinterpret(reshape, SVector{ndims(system), eltype(v)}, - current_velocity(v, system))) - - # TODO this needs to be adapted to multi-resolution. - # Section 3.2 explains what else needs to be changed. - dx = particle_spacing(system, 1) - Wdx = smoothing_kernel(system, dx, 1) - h = smoothing_length(system, 1) - - foreach_system(semi) do neighbor_system - u_neighbor = wrap_u(u_ode, neighbor_system, semi) - v_neighbor = wrap_v(v_ode, neighbor_system, semi) - - system_coords = current_coordinates(u, system) - neighbor_coords = current_coordinates(u_neighbor, neighbor_system) - - foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, - semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance - m_b = hydrodynamic_mass(neighbor_system, neighbor) - rho_a = current_density(v, system, particle) - rho_b = current_density(v_neighbor, neighbor_system, neighbor) - - kernel = smoothing_kernel(system, distance, particle) - grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle) - - # According to p. 29 below Eq. 9 - R = 2 // 10 - n = 4 - - # Eq. 7 in Sun et al. (2017). - # According to the paper, CFL * Ma can be rewritten as Δt * v_max / h - # (see p. 29, right above Eq. 9), but this does not work when scaling h. - # When setting CFL * Ma = Δt * v_max / (2 * Δx), PST works as expected - # for both small and large smoothing length factors. - # We need to scale - # - quadratically with the smoothing length, - # - linearly with the particle spacing, - # - linearly with the time step. - # See https://github.com/trixi-framework/TrixiParticles.jl/pull/834. - delta_r_ = -dt * v_max * (2 * h)^2 / (2 * dx) * (1 + R * (kernel / Wdx)^n) * - m_b / (rho_a + rho_b) * grad_kernel - - # Write into the buffer - for i in eachindex(delta_r_) - @inbounds delta_r[i, particle] += delta_r_[i] - end - end - end - - # Add δ_r from the buffer to the current coordinates - @threaded semi for particle in eachparticle(system) - for i in axes(delta_r, 1) - @inbounds u[i, particle] += delta_r[i, particle] - end - end - - return u -end - -function Base.show(io::IO, cb::DiscreteCallback{<:Any, typeof(particle_shifting!)}) - @nospecialize cb # reduce precompilation time - print(io, "ParticleShiftingCallback()") -end - -function Base.show(io::IO, ::MIME"text/plain", - cb::DiscreteCallback{<:Any, typeof(particle_shifting!)}) - @nospecialize cb # reduce precompilation time - - if get(io, :compact, false) - show(io, cb) - else - summary_box(io, "ParticleShiftingCallback") - end -end diff --git a/src/callbacks/update.jl b/src/callbacks/update.jl index 77496a46c5..8eecb8380a 100644 --- a/src/callbacks/update.jl +++ b/src/callbacks/update.jl @@ -74,22 +74,32 @@ function (update_callback!::UpdateCallback)(integrator) semi = integrator.p v_ode, u_ode = integrator.u.x - # Update quantities that are stored in the systems. These quantities (e.g. pressure) - # still have the values from the last stage of the previous step if not updated here. - update_systems_and_nhs(v_ode, u_ode, semi, t) - - # Update open boundaries first, since particles might be activated or deactivated - @trixi_timeit timer() "update open boundary" foreach_system(semi) do system - update_open_boundary_eachstep!(system, v_ode, u_ode, semi, t) - end - - @trixi_timeit timer() "update particle packing" foreach_system(semi) do system - update_particle_packing(system, v_ode, u_ode, semi, integrator) - end - - # This is only used by the particle packing system and should be removed in the future - @trixi_timeit timer() "update TVF" foreach_system(semi) do system - update_transport_velocity!(system, v_ode, semi) + @trixi_timeit timer() "update callback" begin + # Update quantities that are stored in the systems. These quantities (e.g. pressure) + # still have the values from the last stage of the previous step if not updated here. + @trixi_timeit timer() "update systems and nhs" begin + # Don't create sub-timers here to avoid cluttering the timer output + @notimeit timer() update_systems_and_nhs(v_ode, u_ode, semi, t) + end + + # Update open boundaries first, since particles might be activated or deactivated + @trixi_timeit timer() "update open boundary" foreach_system(semi) do system + update_open_boundary_eachstep!(system, v_ode, u_ode, semi, t) + end + + @trixi_timeit timer() "update particle packing" foreach_system(semi) do system + update_particle_packing(system, v_ode, u_ode, semi, integrator) + end + + # This is only used by the particle packing system and should be removed in the future + @trixi_timeit timer() "update TVF" foreach_system(semi) do system + update_transport_velocity!(system, v_ode, semi) + end + + @trixi_timeit timer() "particle shifting" foreach_system(semi) do system + particle_shifting_from_callback!(u_ode, shifting_technique(system), system, + v_ode, semi, integrator.dt) + end end # Tell OrdinaryDiffEq that `u` has been modified diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl index a9c78b3349..9532d812ab 100644 --- a/src/general/semidiscretization.jl +++ b/src/general/semidiscretization.jl @@ -485,7 +485,7 @@ end @inline add_velocity!(du, v, particle, system::BoundarySPHSystem) = du @inline function add_velocity!(du, v, particle, system::FluidSystem) - # This is zero unless a transport velocity is used + # This is zero unless a shifting technique is used delta_v_ = delta_v(system, particle) for i in 1:ndims(system) diff --git a/src/general/system.jl b/src/general/system.jl index 929fb8b425..5b4d5aecb8 100644 --- a/src/general/system.jl +++ b/src/general/system.jl @@ -147,9 +147,6 @@ function update_final!(system, v, u, v_ode, u_ode, semi, t) return system end -# Only for systems requiring the use of the `UpdateCallback` -@inline requires_update_callback(system) = false - @inline initial_smoothing_length(system) = smoothing_length(system, nothing) @inline function smoothing_length(system, particle) diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index b93e25f324..1aca732098 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -349,9 +349,6 @@ function write2vtk!(vtk, v, u, t, system::FluidSystem; write_meta_data=true) else vtk["solver"] = "EDAC" vtk["sound_speed"] = system.sound_speed - vtk["background_pressure_TVF"] = system.transport_velocity isa Nothing ? - "-" : - system.transport_velocity.background_pressure end end diff --git a/src/schemes/fluid/entropically_damped_sph/rhs.jl b/src/schemes/fluid/entropically_damped_sph/rhs.jl index 08139e2ca6..a4d7d280a4 100644 --- a/src/schemes/fluid/entropically_damped_sph/rhs.jl +++ b/src/schemes/fluid/entropically_damped_sph/rhs.jl @@ -51,13 +51,12 @@ function interact!(dv, v_particle_system, u_particle_system, particle, neighbor, pos_diff, distance, sound_speed, m_a, m_b, rho_a, rho_b, grad_kernel) - # Add convection term (only when using `TransportVelocityAdami`) - dv_tvf = dv_transport_velocity(transport_velocity(particle_system), - particle_system, neighbor_system, - particle, neighbor, - v_particle_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) + # Extra terms in the momentum equation when using a shifting technique + dv_tvf = dv_shifting(shifting_technique(particle_system), + particle_system, neighbor_system, particle, neighbor, + v_particle_system, v_neighbor_system, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) dv_surface_tension = surface_tension_force(surface_tension_a, surface_tension_b, particle_system, neighbor_system, diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index 19e50c441a..89d8a923ff 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -3,7 +3,7 @@ smoothing_length, sound_speed; pressure_acceleration=inter_particle_averaged_pressure, density_calculator=SummationDensity(), - transport_velocity=nothing, + shifting_technique=nothing, alpha=0.5, viscosity=nothing, acceleration=ntuple(_ -> 0.0, NDIMS), surface_tension=nothing, surface_normal_method=nothing, buffer_size=nothing, @@ -30,10 +30,11 @@ See [Entropically Damped Artificial Compressibility for SPH](@ref edac) for more When set to `nothing`, the pressure acceleration formulation for the corresponding [density calculator](@ref density_calculator) is chosen. - `density_calculator`: [Density calculator](@ref density_calculator) (default: [`SummationDensity`](@ref)) -- `transport_velocity`: [Transport Velocity Formulation (TVF)](@ref transport_velocity_formulation). - Default is no TVF. +- `shifting_technique`: [Shifting technique](@ref shifting) or [transport velocity + formulation](@ref transport_velocity_formulation) to use + with this system. Default is no shifting. - `average_pressure_reduction`: Whether to subtract the average pressure of neighboring particles - from the local pressure (default: `true` when using TVF, `false` otherwise). + from the local pressure (default: `true` when using shifting, `false` otherwise). - `buffer_size`: Number of buffer particles. This is needed when simulating with [`OpenBoundarySPHSystem`](@ref). - `correction`: Correction method used for this system. (default: no correction, see [Corrections](@ref corrections)) @@ -67,7 +68,7 @@ struct EntropicallyDampedSPHSystem{NDIMS, ELTYPE <: Real, IC, M, DC, K, V, COR, acceleration :: SVector{NDIMS, ELTYPE} correction :: COR pressure_acceleration_formulation :: PF - transport_velocity :: TV + shifting_technique :: TV average_pressure_reduction :: AVGP source_terms :: ST surface_tension :: SRFT @@ -83,8 +84,8 @@ function EntropicallyDampedSPHSystem(initial_condition, smoothing_kernel, smoothing_length, sound_speed; pressure_acceleration=inter_particle_averaged_pressure, density_calculator=SummationDensity(), - transport_velocity=nothing, - average_pressure_reduction=(!isnothing(transport_velocity)), + shifting_technique=nothing, + average_pressure_reduction=(!isnothing(shifting_technique)), alpha=0.5, viscosity=nothing, acceleration=ntuple(_ -> 0.0, ndims(smoothing_kernel)), @@ -137,7 +138,7 @@ function EntropicallyDampedSPHSystem(initial_condition, smoothing_kernel, nu_edac = (alpha * smoothing_length * sound_speed) / 8 cache = (; create_cache_density(initial_condition, density_calculator)..., - create_cache_tvf(initial_condition, transport_velocity)..., + create_cache_shifting(initial_condition, shifting_technique)..., create_cache_avg_pressure_reduction(initial_condition, avg_pressure_reduction)..., create_cache_surface_normal(surface_normal_method, ELTYPE, NDIMS, @@ -161,14 +162,14 @@ function EntropicallyDampedSPHSystem(initial_condition, smoothing_kernel, EntropicallyDampedSPHSystem{NDIMS, ELTYPE, typeof(initial_condition), typeof(mass), typeof(density_calculator), typeof(smoothing_kernel), typeof(viscosity), typeof(correction), - typeof(pressure_acceleration), typeof(transport_velocity), + typeof(pressure_acceleration), typeof(shifting_technique), typeof(avg_pressure_reduction), typeof(source_terms), typeof(surface_tension), typeof(surface_normal_method), typeof(buffer), Nothing, typeof(cache)}(initial_condition, mass, density_calculator, smoothing_kernel, sound_speed, viscosity, nu_edac, acceleration_, correction, - pressure_acceleration, transport_velocity, + pressure_acceleration, shifting_technique, avg_pressure_reduction, source_terms, surface_tension, surface_normal_method, buffer, @@ -218,8 +219,7 @@ function Base.show(io::IO, ::MIME"text/plain", system::EntropicallyDampedSPHSyst summary_line(io, "viscosity", system.viscosity |> typeof |> nameof) summary_line(io, "ν₍EDAC₎", "≈ $(round(system.nu_edac; digits=3))") summary_line(io, "smoothing kernel", system.smoothing_kernel |> typeof |> nameof) - summary_line(io, "tansport velocity formulation", - system.transport_velocity |> typeof |> nameof) + summary_line(io, "shifting technique", system.shifting_technique) summary_line(io, "average pressure reduction", typeof(system.average_pressure_reduction).parameters[1] ? "yes" : "no") summary_line(io, "acceleration", system.acceleration) @@ -271,7 +271,7 @@ end @inline system_sound_speed(system::EntropicallyDampedSPHSystem) = system.sound_speed -@inline transport_velocity(system::EntropicallyDampedSPHSystem) = system.transport_velocity +@inline shifting_technique(system::EntropicallyDampedSPHSystem) = system.shifting_technique @inline function average_pressure(system::EntropicallyDampedSPHSystem, particle) average_pressure(system, system.average_pressure_reduction, particle) @@ -321,7 +321,7 @@ function update_final!(system::EntropicallyDampedSPHSystem, v, u, v_ode, u_ode, compute_curvature!(system, surface_tension, v, u, v_ode, u_ode, semi, t) compute_stress_tensors!(system, surface_tension, v, u, v_ode, u_ode, semi, t) update_average_pressure!(system, system.average_pressure_reduction, v_ode, u_ode, semi) - update_tvf!(system, transport_velocity(system), v, u, v_ode, u_ode, semi, t) + update_shifting!(system, shifting_technique(system), v, u, v_ode, u_ode, semi) end # No average pressure reduction is used diff --git a/src/schemes/fluid/fluid.jl b/src/schemes/fluid/fluid.jl index d3e6adf971..59703f9b6d 100644 --- a/src/schemes/fluid/fluid.jl +++ b/src/schemes/fluid/fluid.jl @@ -193,7 +193,7 @@ end include("pressure_acceleration.jl") include("viscosity.jl") -include("transport_velocity.jl") +include("shifting_techniques.jl") include("surface_tension.jl") include("surface_normal_sph.jl") include("weakly_compressible_sph/weakly_compressible_sph.jl") diff --git a/src/schemes/fluid/shifting_techniques.jl b/src/schemes/fluid/shifting_techniques.jl new file mode 100644 index 0000000000..98775b3be0 --- /dev/null +++ b/src/schemes/fluid/shifting_techniques.jl @@ -0,0 +1,291 @@ +abstract type AbstractShiftingTechnique end + +# No shifting for a system by default +@inline shifting_technique(system) = nothing + +# WARNING: Be careful if defining this function for a specific system type. +# The version for a specific system type will override this generic version. +requires_update_callback(system) = requires_update_callback(shifting_technique(system)) +requires_update_callback(::Nothing) = false +requires_update_callback(::AbstractShiftingTechnique) = true + +# This is called from the `UpdateCallback` +particle_shifting_from_callback!(u_ode, shifting, system, v_ode, semi, dt) = u_ode + +create_cache_shifting(initial_condition, ::Nothing) = (;) + +function create_cache_shifting(initial_condition, ::AbstractShiftingTechnique) + delta_v = zeros(eltype(initial_condition), ndims(initial_condition), + nparticles(initial_condition)) + + return (; delta_v) +end + +# `δv` is the correction to the particle velocity due to the shifting. +# Particles are advected with the velocity `v + δv`. +@propagate_inbounds function delta_v(system, particle) + return delta_v(system, shifting_technique(system), particle) +end + +# Zero when no shifting is used +@inline function delta_v(system, shifting, particle) + return zero(SVector{ndims(system), eltype(system)}) +end + +@propagate_inbounds function delta_v(system, ::AbstractShiftingTechnique, particle) + return extract_svector(system.cache.delta_v, system, particle) +end + +function update_shifting!(system, shifting, v, u, v_ode, u_ode, semi) + return system +end + +# Additional term in the momentum equation due to the shifting technique +@inline function dv_shifting(shifting, system, neighbor_system, + particle, neighbor, v_system, v_neighbor_system, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) + return zero(grad_kernel) +end + +@doc raw""" + ParticleShiftingTechnique() + +Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017). +Following the original paper, the callback is applied in every time step and not +in every stage of a multi-stage time integration method to reduce the computational +cost and improve the stability of the scheme. + +See [Particle Shifting Technique](@ref shifting) for more information on the method. + +!!! warning + The Particle Shifting Technique needs to be disabled close to the free surface + and therefore requires a free surface detection method. This is not yet implemented. + **This technique cannot be used in a free surface simulation.** +""" +struct ParticleShiftingTechnique <: AbstractShiftingTechnique end + +# Zero because PST is applied in a callback +@inline function delta_v(system, ::ParticleShiftingTechnique, particle) + return zero(SVector{ndims(system), eltype(system)}) +end + +function particle_shifting_from_callback!(u_ode, shifting::ParticleShiftingTechnique, + system, v_ode, semi, dt) + @trixi_timeit timer() "particle shifting" begin + v = wrap_v(v_ode, system, semi) + u = wrap_u(u_ode, system, semi) + + # Update the shifting velocity + update_shifting_from_callback!(system, shifting, v, u, v_ode, u_ode, semi) + + # Update the particle positions with the shifting velocity + particle_shifting!(u_ode, shifting, system, semi, dt) + end +end + +function update_shifting_from_callback!(system, ::ParticleShiftingTechnique, + v, u, v_ode, u_ode, semi) + (; cache) = system + (; delta_v) = cache + + set_zero!(delta_v) + + # This has similar performance to `maximum(..., eachparticle(system))`, + # but is GPU-compatible. + v_max = maximum(x -> sqrt(dot(x, x)), + reinterpret(reshape, SVector{ndims(system), eltype(v)}, + current_velocity(v, system))) + + # TODO this needs to be adapted to multi-resolution. + # Section 3.2 explains what else needs to be changed. + dx = particle_spacing(system, 1) + Wdx = smoothing_kernel(system, dx, 1) + h = smoothing_length(system, 1) + + foreach_system(semi) do neighbor_system + u_neighbor = wrap_u(u_ode, neighbor_system, semi) + v_neighbor = wrap_v(v_ode, neighbor_system, semi) + + system_coords = current_coordinates(u, system) + neighbor_coords = current_coordinates(u_neighbor, neighbor_system) + + foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, + semi; + points=each_moving_particle(system)) do particle, neighbor, + pos_diff, distance + m_b = hydrodynamic_mass(neighbor_system, neighbor) + rho_a = current_density(v, system, particle) + rho_b = current_density(v_neighbor, neighbor_system, neighbor) + + kernel = smoothing_kernel(system, distance, particle) + grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle) + + # According to p. 29 below Eq. 9 + R = 2 // 10 + n = 4 + + # Eq. 7 in Sun et al. (2017). + # According to the paper, CFL * Ma can be rewritten as Δt * v_max / h + # (see p. 29, right above Eq. 9), but this does not work when scaling h. + # When setting CFL * Ma = Δt * v_max / (2 * Δx), PST works as expected + # for both small and large smoothing length factors. + # We need to scale + # - quadratically with the smoothing length, + # - linearly with the particle spacing, + # - linearly with the time step. + # See https://github.com/trixi-framework/TrixiParticles.jl/pull/834. + delta_v_ = -v_max * (2 * h)^2 / (2 * dx) * (1 + R * (kernel / Wdx)^n) * + m_b / (rho_a + rho_b) * grad_kernel + + # Write into the buffer + for i in eachindex(delta_v_) + @inbounds delta_v[i, particle] += delta_v_[i] + end + end + end + + return system +end + +function particle_shifting!(u_ode, ::ParticleShiftingTechnique, system, semi, dt) + (; cache) = system + (; delta_v) = cache + + u = wrap_u(u_ode, system, semi) + + # Add δr from the cache to the current coordinates + @threaded semi for particle in eachparticle(system) + for i in axes(delta_v, 1) + @inbounds u[i, particle] += dt * delta_v[i, particle] + end + end + + return u +end + +""" + TransportVelocityAdami(background_pressure::Real) + +Transport Velocity Formulation (TVF) by [Adami et al. (2013)](@cite Adami2013) +to suppress pairing and tensile instability. +See [TVF](@ref transport_velocity_formulation) for more details of the method. + +# Arguments +- `background_pressure`: Background pressure. Suggested is a background pressure which is + on the order of the reference pressure. + +!!! warning + The Transport Velocity Formulation needs to be disabled close to the free surface + and therefore requires a free surface detection method. This is not yet implemented. + **This technique cannot be used in a free surface simulation.** +""" +struct TransportVelocityAdami{T <: Real} <: AbstractShiftingTechnique + background_pressure::T +end + +@inline function dv_shifting(::TransportVelocityAdami, system, neighbor_system, + particle, neighbor, v_system, v_neighbor_system, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) + v_a = current_velocity(v_system, system, particle) + delta_v_a = delta_v(system, particle) + + v_b = current_velocity(v_neighbor_system, neighbor_system, neighbor) + delta_v_b = delta_v(neighbor_system, neighbor) + + A_a = rho_a * v_a * delta_v_a' + A_b = rho_b * v_b * delta_v_b' + + # The following term depends on the pressure acceleration formulation. + # See the large comment below. In the original paper (Adami et al., 2013), this is + # (V_a^2 + V_b^2) / m_a * ((A_a + A_b) / 2) * ∇W_ab. + # With the most common pressure acceleration formulation, this is + # m_b * (A_a + A_b) / (ρ_a * ρ_b) * ∇W_ab. + # In order to obtain this, we pass `p_a = A_a` and `p_b = A_b` to the + # `pressure_acceleration` function. + return pressure_acceleration(system, neighbor_system, particle, neighbor, + m_a, m_b, A_a, A_b, rho_a, rho_b, pos_diff, + distance, grad_kernel, correction) +end + +function update_shifting!(system, shifting::TransportVelocityAdami, v, u, v_ode, + u_ode, semi) + (; cache, correction) = system + (; delta_v) = cache + (; background_pressure) = shifting + + sound_speed = system_sound_speed(system) + + set_zero!(delta_v) + + foreach_system(semi) do neighbor_system + v_neighbor = wrap_v(v_ode, neighbor_system, semi) + u_neighbor = wrap_u(u_ode, neighbor_system, semi) + + system_coords = current_coordinates(u, system) + neighbor_coords = current_coordinates(u_neighbor, neighbor_system) + + foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, + semi; + points=each_moving_particle(system)) do particle, neighbor, + pos_diff, distance + m_a = @inbounds hydrodynamic_mass(system, particle) + m_b = @inbounds hydrodynamic_mass(neighbor_system, neighbor) + + rho_a = @inbounds current_density(v, system, particle) + rho_b = @inbounds current_density(v_neighbor, neighbor_system, neighbor) + + h = smoothing_length(system, particle) + + grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle) + + # In the original paper (Adami et al., 2013), the transport velocity is applied + # as follows: + # v_{1/2} = v_0 + Δt/2 * a, + # where a is the regular SPH acceleration term (pressure, viscosity, etc.). + # r_1 = r_0 + Δt * (v_{1/2}, + # where ̃v_{1/2} = v_{1/2} + Δt/2 * p_0 / m_a * \sum_b[ (V_a^2 + V_b^2) * ∇W_ab ] + # is the transport velocity. + # We call δv_{1/2} = ̃v_{1/2} - v_{1/2} the shifting velocity. + # We will call δv_{1/2} the shifting velocity, which is given by + # δv = -Δt/2 * p_0 / m_a * \sum_b[ (V_a^2 + V_b^2) * ∇W_ab ], + # where p_0 is the background pressure, V_a = m_a / ρ_a, V_b = m_b / ρ_b. + # This term depends on the pressure acceleration formulation. + # In Zhang et al. (2017), the pressure acceleration term + # m_b * (p_a / ρ_a^2 + p_b / ρ_b^2) * ∇W_ab + # is used. They consequently changed the shifting velocity to + # δv = -Δt/2 * p_0 * \sum_b[ m_b * (1 / ρ_a^2 + 1 / ρ_b^2) * ∇W_ab ]. + # We therefore use the function `pressure_acceleration` to compute the + # shifting velocity according to the used pressure acceleration formulation. + # In most cases, this will be + # δv = -Δt/2 * p_0 * \sum_b[ m_b * (1 + 1) / (ρ_a * ρ_b) * ∇W_ab ]. + # + # In these papers, the shifting velocity is scaled by the time step Δt. + # We generally want the spatial discretization to be independent of the time step. + # Scaling the shifting velocity by the time step would lead to less shifting + # when very small time steps are used for testing/debugging purposes. + # This is especially problematic in TrixiParticles.jl, as the time step can vary + # significantly between different time integration methods (low vs high order). + # In order to eliminate the time step from the shifting velocity, we apply the + # CFL condition used in Adami et al. (2013): + # Δt <= 0.25 * h / c, + # where h is the smoothing length and c is the sound speed. + # Applying this equation as equality yields the shifting velocity + # δv = -p_0 / 8 * h / c * \sum_b[ m_b * (1 + 1) / (ρ_a * ρ_b) * ∇W_ab ]. + # The last part is achieved by passing `p_a = 1` and `p_b = 1` to the + # `pressure_acceleration` function. + delta_v_ = background_pressure / 8 * h / sound_speed * + pressure_acceleration(system, neighbor_system, particle, neighbor, + m_a, m_b, 1, 1, rho_a, rho_b, pos_diff, + distance, grad_kernel, correction) + + # Write into the buffer + for i in eachindex(delta_v_) + @inbounds delta_v[i, particle] += delta_v_[i] + end + end + end + + return system +end diff --git a/src/schemes/fluid/transport_velocity.jl b/src/schemes/fluid/transport_velocity.jl deleted file mode 100644 index fce7375dd5..0000000000 --- a/src/schemes/fluid/transport_velocity.jl +++ /dev/null @@ -1,158 +0,0 @@ -""" - TransportVelocityAdami(background_pressure::Real) - -Transport Velocity Formulation (TVF) by [Adami et al. (2013)](@cite Adami2013) -to suppress pairing and tensile instability. -See [TVF](@ref transport_velocity_formulation) for more details of the method. - -# Arguments -- `background_pressure`: Background pressure. Suggested is a background pressure which is - on the order of the reference pressure. -""" -struct TransportVelocityAdami{T <: Real} - background_pressure::T -end - -# No TVF for a system by default -@inline transport_velocity(system) = nothing - -create_cache_tvf(initial_condition, ::Nothing) = (;) - -function create_cache_tvf(initial_condition, ::TransportVelocityAdami) - delta_v = zeros(eltype(initial_condition), ndims(initial_condition), - nparticles(initial_condition)) - - return (; delta_v) -end - -# `δv` is the correction to the particle velocity due to the TVF. -# Particles are advected with the velocity `v + δv`. -@propagate_inbounds function delta_v(system, particle) - return delta_v(system, transport_velocity(system), particle) -end - -@propagate_inbounds function delta_v(system, ::TransportVelocityAdami, particle) - return extract_svector(system.cache.delta_v, system, particle) -end - -# Zero when no TVF is used -@inline function delta_v(system, transport_velocity, particle) - return zero(SVector{ndims(system), eltype(system)}) -end - -@inline function dv_transport_velocity(::Nothing, system, neighbor_system, - particle, neighbor, v_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) - return zero(grad_kernel) -end - -@inline function dv_transport_velocity(::TransportVelocityAdami, system, neighbor_system, - particle, neighbor, v_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) - v_a = current_velocity(v_system, system, particle) - delta_v_a = delta_v(system, particle) - - v_b = current_velocity(v_neighbor_system, neighbor_system, neighbor) - delta_v_b = delta_v(neighbor_system, neighbor) - - A_a = rho_a * v_a * delta_v_a' - A_b = rho_b * v_b * delta_v_b' - - # The following term depends on the pressure acceleration formulation. - # See the large comment below. In the original paper (Adami et al., 2013), this is - # (V_a^2 + V_b^2) / m_a * ((A_a + A_b) / 2) * ∇W_ab. - # With the most common pressure acceleration formulation, this is - # m_b * (A_a + A_b) / (ρ_a * ρ_b) * ∇W_ab. - # In order to obtain this, we pass `p_a = A_a` and `p_b = A_b` to the - # `pressure_acceleration` function. - return pressure_acceleration(system, neighbor_system, particle, neighbor, - m_a, m_b, A_a, A_b, rho_a, rho_b, pos_diff, - distance, grad_kernel, correction) -end - -function update_tvf!(system, transport_velocity, v, u, v_ode, u_ode, semi, t) - return system -end - -function update_tvf!(system, transport_velocity::TransportVelocityAdami, v, u, v_ode, - u_ode, semi, t) - (; cache, correction) = system - (; delta_v) = cache - (; background_pressure) = transport_velocity - - sound_speed = system_sound_speed(system) - - set_zero!(delta_v) - - foreach_system(semi) do neighbor_system - v_neighbor = wrap_v(v_ode, neighbor_system, semi) - u_neighbor = wrap_u(u_ode, neighbor_system, semi) - - system_coords = current_coordinates(u, system) - neighbor_coords = current_coordinates(u_neighbor, neighbor_system) - - foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, - semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance - m_a = @inbounds hydrodynamic_mass(system, particle) - m_b = @inbounds hydrodynamic_mass(neighbor_system, neighbor) - - rho_a = @inbounds current_density(v, system, particle) - rho_b = @inbounds current_density(v_neighbor, neighbor_system, neighbor) - - h = smoothing_length(system, particle) - - grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle) - - # In the original paper (Adami et al., 2013), the transport velocity is applied - # as follows: - # v_{1/2} = v_0 + Δt/2 * a, - # where a is the regular SPH acceleration term (pressure, viscosity, etc.). - # r_1 = r_0 + Δt * (v_{1/2}, - # where ̃v_{1/2} = v_{1/2} + Δt/2 * p_0 / m_a * \sum_b[ (V_a^2 + V_b^2) * ∇W_ab ] - # is the transport velocity. - # We call δv_{1/2} = ̃v_{1/2} - v_{1/2} the shifting velocity. - # We will call δv_{1/2} the shifting velocity, which is given by - # δv = -Δt/2 * p_0 / m_a * \sum_b[ (V_a^2 + V_b^2) * ∇W_ab ], - # where p_0 is the background pressure, V_a = m_a / ρ_a, V_b = m_b / ρ_b. - # This term depends on the pressure acceleration formulation. - # In Zhang et al. (2017), the pressure acceleration term - # m_b * (p_a / ρ_a^2 + p_b / ρ_b^2) * ∇W_ab - # is used. They consequently changed the shifting velocity to - # δv = -Δt/2 * p_0 * \sum_b[ m_b * (1 / ρ_a^2 + 1 / ρ_b^2) * ∇W_ab ]. - # We therefore use the function `pressure_acceleration` to compute the - # shifting velocity according to the used pressure acceleration formulation. - # In most cases, this will be - # δv = -Δt/2 * p_0 * \sum_b[ m_b * (1 + 1) / (ρ_a * ρ_b) * ∇W_ab ]. - # - # In these papers, the shifting velocity is scaled by the time step Δt. - # We generally want the spatial discretization to be independent of the time step. - # Scaling the shifting velocity by the time step would lead to less shifting - # when very small time steps are used for testing/debugging purposes. - # This is especially problematic in TrixiParticles.jl, as the time step can vary - # significantly between different time integration methods (low vs high order). - # In order to eliminate the time step from the shifting velocity, we apply the - # CFL condition used in Adami et al. (2013): - # Δt <= 0.25 * h / c, - # where h is the smoothing length and c is the sound speed. - # Applying this equation as equality yields the shifting velocity - # δv = -p_0 / 8 * h / c * \sum_b[ m_b * (1 + 1) / (ρ_a * ρ_b) * ∇W_ab ]. - # The last part is achieved by passing `p_a = 1` and `p_b = 1` to the - # `pressure_acceleration` function. - delta_v_ = background_pressure / 8 * h / sound_speed * - pressure_acceleration(system, neighbor_system, particle, neighbor, - m_a, m_b, 1, 1, rho_a, rho_b, pos_diff, - distance, grad_kernel, correction) - - # Write into the buffer - for i in eachindex(delta_v_) - @inbounds delta_v[i, particle] += delta_v_[i] - end - end - end - - return system -end diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl index af088e0a1b..b9cd27f079 100644 --- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl +++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl @@ -70,13 +70,12 @@ function interact!(dv, v_particle_system, u_particle_system, sound_speed, m_a, m_b, rho_a, rho_b, grad_kernel) - # Add convection term (only when using `TransportVelocityAdami`) - dv_tvf = dv_transport_velocity(transport_velocity(particle_system), - particle_system, neighbor_system, - particle, neighbor, - v_particle_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) + # Extra terms in the momentum equation when using a shifting technique + dv_tvf = dv_shifting(shifting_technique(particle_system), + particle_system, neighbor_system, particle, neighbor, + v_particle_system, v_neighbor_system, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) dv_surface_tension = surface_tension_correction * surface_tension_force(surface_tension_a, surface_tension_b, diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index a5f56499b8..dda2ead5f1 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -5,7 +5,7 @@ acceleration=ntuple(_ -> 0.0, NDIMS), viscosity=nothing, density_diffusion=nothing, pressure_acceleration=nothing, - transport_velocity=nothing, + shifting_technique=nothing, buffer_size=nothing, correction=nothing, source_terms=nothing, surface_tension=nothing, surface_normal_method=nothing, @@ -36,8 +36,9 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method. density calculator and the correction method. To use [Tensile Instability Control](@ref tic), pass [`tensile_instability_control`](@ref) here. -- `transport_velocity`: [Transport Velocity Formulation (TVF)](@ref transport_velocity_formulation). - Default is no TVF. +- `shifting_technique`: [Shifting technique](@ref shifting) or [transport velocity + formulation](@ref transport_velocity_formulation) to use + with this system. Default is no shifting. - `buffer_size`: Number of buffer particles. This is needed when simulating with [`OpenBoundarySPHSystem`](@ref). - `correction`: Correction method used for this system. (default: no correction, see [Corrections](@ref corrections)) @@ -59,7 +60,7 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method. - `color_value`: The value used to initialize the color of particles in the system. """ struct WeaklyCompressibleSPHSystem{NDIMS, ELTYPE <: Real, IC, MA, P, DC, SE, K, V, DD, COR, - PF, TV, ST, B, SRFT, SRFN, PR, C} <: FluidSystem{NDIMS} + PF, SC, ST, B, SRFT, SRFN, PR, C} <: FluidSystem{NDIMS} initial_condition :: IC mass :: MA # Array{ELTYPE, 1} pressure :: P # Array{ELTYPE, 1} @@ -71,7 +72,7 @@ struct WeaklyCompressibleSPHSystem{NDIMS, ELTYPE <: Real, IC, MA, P, DC, SE, K, density_diffusion :: DD correction :: COR pressure_acceleration_formulation :: PF - transport_velocity :: TV + shifting_technique :: SC source_terms :: ST surface_tension :: SRFT surface_normal_method :: SRFN @@ -89,7 +90,7 @@ function WeaklyCompressibleSPHSystem(initial_condition, ndims(smoothing_kernel)), viscosity=nothing, density_diffusion=nothing, pressure_acceleration=nothing, - transport_velocity=nothing, + shifting_technique=nothing, buffer_size=nothing, correction=nothing, source_terms=nothing, surface_tension=nothing, surface_normal_method=nothing, @@ -147,7 +148,7 @@ function WeaklyCompressibleSPHSystem(initial_condition, n_particles)..., create_cache_refinement(initial_condition, particle_refinement, smoothing_length)..., - create_cache_tvf(initial_condition, transport_velocity)..., + create_cache_shifting(initial_condition, shifting_technique)..., color=Int(color_value)) # If the `reference_density_spacing` is set calculate the `ideal_neighbor_count` @@ -162,7 +163,7 @@ function WeaklyCompressibleSPHSystem(initial_condition, density_calculator, state_equation, smoothing_kernel, acceleration_, viscosity, density_diffusion, correction, pressure_acceleration, - transport_velocity, source_terms, surface_tension, + shifting_technique, source_terms, surface_tension, surface_normal_method, buffer, particle_refinement, cache) end @@ -177,6 +178,7 @@ function Base.show(io::IO, system::WeaklyCompressibleSPHSystem) print(io, ", ", system.smoothing_kernel) print(io, ", ", system.viscosity) print(io, ", ", system.density_diffusion) + print(io, ", ", system.shifting_technique) print(io, ", ", system.surface_tension) print(io, ", ", system.surface_normal_method) if system.surface_normal_method isa ColorfieldSurfaceNormal @@ -207,9 +209,8 @@ function Base.show(io::IO, ::MIME"text/plain", system::WeaklyCompressibleSPHSyst summary_line(io, "state equation", system.state_equation |> typeof |> nameof) summary_line(io, "smoothing kernel", system.smoothing_kernel |> typeof |> nameof) summary_line(io, "viscosity", system.viscosity) - summary_line(io, "tansport velocity formulation", - system.transport_velocity |> typeof |> nameof) summary_line(io, "density diffusion", system.density_diffusion) + summary_line(io, "shifting technique", system.shifting_technique) summary_line(io, "surface tension", system.surface_tension) summary_line(io, "surface normal method", system.surface_normal_method) if system.surface_normal_method isa ColorfieldSurfaceNormal @@ -278,7 +279,7 @@ end @inline system_sound_speed(system::WeaklyCompressibleSPHSystem) = system.state_equation.sound_speed -@inline transport_velocity(system::WeaklyCompressibleSPHSystem) = system.transport_velocity +@inline shifting_technique(system::WeaklyCompressibleSPHSystem) = system.shifting_technique function update_quantities!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_ode, semi, t) @@ -316,7 +317,7 @@ function update_final!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_ode, # Surface normal of neighbor and boundary needs to have been calculated already compute_curvature!(system, surface_tension, v, u, v_ode, u_ode, semi, t) compute_stress_tensors!(system, surface_tension, v, u, v_ode, u_ode, semi, t) - update_tvf!(system, transport_velocity(system), v, u, v_ode, u_ode, semi, t) + update_shifting!(system, shifting_technique(system), v, u, v_ode, u_ode, semi) end function kernel_correct_density!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_ode, diff --git a/test/examples/examples_fluid.jl b/test/examples/examples_fluid.jl index ede9e6bbf9..a84a69317d 100644 --- a/test/examples/examples_fluid.jl +++ b/test/examples/examples_fluid.jl @@ -273,7 +273,19 @@ joinpath(examples_dir(), "fluid", "periodic_channel_2d.jl"), tspan=(0.0, 0.2), - extra_callback=ParticleShiftingCallback()) + shifting_technique=ParticleShiftingTechnique(), + extra_callback=UpdateCallback()) + @test sol.retcode == ReturnCode.Success + @test count_rhs_allocations(sol, semi) == 0 + end + + @trixi_testset "fluid/periodic_channel_2d.jl with TVF" begin + @trixi_test_nowarn trixi_include(@__MODULE__, + joinpath(examples_dir(), "fluid", + "periodic_channel_2d.jl"), + tspan=(0.0, 0.2), + shifting_technique=TransportVelocityAdami(50_000.0), + extra_callback=UpdateCallback()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end @@ -283,8 +295,9 @@ joinpath(examples_dir(), "fluid", "periodic_channel_2d.jl"), tspan=(0.0, 0.2), - extra_callback=ParticleShiftingCallback(), - pressure_acceleration=tensile_instability_control) + shifting_technique=ParticleShiftingTechnique(), + pressure_acceleration=tensile_instability_control, + extra_callback=UpdateCallback()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end diff --git a/test/systems/edac_system.jl b/test/systems/edac_system.jl index 3d8cf9babb..ed37b9ea55 100644 --- a/test/systems/edac_system.jl +++ b/test/systems/edac_system.jl @@ -32,7 +32,7 @@ @test system.mass == mass @test system.smoothing_kernel == smoothing_kernel @test TrixiParticles.initial_smoothing_length(system) == smoothing_length - @test system.transport_velocity isa Nothing + @test system.shifting_technique isa Nothing @test system.viscosity === nothing @test system.nu_edac == (0.5 * smoothing_length * sound_speed) / 8 @test system.acceleration == [0.0 for _ in 1:NDIMS] @@ -87,7 +87,7 @@ @test system.mass == setup.mass @test system.smoothing_kernel == smoothing_kernel @test TrixiParticles.initial_smoothing_length(system) == smoothing_length - @test system.transport_velocity isa Nothing + @test system.shifting_technique isa Nothing @test system.viscosity === nothing @test system.nu_edac == (0.5 * smoothing_length * sound_speed) / 8 @test system.acceleration == [0.0 for _ in 1:NDIMS] @@ -140,7 +140,7 @@ │ viscosity: …………………………………………………… Nothing │ │ ν₍EDAC₎: ………………………………………………………… ≈ 0.226 │ │ smoothing kernel: ………………………………… Val │ - │ tansport velocity formulation: Nothing │ + │ shifting technique: …………………………… nothing │ │ average pressure reduction: ……… no │ │ acceleration: …………………………………………… [0.0, 0.0] │ │ surface tension: …………………………………… nothing │ @@ -225,7 +225,7 @@ names = ["No TVF", "TransportVelocityAdami"] @testset "$(names[i])" for i in eachindex(transport_velocity) system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, - transport_velocity=transport_velocity[i], + shifting_technique=transport_velocity[i], average_pressure_reduction=true, smoothing_length, 0.0) semi = Semidiscretization(system) diff --git a/test/systems/wcsph_system.jl b/test/systems/wcsph_system.jl index cf6020d068..99f7152db6 100644 --- a/test/systems/wcsph_system.jl +++ b/test/systems/wcsph_system.jl @@ -199,7 +199,7 @@ smoothing_length, density_diffusion=density_diffusion) - show_compact = "WeaklyCompressibleSPHSystem{2}(SummationDensity(), nothing, Val{:state_equation}(), Val{:smoothing_kernel}(), nothing, Val{:density_diffusion}(), nothing, nothing, [0.0, 0.0], nothing) with 2 particles" + show_compact = "WeaklyCompressibleSPHSystem{2}(SummationDensity(), nothing, Val{:state_equation}(), Val{:smoothing_kernel}(), nothing, Val{:density_diffusion}(), nothing, nothing, nothing, [0.0, 0.0], nothing) with 2 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -211,8 +211,8 @@ │ state equation: ……………………………………… Val │ │ smoothing kernel: ………………………………… Val │ │ viscosity: …………………………………………………… nothing │ - │ tansport velocity formulation: Nothing │ │ density diffusion: ……………………………… Val{:density_diffusion}() │ + │ shifting technique: …………………………… nothing │ │ surface tension: …………………………………… nothing │ │ surface normal method: …………………… nothing │ │ acceleration: …………………………………………… [0.0, 0.0] │ From 821260dad7749dcf8430310787e0d88cbcebfeab Mon Sep 17 00:00:00 2001 From: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:15:11 +0200 Subject: [PATCH 03/15] Add `dvdu_ode` vector to `custom_quantities` (#879) * add dvu_ode vector to custom quantities * add for fluid and boundary * fix tests * fix tests * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Combine PST and TVF into a unified framework (#884) * Combine PST and TVF into a unified framework * Require update callback for PST * Fix WCSPH * Update PST only in callback * Fix EDAC * Update docs * Fix alle example files * Fix tests * Fix periodic channel * Fix docs * Update news * Fix tests * Fix example file * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Combine PST and TVF into a unified framework (#884) * Combine PST and TVF into a unified framework * Require update callback for PST * Fix WCSPH * Update PST only in callback * Fix EDAC * Update docs * Fix alle example files * Fix tests * Fix periodic channel * Fix docs * Update news * Fix tests * Fix example file * fix tests * fix doctests * implement suggestions * fix tests * fix tests * implement suggestions * fix * fix --------- Co-authored-by: LasNikas Co-authored-by: Sven Berger Co-authored-by: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> --- .../postprocessing/interpolation_plane.jl | 4 +- examples/postprocessing/postprocessing.jl | 4 +- src/TrixiParticles.jl | 3 +- src/callbacks/post_process.jl | 3 +- src/callbacks/solution_saving.jl | 3 +- src/callbacks/steady_state_reached.jl | 3 +- src/general/custom_quantities.jl | 34 ++++----- src/io/write_vtk.jl | 72 +++++++++++++------ src/schemes/boundary/open_boundary/system.jl | 2 +- src/schemes/boundary/system.jl | 11 +-- src/schemes/fluid/fluid.jl | 8 ++- .../solid/discrete_element_method/system.jl | 2 +- .../solid/total_lagrangian_sph/system.jl | 7 +- test/callbacks/solution_saving.jl | 18 +++-- test/general/custom_quantities.jl | 36 +++++----- test/io/read_vtk.jl | 15 ++-- .../dam_break_2d/validation_dam_break_2d.jl | 6 +- .../validation_lid_driven_cavity_2d.jl | 5 +- .../validation_taylor_green_vortex_2d.jl | 8 +-- 19 files changed, 150 insertions(+), 94 deletions(-) diff --git a/examples/postprocessing/interpolation_plane.jl b/examples/postprocessing/interpolation_plane.jl index 900c3daed1..edf4ebed8f 100644 --- a/examples/postprocessing/interpolation_plane.jl +++ b/examples/postprocessing/interpolation_plane.jl @@ -104,10 +104,10 @@ combined_plot = Plots.plot(plot1, plot2, plot3, plot_3d, layout=(2, 2), size=(1000, 1500), margin=3mm) # If we want to save planes at regular intervals, we can use the postprocessing callback. -# Note that the arguments `system, v_ode, u_ode, semi, t` are more powerful than the +# Note that the arguments `system, dv_ode, du_ode, v_ode, u_ode, semi, t` are more powerful than the # documented arguments `system, data, t`, allowing us to use interpolation (which requires # a semidiscretization). -function save_interpolated_plane(system, v_ode, u_ode, semi, t) +function save_interpolated_plane(system, dv_ode, du_ode, v_ode, u_ode, semi, t) # Size of the patch to be interpolated interpolation_start = [0.0, 0.0] interpolation_end = [tank_size[1], tank_size[2]] diff --git a/examples/postprocessing/postprocessing.jl b/examples/postprocessing/postprocessing.jl index 58a1e02cbc..bd0aa86115 100644 --- a/examples/postprocessing/postprocessing.jl +++ b/examples/postprocessing/postprocessing.jl @@ -13,9 +13,9 @@ using CSV using DataFrames using JSON -# Any custom function with the arguments `v, u, t, system` can be passed to the callback +# Any custom function with the arguments `system, data, t` can be passed to the callback # to be called every 10th timestep. See example below: -function hello(system, v_ode, u_ode, semi, t) +function hello(system, data, t) # Will write "hello" and the current simulation time println("hello at ", t) diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 2a53025d46..ed1b159ea6 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -23,7 +23,8 @@ using ReadVTK: ReadVTK using RecipesBase: RecipesBase, @series using Random: seed! using SciMLBase: CallbackSet, DiscreteCallback, DynamicalODEProblem, u_modified!, - get_tmp_cache, set_proposed_dt!, ODESolution, ODEProblem, terminate! + get_tmp_cache, set_proposed_dt!, ODESolution, ODEProblem, terminate!, + get_du @reexport using StaticArrays: SVector using StaticArrays: @SMatrix, SMatrix, setindex using StrideArrays: PtrArray, StaticInt diff --git a/src/callbacks/post_process.jl b/src/callbacks/post_process.jl index 36e16ce7f0..808b2eafc3 100644 --- a/src/callbacks/post_process.jl +++ b/src/callbacks/post_process.jl @@ -229,6 +229,7 @@ end # `affect!` function (pp::PostprocessCallback)(integrator) @trixi_timeit timer() "apply postprocess cb" begin + dv_ode, du_ode = get_du(integrator).x vu_ode = integrator.u v_ode, u_ode = vu_ode.x semi = integrator.p @@ -251,7 +252,7 @@ function (pp::PostprocessCallback)(integrator) system_index = system_indices(system, semi) for (key, f) in pp.func - result = custom_quantity(f, system, v_ode, u_ode, semi, t) + result = custom_quantity(f, system, dv_ode, du_ode, v_ode, u_ode, semi, t) if result !== nothing add_entry!(pp, string(key), t, result, filenames[system_index]) new_data = true diff --git a/src/callbacks/solution_saving.jl b/src/callbacks/solution_saving.jl index f9d8244f2f..39a0925210 100644 --- a/src/callbacks/solution_saving.jl +++ b/src/callbacks/solution_saving.jl @@ -154,6 +154,7 @@ function (solution_callback::SolutionSavingCallback)(integrator) (; interval, output_directory, custom_quantities, write_meta_data, git_hash, verbose, prefix, latest_saved_iter, max_coordinates) = solution_callback + dvdu_ode = get_du(integrator) vu_ode = integrator.u semi = integrator.p iter = get_iter(interval, integrator) @@ -173,7 +174,7 @@ function (solution_callback::SolutionSavingCallback)(integrator) println("Writing solution to $output_directory at t = $(integrator.t)") end - @trixi_timeit timer() "save solution" trixi2vtk(vu_ode, semi, integrator.t; + @trixi_timeit timer() "save solution" trixi2vtk(dvdu_ode, vu_ode, semi, integrator.t; iter, output_directory, prefix, write_meta_data, git_hash=git_hash[], max_coordinates, custom_quantities...) diff --git a/src/callbacks/steady_state_reached.jl b/src/callbacks/steady_state_reached.jl index 2ff7b5f9a3..2f30177b9a 100644 --- a/src/callbacks/steady_state_reached.jl +++ b/src/callbacks/steady_state_reached.jl @@ -131,11 +131,12 @@ end vu_ode = integrator.u v_ode, u_ode = vu_ode.x + dv_ode, du_ode = get_du(integrator).x semi = integrator.p # Calculate kinetic energy ekin = sum(semi.systems) do system - return kinetic_energy(system, v_ode, u_ode, semi, 0) + return kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, 0) end if length(previous_ekin) == interval_size diff --git a/src/general/custom_quantities.jl b/src/general/custom_quantities.jl index 58e40669c9..975e7a8394 100644 --- a/src/general/custom_quantities.jl +++ b/src/general/custom_quantities.jl @@ -3,7 +3,7 @@ Returns the total kinetic energy of all particles in a system. """ -function kinetic_energy(system, v_ode, u_ode, semi, t) +function kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) # TODO: `current_velocity` should only contain active particles @@ -17,18 +17,20 @@ function kinetic_energy(system, v_ode, u_ode, semi, t) end end -kinetic_energy(system::BoundarySystem, v_ode, u_ode, semi, t) = zero(eltype(system)) +function kinetic_energy(system::BoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t) + return zero(eltype(system)) +end """ total_mass Returns the total mass of all particles in a system. """ -function total_mass(system, v_ode, u_ode, semi, t) +function total_mass(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return sum(system.mass) end -function total_mass(system::BoundarySystem, v_ode, u_ode, semi, t) +function total_mass(system::BoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t) # It does not make sense to return a mass for boundary systems. # The material density and therefore the physical mass of the boundary is not relevant # when simulating a solid, stationary wall. The boundary always behaves as if it had @@ -47,12 +49,12 @@ end Returns the maximum pressure over all particles in a system. """ -function max_pressure(system::FluidSystem, v_ode, u_ode, semi, t) +function max_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return maximum(current_pressure(v, system)) end -function max_pressure(system, v_ode, u_ode, semi, t) +function max_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end @@ -61,12 +63,12 @@ end Returns the minimum pressure over all particles in a system. """ -function min_pressure(system::FluidSystem, v_ode, u_ode, semi, t) +function min_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return minimum(current_pressure(v, system)) end -function min_pressure(system, v_ode, u_ode, semi, t) +function min_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end @@ -75,13 +77,13 @@ end Returns the average pressure over all particles in a system. """ -function avg_pressure(system::FluidSystem, v_ode, u_ode, semi, t) +function avg_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) sum_ = sum(current_pressure(v, system)) return sum_ / nparticles(system) end -function avg_pressure(system, v_ode, u_ode, semi, t) +function avg_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end @@ -90,12 +92,12 @@ end Returns the maximum density over all particles in a system. """ -function max_density(system::FluidSystem, v_ode, u_ode, semi, t) +function max_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return maximum(current_density(v, system)) end -function max_density(system, v_ode, u_ode, semi, t) +function max_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end @@ -104,12 +106,12 @@ end Returns the minimum density over all particles in a system. """ -function min_density(system::FluidSystem, v_ode, u_ode, semi, t) +function min_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return minimum(current_density(v, system)) end -function min_density(system, v_ode, u_ode, semi, t) +function min_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end @@ -118,12 +120,12 @@ end Returns the average_density over all particles in a system. """ -function avg_density(system::FluidSystem, v_ode, u_ode, semi, t) +function avg_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) sum_ = sum(current_density(v, system)) return sum_ / nparticles(system) end -function avg_density(system, v_ode, u_ode, semi, t) +function avg_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return NaN end diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index 1aca732098..2d8b220f05 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -48,15 +48,27 @@ trixi2vtk(sol.u[end], semi, 0.0, iter=1, my_custom_quantity=kinetic_energy) ``` """ -function trixi2vtk(vu_ode, semi, t; iter=nothing, output_directory="out", prefix="", - write_meta_data=true, git_hash=compute_git_hash(), +function trixi2vtk(vu_ode, semi, t; iter=nothing, output_directory="out", + prefix="", write_meta_data=true, git_hash=compute_git_hash(), + max_coordinates=Inf, custom_quantities...) + + # The first argument is not necessary in most cases. Since it is usually not available to the user, + # this API wrapper makes it optional. + # Note that custom quantities using the fluid acceleration will not work and return NaN acceleration. + return trixi2vtk(fill!(similar(vu_ode), NaN), vu_ode, semi, t; iter, output_directory, + prefix, write_meta_data, git_hash, max_coordinates, + custom_quantities...) +end + +function trixi2vtk(dvdu_ode, vu_ode, semi, t; iter=nothing, output_directory="out", + prefix="", write_meta_data=true, git_hash=compute_git_hash(), max_coordinates=Inf, custom_quantities...) (; systems) = semi - v_ode, u_ode = vu_ode.x # Update quantities that are stored in the systems. These quantities (e.g. pressure) # still have the values from the last stage of the previous step if not updated here. @trixi_timeit timer() "update systems" begin + v_ode, u_ode = vu_ode.x # Don't create sub-timers here to avoid cluttering the timer output @notimeit timer() update_systems_and_nhs(v_ode, u_ode, semi, t) end @@ -67,17 +79,17 @@ function trixi2vtk(vu_ode, semi, t; iter=nothing, output_directory="out", prefix system_index = system_indices(system, semi) periodic_box = get_neighborhood_search(system, semi).periodic_box - trixi2vtk(system, v_ode, u_ode, semi, t, periodic_box; + trixi2vtk(system, dvdu_ode, vu_ode, semi, t, periodic_box; system_name=filenames[system_index], output_directory, iter, prefix, write_meta_data, git_hash, max_coordinates, custom_quantities...) end end # Convert data for a single TrixiParticle system to VTK format -function trixi2vtk(system_, v_ode_, u_ode_, semi_, t, periodic_box; output_directory="out", - prefix="", iter=nothing, system_name=vtkname(system_), - write_meta_data=true, max_coordinates=Inf, git_hash=compute_git_hash(), - custom_quantities...) +function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; + output_directory="out", prefix="", iter=nothing, + system_name=vtkname(system_), write_meta_data=true, max_coordinates=Inf, + git_hash=compute_git_hash(), custom_quantities...) mkpath(output_directory) # Skip empty systems @@ -85,6 +97,8 @@ function trixi2vtk(system_, v_ode_, u_ode_, semi_, t, periodic_box; output_direc return end + v_ode_, u_ode_ = vu_ode_.x + # Transfer to CPU if data is on the GPU. Do nothing if already on CPU. v_ode, u_ode, system, semi = transfer2cpu(v_ode_, u_ode_, system_, semi_) @@ -136,10 +150,16 @@ function trixi2vtk(system_, v_ode_, u_ode_, semi_, t, periodic_box; output_direc end # Extract custom quantities for this system - for (key, quantity) in custom_quantities - value = custom_quantity(quantity, system, v_ode, u_ode, semi, t) - if value !== nothing - vtk[string(key)] = value + if !isempty(custom_quantities) + dv_ode, du_ode = dvdu_ode_.x + dv_ode, du_ode = transfer2cpu(dv_ode, du_ode) + + for (key, quantity) in custom_quantities + value = custom_quantity(quantity, system, dv_ode, du_ode, v_ode, u_ode, + semi, t) + if value !== nothing + vtk[string(key)] = value + end end end @@ -150,12 +170,12 @@ function trixi2vtk(system_, v_ode_, u_ode_, semi_, t, periodic_box; output_direc end function transfer2cpu(v_::AbstractGPUArray, u_, system_, semi_) - v = Adapt.adapt(Array, v_) - u = Adapt.adapt(Array, u_) semi = Adapt.adapt(Array, semi_) system_index = system_indices(system_, semi_) system = semi.systems[system_index] + v, u = transfer2cpu(v_, u_) + return v, u, system, semi end @@ -163,20 +183,32 @@ function transfer2cpu(v_, u_, system_, semi_) return v_, u_, system_, semi_ end -function custom_quantity(quantity::AbstractArray, system, v_ode, u_ode, semi, t) +function transfer2cpu(v_::AbstractGPUArray, u_) + v = Adapt.adapt(Array, v_) + u = Adapt.adapt(Array, u_) + + return v, u +end + +function transfer2cpu(v_, u_) + return v_, u_ +end + +function custom_quantity(quantity::AbstractArray, system, dv_ode, du_ode, v_ode, u_ode, + semi, t) return quantity end -function custom_quantity(quantity, system, v_ode, u_ode, semi, t) +function custom_quantity(quantity, system, dv_ode, du_ode, v_ode, u_ode, semi, t) # Check if `quantity` is a function of `system`, `v_ode`, `u_ode`, `semi` and `t` if !isempty(methods(quantity, - (typeof(system), typeof(v_ode), typeof(u_ode), - typeof(semi), typeof(t)))) - return quantity(system, v_ode, u_ode, semi, t) + (typeof(system), typeof(dv_ode), typeof(du_ode), typeof(v_ode), + typeof(u_ode), typeof(semi), typeof(t)))) + return quantity(system, dv_ode, du_ode, v_ode, u_ode, semi, t) end # Assume `quantity` is a function of `data` - data = system_data(system, v_ode, u_ode, semi) + data = system_data(system, dv_ode, du_ode, v_ode, u_ode, semi) return quantity(system, data, t) end diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index bc21bc70d6..45eb0ec1e8 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -484,7 +484,7 @@ end # When the neighbor is an open boundary system, just use the viscosity of the fluid `system` instead @inline viscosity_model(system, neighbor_system::OpenBoundarySPHSystem) = system.viscosity -function system_data(system::OpenBoundarySPHSystem, v_ode, u_ode, semi) +function system_data(system::OpenBoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode, semi) v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) diff --git a/src/schemes/boundary/system.jl b/src/schemes/boundary/system.jl index 1e3a526551..e039aa8c71 100644 --- a/src/schemes/boundary/system.jl +++ b/src/schemes/boundary/system.jl @@ -453,23 +453,24 @@ function system_correction(system::BoundarySPHSystem{<:BoundaryModelDummyParticl return system.boundary_model.correction end -function system_data(system::BoundarySPHSystem, v_ode, u_ode, semi) +function system_data(system::BoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode, semi) + dv = [current_acceleration(system, particle) for particle in eachparticle(system)] v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) coordinates = current_coordinates(u, system) - velocity = current_velocity(v, system) + velocity = [current_velocity(v, system, particle) for particle in eachparticle(system)] density = current_density(v, system) pressure = current_pressure(v, system) - return (; coordinates, velocity, density, pressure) + return (; coordinates, velocity, density, pressure, acceleration=dv) end function available_data(::BoundarySPHSystem) - return (:coordinates, :velocity, :density, :pressure) + return (:coordinates, :velocity, :density, :pressure, :acceleration) end -function system_data(system::BoundaryDEMSystem, v_ode, u_ode, semi) +function system_data(system::BoundaryDEMSystem, dv_ode, du_ode, v_ode, u_ode, semi) (; coordinates, radius, normal_stiffness) = system return (; coordinates, radius, normal_stiffness) diff --git a/src/schemes/fluid/fluid.jl b/src/schemes/fluid/fluid.jl index 59703f9b6d..de9731fe82 100644 --- a/src/schemes/fluid/fluid.jl +++ b/src/schemes/fluid/fluid.jl @@ -173,22 +173,24 @@ end return nothing end -function system_data(system::FluidSystem, v_ode, u_ode, semi) +function system_data(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi) (; mass) = system + dv = wrap_v(dv_ode, system, semi) v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) coordinates = current_coordinates(u, system) velocity = current_velocity(v, system) + acceleration = current_velocity(dv, system) density = current_density(v, system) pressure = current_pressure(v, system) - return (; coordinates, velocity, mass, density, pressure) + return (; coordinates, velocity, mass, density, pressure, acceleration) end function available_data(::FluidSystem) - return (:coordinates, :velocity, :mass, :density, :pressure) + return (:coordinates, :velocity, :mass, :density, :pressure, :acceleration) end include("pressure_acceleration.jl") diff --git a/src/schemes/solid/discrete_element_method/system.jl b/src/schemes/solid/discrete_element_method/system.jl index 1b65c7e264..bc93aa8c2f 100644 --- a/src/schemes/solid/discrete_element_method/system.jl +++ b/src/schemes/solid/discrete_element_method/system.jl @@ -150,7 +150,7 @@ end return system.radius[particle] end -function system_data(system::DEMSystem, v_ode, u_ode, semi) +function system_data(system::DEMSystem, dv_ode, du_ode, v_ode, u_ode, semi) (; mass, radius, damping_coefficient) = system v = wrap_v(v_ode, system, semi) diff --git a/src/schemes/solid/total_lagrangian_sph/system.jl b/src/schemes/solid/total_lagrangian_sph/system.jl index f843fdaf33..c0ef99b30a 100644 --- a/src/schemes/solid/total_lagrangian_sph/system.jl +++ b/src/schemes/solid/total_lagrangian_sph/system.jl @@ -491,10 +491,11 @@ end return neighbor_system.boundary_model.viscosity end -function system_data(system::TotalLagrangianSPHSystem, v_ode, u_ode, semi) +function system_data(system::TotalLagrangianSPHSystem, dv_ode, du_ode, v_ode, u_ode, semi) (; mass, material_density, deformation_grad, pk1_corrected, young_modulus, poisson_ratio, lame_lambda, lame_mu) = system + dv = wrap_v(dv_ode, system, semi) v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) @@ -504,11 +505,11 @@ function system_data(system::TotalLagrangianSPHSystem, v_ode, u_ode, semi) return (; coordinates, initial_coordinates=initial_coordinates_, velocity, mass, material_density, deformation_grad, pk1_corrected, young_modulus, poisson_ratio, - lame_lambda, lame_mu) + lame_lambda, lame_mu, acceleration=current_velocity(dv, system)) end function available_data(::TotalLagrangianSPHSystem) return (:coordinates, :initial_coordinates, :velocity, :mass, :material_density, :deformation_grad, :pk1_corrected, :young_modulus, :poisson_ratio, - :lame_lambda, :lame_mu) + :lame_lambda, :lame_mu, :acceleration) end diff --git a/test/callbacks/solution_saving.jl b/test/callbacks/solution_saving.jl index 3b29d2df4a..b63cb6b201 100644 --- a/test/callbacks/solution_saving.jl +++ b/test/callbacks/solution_saving.jl @@ -69,17 +69,21 @@ @testset verbose=true "custom quantities" begin # Test that `custom_quantity` correctly chooses the correct method quantity1(system, data, t) = data - quantity2(system, v_ode, u_ode, semi, t) = 2 + quantity2(system, dv_ode, du_ode, v_ode, u_ode, semi, t) = 2 quantity3() = 3 system = Val(:mock_system) - TrixiParticles.system_data(::Val{:mock_system}, v_ode, u_ode, semi) = 1 + TrixiParticles.system_data(::Val{:mock_system}, dv_ode, du_ode, v_ode, u_ode, + semi) = 1 - data = v_ode = u_ode = semi = t = nothing + data = v_ode = u_ode = dv_ode = du_ode = semi = t = nothing - @test TrixiParticles.custom_quantity(quantity1, system, v_ode, u_ode, semi, t) == 1 - @test TrixiParticles.custom_quantity(quantity2, system, v_ode, u_ode, semi, t) == 2 - @test_throws MethodError TrixiParticles.custom_quantity(quantity3, system, v_ode, - u_ode, semi, t) + @test TrixiParticles.custom_quantity(quantity1, system, dv_ode, du_ode, v_ode, + u_ode, semi, t) == 1 + @test TrixiParticles.custom_quantity(quantity2, system, dv_ode, du_ode, v_ode, + u_ode, semi, t) == 2 + @test_throws MethodError TrixiParticles.custom_quantity(quantity3, system, dv_ode, + du_ode, v_ode, u_ode, + semi, t) end end diff --git a/test/general/custom_quantities.jl b/test/general/custom_quantities.jl index 37cb59ea92..f75e5db25d 100644 --- a/test/general/custom_quantities.jl +++ b/test/general/custom_quantities.jl @@ -26,11 +26,12 @@ semi = Semidiscretization(fluid_system, boundary_system) v_ode, u_ode = semidiscretize(semi, (0, 1)).u0.x + dv_ode, du_ode = similar(v_ode) t = 0.0 @testset "Kinetic Energy" begin @testset "Fluid System" begin - ekin = kinetic_energy(fluid_system, v_ode, u_ode, semi, t) + ekin = kinetic_energy(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) expected_ekin = sum(velocities) do velocity return 0.5 * first(fluid_system.mass) * dot(velocity, velocity) @@ -40,21 +41,21 @@ end @testset "Boundary System" begin - ekin = kinetic_energy(boundary_system, v_ode, u_ode, semi, t) + ekin = kinetic_energy(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test ekin == 0 end end @testset "Total Mass" begin @testset "Fluid System" begin - mass = total_mass(fluid_system, v_ode, u_ode, semi, t) + mass = total_mass(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) expected_mass = first(fluid_system.mass) * nparticles(fluid_system) @test isapprox(mass, expected_mass) end @testset "Boundary System" begin - mass = total_mass(boundary_system, v_ode, u_ode, semi, t) + mass = total_mass(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test isnan(mass) end @@ -62,54 +63,57 @@ @testset "Pressure Quantities" begin @testset "Max Pressure" begin - max_p = max_pressure(fluid_system, v_ode, u_ode, semi, t) + max_p = max_pressure(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test isapprox(max_p, 101330.0) # Boundary system should return NaN - @test isnan(max_pressure(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(max_pressure(boundary_system, dv_ode, du_ode, v_ode, u_ode, + semi, t)) end @testset "Min Pressure" begin - min_p = min_pressure(fluid_system, v_ode, u_ode, semi, t) + min_p = min_pressure(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test min_p ≈ 101320.0 # Boundary system should return NaN - @test isnan(min_pressure(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(min_pressure(boundary_system, dv_ode, du_ode, v_ode, u_ode, + semi, t)) end @testset "Average Pressure" begin - avg_p = avg_pressure(fluid_system, v_ode, u_ode, semi, t) + avg_p = avg_pressure(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) expected_avg = (101325.0 + 101330.0 + 101320.0) / 3 @test isapprox(avg_p, expected_avg) # Boundary system should return NaN - @test isnan(avg_pressure(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(avg_pressure(boundary_system, dv_ode, du_ode, v_ode, u_ode, + semi, t)) end end @testset "Density Quantities" begin @testset "max_density" begin - max_d = max_density(fluid_system, v_ode, u_ode, semi, t) + max_d = max_density(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test isapprox(max_d, 1000.0) # All particles have same density # Boundary system should return NaN - @test isnan(max_density(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(max_density(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t)) end @testset "min_density" begin - min_d = min_density(fluid_system, v_ode, u_ode, semi, t) + min_d = min_density(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test isapprox(min_d, 1000.0) # Boundary system should return NaN - @test isnan(min_density(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(min_density(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t)) end @testset "avg_density" begin - avg_d = avg_density(fluid_system, v_ode, u_ode, semi, t) + avg_d = avg_density(fluid_system, dv_ode, du_ode, v_ode, u_ode, semi, t) @test isapprox(avg_d, 1000.0) # Boundary system should return NaN - @test isnan(avg_density(boundary_system, v_ode, u_ode, semi, t)) + @test isnan(avg_density(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t)) end end end diff --git a/test/io/read_vtk.jl b/test/io/read_vtk.jl index 5db6406c7e..8dfb626737 100644 --- a/test/io/read_vtk.jl +++ b/test/io/read_vtk.jl @@ -1,7 +1,7 @@ @testset verbose=true "`vtk2trixi`" begin mktempdir() do tmp_dir - coordinates=fill(1.0, 2, 12) - velocity=fill(2.0, 2, 12) + coordinates = fill(1.0, 2, 12) + velocity = fill(2.0, 2, 12) expected_ic = InitialCondition(; coordinates=coordinates, velocity=velocity, density=1000.0, pressure=900.0, mass=50.0) @@ -29,15 +29,17 @@ semi = Semidiscretization(fluid_system) # Create random ODE solutions + dvdu_ode = nothing v = fill(2.0, ndims(fluid_system), nparticles(fluid_system)) pressure = fill(3.0, nparticles(fluid_system)) v_ode = vec([v; pressure']) - u = fill(1.0, ndims(fluid_system), nparticles(fluid_system)) u_ode = vec(u) + x = (; v_ode, u_ode) + vu_ode = (; x) # Write out `FluidSystem` Simulation-File - trixi2vtk(fluid_system, v_ode, u_ode, semi, 0.0, + trixi2vtk(fluid_system, dvdu_ode, vu_ode, semi, 0.0, nothing; system_name="tmp_file_fluid", output_directory=tmp_dir, iter=1) @@ -65,11 +67,14 @@ semi = Semidiscretization(boundary_system) # Create dummy ODE solutions + dvdu_ode = nothing v_ode = zeros(ndims(boundary_system) * nparticles(boundary_system)) u_ode = zeros(ndims(boundary_system) * nparticles(boundary_system)) + x = (; v_ode, u_ode) + vu_ode = (; x) # Write out `BoundarySystem` Simulation-File - trixi2vtk(boundary_system, v_ode, u_ode, semi, 0.0, + trixi2vtk(boundary_system, dvdu_ode, vu_ode, semi, 0.0, nothing; system_name="tmp_file_boundary", output_directory=tmp_dir, iter=1) diff --git a/validation/dam_break_2d/validation_dam_break_2d.jl b/validation/dam_break_2d/validation_dam_break_2d.jl index aee5f72e0d..3f9b008c3b 100644 --- a/validation/dam_break_2d/validation_dam_break_2d.jl +++ b/validation/dam_break_2d/validation_dam_break_2d.jl @@ -49,15 +49,15 @@ sensor_names = ["P1", "P2", "P3"] tank_right_wall_x = floor(5.366 * H / particle_spacing) * particle_spacing - 0.5 * particle_spacing -pressure_P1 = (system, v_ode, u_ode, semi, +pressure_P1 = (system, dv_ode, du_ode, v_ode, u_ode, semi, t) -> interpolated_pressure([tank_right_wall_x, P1_y_top], [tank_right_wall_x, P1_y_bottom], v_ode, u_ode, t, system, semi) -pressure_P2 = (system, v_ode, u_ode, semi, +pressure_P2 = (system, dv_ode, du_ode, v_ode, u_ode, semi, t) -> interpolated_pressure([tank_right_wall_x, P2_y_top], [tank_right_wall_x, P2_y_bottom], v_ode, u_ode, t, system, semi) -pressure_P3 = (system, v_ode, u_ode, semi, +pressure_P3 = (system, dv_ode, du_ode, v_ode, u_ode, semi, t) -> interpolated_pressure([tank_right_wall_x, P3_y_top], [tank_right_wall_x, P3_y_bottom], v_ode, u_ode, t, system, semi) diff --git a/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl b/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl index c75d1500e3..6fc7278fe4 100644 --- a/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl +++ b/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl @@ -13,9 +13,10 @@ reynolds_numbers = [100.0, 1000.0, 10_000.0] const SENSOR_CAPTURE_TIME = 24.8 const CAPTURE_STARTED = Ref(false) -interpolated_velocity(system, v, u, semi, t) = nothing +interpolated_velocity(system, dv_ode, du_ode, v, u, semi, t) = nothing -function interpolated_velocity(system::TrixiParticles.FluidSystem, v, u, semi, t) +function interpolated_velocity(system::TrixiParticles.FluidSystem, dv_ode, du_ode, v, u, + semi, t) if t < SENSOR_CAPTURE_TIME return nothing end diff --git a/validation/taylor_green_vortex_2d/validation_taylor_green_vortex_2d.jl b/validation/taylor_green_vortex_2d/validation_taylor_green_vortex_2d.jl index 849ddd38b6..20292524da 100644 --- a/validation/taylor_green_vortex_2d/validation_taylor_green_vortex_2d.jl +++ b/validation/taylor_green_vortex_2d/validation_taylor_green_vortex_2d.jl @@ -23,7 +23,7 @@ perturb_coordinates = [false, true] return zero(eltype(system)) end -function compute_l1v_error(system, v_ode, u_ode, semi, t) +function compute_l1v_error(system, dv_ode, du_ode, v_ode, u_ode, semi, t) v_analytical_avg = 0.0 v_avg = 0.0 @@ -46,7 +46,7 @@ function compute_l1v_error(system, v_ode, u_ode, semi, t) return v_avg /= v_analytical_avg end -function compute_l1p_error(system, v_ode, u_ode, semi, t) +function compute_l1p_error(system, dv_ode, du_ode, v_ode, u_ode, semi, t) p_max_exact = 0.0 L1p = 0.0 @@ -74,8 +74,8 @@ end # The pressure plotted in the paper is the difference of the local pressure minus # the average of the pressure of all particles. -function diff_p_loc_p_avg(system, v, u, semi, t) - p_avg_tot = avg_pressure(system, v, u, semi, t) +function diff_p_loc_p_avg(system, dv_ode, du_ode, v, u, semi, t) + p_avg_tot = avg_pressure(system, dv_ode, du_ode, v, u, semi, t) return v[end, :] .- p_avg_tot end From ceee740cc6877ece5c676a0da2fa668efcfab193 Mon Sep 17 00:00:00 2001 From: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:05:39 +0200 Subject: [PATCH 04/15] Revise open boundaries (#866) * swap extrapolation * swap density pressure * adapt docs * rm example * fix missing nhs update * implement different mirror methods * add docs * fix tests * fix typos * add docs * add tests * apply formatter * add `average_velocity!` for `BoundaryModelLastiwka` * fix * add interpolation functions * restructure again * fix gpu bugs * remove unnecessary argument * fix gpu again * fix gpu * implement suggestions * fix * add comments * implement suggestions * first prototype: NOT VALIDATED YET * fix avg vel in LastiwkaModel * fix avg in mirroring * fix tests * adapt example file * fix allocations * fix gpu * first GPU working prototype * improve API * fix tests * rm export * fix * fix type stability * implement suggestions * rm ref * format * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * fix merge bugs * supersede #852 * supersede #850 * fix * rename boundary models * fix? * fix tests * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * fix characteristics * revise pipe flow example * fix include bug * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Combine PST and TVF into a unified framework (#884) * Combine PST and TVF into a unified framework * Require update callback for PST * Fix WCSPH * Update PST only in callback * Fix EDAC * Update docs * Fix alle example files * Fix tests * Fix periodic channel * Fix docs * Update news * Fix tests * Fix example file * add doc strings * implement suggestions * remove duplicated system * rm unnecessary variable * apply formatter * set pressure for WCSPH * setter for open boundary * rename reference values * fix gpu tests * fix sign * implement suggestions * fix unit tests * revise doc strings * add NEWS entry * update NEWS --------- Co-authored-by: LasNikas Co-authored-by: Sven Berger Co-authored-by: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> --- NEWS.md | 3 + examples/fluid/pipe_flow_2d.jl | 128 +++---- examples/fluid/pipe_flow_3d.jl | 26 +- src/TrixiParticles.jl | 3 +- src/general/buffer.jl | 4 + src/general/semidiscretization.jl | 16 +- src/general/system.jl | 11 +- src/io/write_vtk.jl | 30 +- src/preprocessing/particle_packing/system.jl | 2 +- .../boundary/open_boundary/boundary_zones.jl | 323 +++++++++++++++--- .../method_of_characteristics.jl | 200 ++++++----- .../boundary/open_boundary/mirroring.jl | 127 +++---- src/schemes/boundary/open_boundary/system.jl | 319 ++++++----------- .../fluid/entropically_damped_sph/system.jl | 2 + .../fluid/weakly_compressible_sph/system.jl | 2 + .../solid/discrete_element_method/system.jl | 3 +- .../solid/total_lagrangian_sph/system.jl | 3 +- src/util.jl | 11 + test/examples/examples_fluid.jl | 41 +-- test/examples/gpu.jl | 22 +- test/general/buffer.jl | 12 +- test/general/semidiscretization.jl | 2 +- .../boundary/open_boundary/boundary_zone.jl | 102 +++++- .../open_boundary/characteristic_variables.jl | 24 +- .../boundary/open_boundary/mirroring.jl | 35 +- test/systems/open_boundary_system.jl | 102 ++---- 26 files changed, 862 insertions(+), 691 deletions(-) diff --git a/NEWS.md b/NEWS.md index d4e2ccdd7d..e88701f1a6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,9 @@ used in the Julia ecosystem. Notable changes will be documented in this file for ### API Changes +- API for `OpenBoundarySPHSystem` and `BoundaryZone` changed. + It is now possible to pass multiple `BoundaryZone`s to a single `OpenBoundarySPHSystem`. + Reference values are now assigned individually to each `BoundaryZone`. (#866) - Combined transport velocity formulation (TVF) and particle shifting technique (PST) into one unified framework. The keyword argument `transport_velocity` now changed to `shifting_technique`. diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl index 85e3461ccc..c206f76484 100644 --- a/examples/fluid/pipe_flow_2d.jl +++ b/examples/fluid/pipe_flow_2d.jl @@ -12,7 +12,7 @@ using OrdinaryDiffEq # ========================================================================================== # ==== Resolution -particle_spacing = 0.05 +particle_spacing = 0.02 # Make sure that the kernel support of fluid particles at a boundary is always fully sampled boundary_layers = 4 @@ -21,7 +21,7 @@ boundary_layers = 4 # fully sampled. # Note: Due to the dynamics at the inlets and outlets of open boundaries, # it is recommended to use `open_boundary_layers > boundary_layers` -open_boundary_layers = 8 +open_boundary_layers = 6 # ========================================================================================== # ==== Experiment Setup @@ -30,65 +30,71 @@ tspan = (0.0, 2.0) # Boundary geometry and initial fluid particle positions domain_size = (1.0, 0.4) -flow_direction = [1.0, 0.0] reynolds_number = 100 -const prescribed_velocity = 2.0 +const prescribed_velocity = (1.0, 0.0) +flow_direction = [1.0, 0.0] -boundary_size = (domain_size[1] + 2 * particle_spacing * open_boundary_layers, - domain_size[2]) +open_boundary_size = (particle_spacing * open_boundary_layers, domain_size[2]) fluid_density = 1000.0 -# For this particular example, it is necessary to have a background pressure. -# Otherwise the suction at the outflow is to big and the simulation becomes unstable. -pressure = 1000.0 - -sound_speed = 20 * prescribed_velocity - -state_equation = nothing +sound_speed = 10 * maximum(abs.(prescribed_velocity)) -pipe = RectangularTank(particle_spacing, domain_size, boundary_size, fluid_density, - pressure=pressure, n_layers=boundary_layers, +pipe = RectangularTank(particle_spacing, domain_size, domain_size, fluid_density, + n_layers=boundary_layers, velocity=prescribed_velocity, faces=(false, false, true, true)) -# Shift pipe walls in negative x-direction for the inflow -pipe.boundary.coordinates[1, :] .-= particle_spacing * open_boundary_layers +min_coords_inlet = (-open_boundary_layers * particle_spacing, 0.0) +inlet = RectangularTank(particle_spacing, open_boundary_size, open_boundary_size, + fluid_density, n_layers=boundary_layers, + min_coordinates=min_coords_inlet, + faces=(false, false, true, true)) + +min_coords_outlet = (pipe.fluid_size[1], 0.0) +outlet = RectangularTank(particle_spacing, open_boundary_size, open_boundary_size, + fluid_density, n_layers=boundary_layers, + min_coordinates=min_coords_outlet, + faces=(false, false, true, true)) NDIMS = ndims(pipe.fluid) -n_buffer_particles = 5 * pipe.n_particles_per_dimension[2]^(NDIMS - 1) +n_buffer_particles = 10 * pipe.n_particles_per_dimension[2]^(NDIMS - 1) # ========================================================================================== # ==== Fluid -wcsph = false +wcsph = true smoothing_length = 1.5 * particle_spacing smoothing_kernel = WendlandC2Kernel{NDIMS}() fluid_density_calculator = ContinuityDensity() -kinematic_viscosity = prescribed_velocity * domain_size[2] / reynolds_number +kinematic_viscosity = maximum(prescribed_velocity) * domain_size[2] / reynolds_number viscosity = ViscosityAdami(nu=kinematic_viscosity) -fluid_system = EntropicallyDampedSPHSystem(pipe.fluid, smoothing_kernel, smoothing_length, - sound_speed, viscosity=viscosity, - density_calculator=fluid_density_calculator, - shifting_technique=ParticleShiftingTechnique(), - buffer_size=n_buffer_particles) - # Alternatively the WCSPH scheme can be used if wcsph state_equation = StateEquationCole(; sound_speed, reference_density=fluid_density, - exponent=1, background_pressure=pressure) - alpha = 8 * kinematic_viscosity / (smoothing_length * sound_speed) - viscosity = ArtificialViscosityMonaghan(; alpha, beta=0.0) + exponent=1) + density_diffusion = DensityDiffusionMolteniColagrossi(delta=0.1) fluid_system = WeaklyCompressibleSPHSystem(pipe.fluid, fluid_density_calculator, state_equation, smoothing_kernel, + density_diffusion=density_diffusion, smoothing_length, viscosity=viscosity, shifting_technique=ParticleShiftingTechnique(), buffer_size=n_buffer_particles) +else + # Alternatively the EDAC scheme can be used + state_equation = nothing + + fluid_system = EntropicallyDampedSPHSystem(pipe.fluid, smoothing_kernel, + smoothing_length, sound_speed, + viscosity=viscosity, + density_calculator=fluid_density_calculator, + shifting_technique=ParticleShiftingTechnique(), + buffer_size=n_buffer_particles) end # ========================================================================================== @@ -98,63 +104,61 @@ function velocity_function2d(pos, t) # Use this for a time-dependent inflow velocity # return SVector(0.5prescribed_velocity * sin(2pi * t) + prescribed_velocity, 0) - return SVector(prescribed_velocity, 0.0) + return SVector(prescribed_velocity) end -open_boundary_model = BoundaryModelLastiwka() +open_boundary_model = BoundaryModelMirroringTafuni(; mirror_method=ZerothOrderMirroring()) +reference_velocity_in = velocity_function2d +reference_pressure_in = nothing +reference_density_in = nothing boundary_type_in = InFlow() plane_in = ([0.0, 0.0], [0.0, domain_size[2]]) inflow = BoundaryZone(; plane=plane_in, plane_normal=flow_direction, open_boundary_layers, density=fluid_density, particle_spacing, - boundary_type=boundary_type_in) - -reference_velocity_in = velocity_function2d -reference_pressure_in = pressure -reference_density_in = fluid_density -open_boundary_in = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=open_boundary_model, - buffer_size=n_buffer_particles, - reference_density=reference_density_in, - reference_pressure=reference_pressure_in, - reference_velocity=reference_velocity_in) - + reference_density=reference_density_in, + reference_pressure=reference_pressure_in, + reference_velocity=reference_velocity_in, + initial_condition=inlet.fluid, boundary_type=boundary_type_in) + +reference_velocity_out = nothing +reference_pressure_out = nothing +reference_density_out = nothing boundary_type_out = OutFlow() -plane_out = ([domain_size[1], 0.0], [domain_size[1], domain_size[2]]) +plane_out = ([min_coords_outlet[1], 0.0], [min_coords_outlet[1], domain_size[2]]) outflow = BoundaryZone(; plane=plane_out, plane_normal=(-flow_direction), open_boundary_layers, density=fluid_density, particle_spacing, - boundary_type=boundary_type_out) - -reference_velocity_out = velocity_function2d -reference_pressure_out = pressure -reference_density_out = fluid_density -open_boundary_out = OpenBoundarySPHSystem(outflow; fluid_system, - boundary_model=open_boundary_model, - buffer_size=n_buffer_particles, - reference_density=reference_density_out, - reference_pressure=reference_pressure_out, - reference_velocity=reference_velocity_out) + reference_density=reference_density_out, + reference_pressure=reference_pressure_out, + reference_velocity=reference_velocity_out, + initial_condition=outlet.fluid, boundary_type=boundary_type_out) + +open_boundary = OpenBoundarySPHSystem(inflow, outflow; fluid_system, + boundary_model=open_boundary_model, + buffer_size=n_buffer_particles) + # ========================================================================================== # ==== Boundary -viscosity_boundary = ViscosityAdami(nu=1e-4) -boundary_model = BoundaryModelDummyParticles(pipe.boundary.density, pipe.boundary.mass, +wall = union(pipe.boundary, inlet.boundary, outlet.boundary) +viscosity_boundary = viscosity +boundary_model = BoundaryModelDummyParticles(wall.density, wall.mass, AdamiPressureExtrapolation(), state_equation=state_equation, viscosity=viscosity_boundary, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(pipe.boundary, boundary_model) +boundary_system = BoundarySPHSystem(wall, boundary_model) # ========================================================================================== # ==== Simulation -min_corner = minimum(pipe.boundary.coordinates .- particle_spacing, dims=2) -max_corner = maximum(pipe.boundary.coordinates .+ particle_spacing, dims=2) +min_corner = minimum(wall.coordinates .- particle_spacing, dims=2) +max_corner = maximum(wall.coordinates .+ particle_spacing, dims=2) nhs = GridNeighborhoodSearch{NDIMS}(; cell_list=FullGridCellList(; min_corner, max_corner), update_strategy=ParallelUpdate()) -semi = Semidiscretization(fluid_system, open_boundary_in, open_boundary_out, - boundary_system, neighborhood_search=nhs, +semi = Semidiscretization(fluid_system, open_boundary, boundary_system, + neighborhood_search=nhs, parallelization_backend=PolyesterBackend()) ode = semidiscretize(semi, tspan) diff --git a/examples/fluid/pipe_flow_3d.jl b/examples/fluid/pipe_flow_3d.jl index 10035f4308..595859affd 100644 --- a/examples/fluid/pipe_flow_3d.jl +++ b/examples/fluid/pipe_flow_3d.jl @@ -22,28 +22,24 @@ open_boundary_layers = 6 # ========================================================================================== # ==== Experiment Setup -tspan = (0.0, 2.0) - -function velocity_function3d(pos, t) - # Use this for a time-dependent inflow velocity - # return SVector(0.5prescribed_velocity * sin(2pi * t) + prescribed_velocity, 0) - - return SVector(prescribed_velocity, 0.0, 0.0) -end +tspan = (0.0, 0.5) domain_size = (1.0, 0.4, 0.4) - -boundary_size = (domain_size[1] + 2 * particle_spacing * open_boundary_layers, - domain_size[2], domain_size[3]) - +const prescribed_velocity = (1.0, 0.0, 0.0) flow_direction = [1.0, 0.0, 0.0] +open_boundary_size = (domain_size[1] + 2 * particle_spacing * open_boundary_layers, + domain_size[2], domain_size[3]) +min_coords_inlet = (-open_boundary_layers * particle_spacing, 0.0, 0.0) +min_coords_outlet = (-open_boundary_layers * particle_spacing, 0.0, 0.0) + # setup simulation trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), - domain_size=domain_size, boundary_size=boundary_size, + domain_size=domain_size, open_boundary_size=open_boundary_size, flow_direction=flow_direction, faces=(false, false, true, true, true, true), - tspan=tspan, reference_velocity=velocity_function3d, - open_boundary_layers=open_boundary_layers, + tspan=tspan, prescribed_velocity=prescribed_velocity, + open_boundary_layers=open_boundary_layers, min_coords_inlet=min_coords_inlet, + min_coords_outlet=min_coords_outlet, plane_in=([0.0, 0.0, 0.0], [0.0, domain_size[2], 0.0], [0.0, 0.0, domain_size[3]]), plane_out=([domain_size[1], 0.0, 0.0], [domain_size[1], domain_size[2], 0.0], diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index b15c8873aa..0aff00a73a 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -79,7 +79,8 @@ export DensityDiffusion, DensityDiffusionMolteniColagrossi, DensityDiffusionFerr DensityDiffusionAntuono export tensile_instability_control export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureExtrapolation, - PressureMirroring, PressureZeroing, BoundaryModelLastiwka, BoundaryModelTafuni, + PressureMirroring, PressureZeroing, BoundaryModelCharacteristicsLastiwka, + BoundaryModelMirroringTafuni, BernoulliPressureExtrapolation export FirstOrderMirroring, ZerothOrderMirroring, SimpleMirroring export HertzContactModel, LinearContactModel diff --git a/src/general/buffer.jl b/src/general/buffer.jl index 3e1c28e8ca..214d9765bc 100644 --- a/src/general/buffer.jl +++ b/src/general/buffer.jl @@ -36,6 +36,10 @@ function allocate_buffer(initial_condition, buffer::SystemBuffer) return union(initial_condition, buffer_ic) end +# By default, there is no buffer. +# Dispatch by system type to handle systems that provide a buffer. +@inline buffer(system) = nothing + @inline update_system_buffer!(buffer::Nothing, semi) = buffer # TODO `resize` allocates. Find a non-allocating version diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl index 9532d812ab..f6cc8015c9 100644 --- a/src/general/semidiscretization.jl +++ b/src/general/semidiscretization.jl @@ -972,17 +972,17 @@ end function check_configuration(system::OpenBoundarySPHSystem, systems, neighborhood_search::PointNeighbors.AbstractNeighborhoodSearch) - (; boundary_model, boundary_zone) = system + (; boundary_model, boundary_zones) = system # Store index of the fluid system. This is necessary for re-linking # in case we use Adapt.jl to create a new semidiscretization. fluid_system_index = findfirst(==(system.fluid_system), systems) system.fluid_system_index[] = fluid_system_index - if boundary_model isa BoundaryModelLastiwka && - boundary_zone isa BoundaryZone{BidirectionalFlow} - throw(ArgumentError("`BoundaryModelLastiwka` needs a specific flow direction. " * - "Please specify inflow and outflow.")) + if boundary_model isa BoundaryModelCharacteristicsLastiwka && + any(zone -> isnothing(zone.flow_direction), boundary_zones) + throw(ArgumentError("`BoundaryModelCharacteristicsLastiwka` needs a specific flow direction. " * + "Please specify `InFlow()` and `OutFlow()`.")) end if first(PointNeighbors.requires_update(neighborhood_search)) @@ -1011,10 +1011,8 @@ function set_system_links(system::OpenBoundarySPHSystem, semi) system.pressure, system.boundary_candidates, system.fluid_candidates, - system.boundary_zone, - system.reference_velocity, - system.reference_pressure, - system.reference_density, + system.boundary_zone_indices, + system.boundary_zones, system.buffer, system.cache) end diff --git a/src/general/system.jl b/src/general/system.jl index 5b4d5aecb8..6c4aed9b0f 100644 --- a/src/general/system.jl +++ b/src/general/system.jl @@ -37,17 +37,18 @@ initialize!(system, semi) = system # Number of particles in the system whose positions are to be integrated (corresponds to the size of u and du) @inline n_moving_particles(system) = nparticles(system) -@inline eachparticle(system) = Base.OneTo(nparticles(system)) +@inline eachparticle(system::System) = active_particles(system) +@inline eachparticle(initial_condition) = Base.OneTo(nparticles(initial_condition)) # Wrapper for systems with `SystemBuffer` -@inline each_moving_particle(system) = each_moving_particle(system, system.buffer) +@inline each_moving_particle(system) = each_moving_particle(system, buffer(system)) @inline each_moving_particle(system, ::Nothing) = Base.OneTo(n_moving_particles(system)) -@inline active_coordinates(u, system) = active_coordinates(u, system, system.buffer) +@inline active_coordinates(u, system) = active_coordinates(u, system, buffer(system)) @inline active_coordinates(u, system, ::Nothing) = current_coordinates(u, system) -@inline active_particles(system) = active_particles(system, system.buffer) -@inline active_particles(system, ::Nothing) = eachparticle(system) +@inline active_particles(system) = active_particles(system, buffer(system)) +@inline active_particles(system, ::Nothing) = Base.OneTo(nparticles(system)) # This should not be dispatched by system type. We always expect to get a column of `A`. @propagate_inbounds function extract_svector(A, system, i) diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index 2d8b220f05..c17410d698 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -137,12 +137,12 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; write2vtk!(vtk, v, u, t, system, write_meta_data=write_meta_data) # Store particle index - vtk["index"] = active_particles(system) + vtk["index"] = eachparticle(system) vtk["time"] = t vtk["ndims"] = ndims(system) vtk["particle_spacing"] = [particle_spacing(system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] if write_meta_data vtk["solver_version"] = git_hash @@ -294,20 +294,20 @@ end function write2vtk!(vtk, v, u, t, system::DEMSystem; write_meta_data=true) vtk["velocity"] = view(v, 1:ndims(system), :) vtk["mass"] = [hydrodynamic_mass(system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] vtk["radius"] = [particle_radius(system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] return vtk end function write2vtk!(vtk, v, u, t, system::FluidSystem; write_meta_data=true) vtk["velocity"] = [current_velocity(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] # Indexing the pressure is a workaround for slicing issue (see https://github.com/JuliaSIMD/StrideArrays.jl/issues/88) vtk["pressure"] = [current_pressure(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] if system.surface_normal_method !== nothing vtk["surf_normal"] = [surface_normal(system, particle) @@ -406,7 +406,7 @@ function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem; write_meta_d n_fixed_particles = nparticles(system) - n_moving_particles(system) vtk["velocity"] = [current_velocity(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] vtk["jacobian"] = [det(deformation_gradient(system, particle)) for particle in eachparticle(system)] @@ -438,19 +438,11 @@ end function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem; write_meta_data=true) vtk["velocity"] = [current_velocity(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] vtk["pressure"] = [current_pressure(v, system, particle) - for particle in active_particles(system)] - - if write_meta_data - vtk["boundary_zone"] = type2string(first(typeof(system.boundary_zone).parameters)) - vtk["width"] = round(system.boundary_zone.zone_width, digits=3) - vtk["velocity_function"] = type2string(system.reference_velocity) - vtk["pressure_function"] = type2string(system.reference_pressure) - vtk["density_function"] = type2string(system.reference_density) - end + for particle in eachparticle(system)] return vtk end diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl index 6f73fb2c68..100d0f4963 100644 --- a/src/preprocessing/particle_packing/system.jl +++ b/src/preprocessing/particle_packing/system.jl @@ -218,7 +218,7 @@ end function write2vtk!(vtk, v, u, t, system::ParticlePackingSystem; write_meta_data=true) vtk["velocity"] = [advection_velocity(v, system, particle) - for particle in active_particles(system)] + for particle in eachparticle(system)] if write_meta_data vtk["signed_distances"] = system.signed_distances end diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl index 30e589e1c9..6738552f4b 100644 --- a/src/schemes/boundary/open_boundary/boundary_zones.jl +++ b/src/schemes/boundary/open_boundary/boundary_zones.jl @@ -7,8 +7,10 @@ struct OutFlow end @doc raw""" BoundaryZone(; plane, plane_normal, density, particle_spacing, initial_condition=nothing, extrude_geometry=nothing, - open_boundary_layers::Integer, boundary_type=BidirectionalFlow(), - average_inflow_velocity=true) + open_boundary_layers::Integer, average_inflow_velocity=true, + boundary_type=BidirectionalFlow(), + reference_density=nothing, reference_pressure=nothing, + reference_velocity=nothing) Boundary zone for [`OpenBoundarySPHSystem`](@ref). @@ -63,48 +65,100 @@ There are three ways to specify the actual shape of the boundary zone: anisotropic buffer-particles distribution, resulting in a potential numerical instability. Averaging mitigates these effects. +- `reference_velocity`: Reference velocity is either a function mapping each particle's coordinates + and time to its velocity, or, for a constant fluid velocity, + a vector holding this velocity. +- `reference_pressure`: Reference pressure is either a function mapping each particle's coordinates + and time to its pressure, or a scalar for a constant pressure over all particles. +- `reference_density`: Reference density is either a function mapping each particle's coordinates + and time to its density, or a scalar for a constant density over all particles. + +!!! note "Note" + The reference values (`reference_velocity`, `reference_pressure`, `reference_density`) + can also be set to `nothing`. + In this case, they will either be extrapolated from the fluid domain ([BoundaryModelMirroringTafuni](@ref BoundaryModelMirroringTafuni)) + or evolved using the characteristic flow variables ([BoundaryModelCharacteristicsLastiwka](@ref BoundaryModelCharacteristicsLastiwka)). # Examples -```julia +```jldoctest; output = false # 2D plane_points = ([0.0, 0.0], [0.0, 1.0]) -plane_normal=[1.0, 0.0] +plane_normal = [1.0, 0.0] + +# Constant reference velocity: +velocity_const = [1.0, 0.0] + +inflow_1 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, + density=1.0, open_boundary_layers=4, boundary_type=InFlow(), + reference_velocity=velocity_const) + +# Reference velocity as a function (parabolic velocity profile): +velocity_func = (pos, t) -> SVector(4.0 * pos[2] * (1.0 - pos[2]), 0.0) -inflow = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, - open_boundary_layers=4, boundary_type=InFlow()) +inflow_2 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, + density=1.0, open_boundary_layers=4, boundary_type=InFlow(), + reference_velocity=velocity_func) # 3D plane_points = ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) -plane_normal=[0.0, 0.0, 1.0] +plane_normal = [0.0, 0.0, 1.0] -outflow = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, - open_boundary_layers=4, boundary_type=OutFlow()) +# Constant reference pressure: +pressure_const = 0.0 + +outflow_1 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, + open_boundary_layers=4, boundary_type=OutFlow(), + reference_pressure=pressure_const) + +# Reference pressure as a function (y-dependent profile, sinusoidal in time): +pressure_func = (pos, t) -> pos[2] * sin(2pi * t) + +outflow_2 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, + open_boundary_layers=4, boundary_type=OutFlow(), + reference_pressure=pressure_func) # 3D particles sampled as cylinder circle = SphereShape(0.1, 0.5, (0.5, 0.5), 1.0, sphere_type=RoundSphere()) bidirectional_flow = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, - density=1.0, extrude_geometry=circle, open_boundary_layers=4) + density=1.0, boundary_type=BidirectionalFlow(), + extrude_geometry=circle, open_boundary_layers=4) + +# output +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ BoundaryZone │ +│ ════════════ │ +│ boundary type: ………………………………………… bidirectional_flow │ +│ #particles: ………………………………………………… 234 │ +│ width: ……………………………………………………………… 0.4 │ +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ ``` !!! warning "Experimental Implementation" This is an experimental feature and may change in any future releases. """ -struct BoundaryZone{BT, IC, S, ZO, ZW, FD, PN} - initial_condition :: IC - spanning_set :: S - zone_origin :: ZO - zone_width :: ZW - flow_direction :: FD - plane_normal :: PN - boundary_type :: BT +struct BoundaryZone{IC, S, ZO, ZW, FD, PN, R} + initial_condition :: IC + spanning_set :: S + zone_origin :: ZO + zone_width :: ZW + flow_direction :: FD + plane_normal :: PN + reference_values :: R + # Note that the following can't be static type parameters, as all boundary zones in a system + # must have the same type, so that we can loop over them in a type-stable way. average_inflow_velocity :: Bool + prescribed_density :: Bool + prescribed_pressure :: Bool + prescribed_velocity :: Bool end function BoundaryZone(; plane, plane_normal, density, particle_spacing, initial_condition=nothing, extrude_geometry=nothing, - open_boundary_layers::Integer, boundary_type=BidirectionalFlow(), - average_inflow_velocity=true) + open_boundary_layers::Integer, average_inflow_velocity=true, + boundary_type=BidirectionalFlow(), + reference_density=nothing, reference_pressure=nothing, + reference_velocity=nothing) if open_boundary_layers <= 0 throw(ArgumentError("`open_boundary_layers` must be positive and greater than zero")) end @@ -112,52 +166,147 @@ function BoundaryZone(; plane, plane_normal, density, particle_spacing, # `plane_normal` always points in fluid domain plane_normal_ = normalize(SVector(plane_normal...)) - if boundary_type isa BidirectionalFlow - flow_direction = nothing + ic, flow_direction, spanning_set_, zone_origin, + zone_width = set_up_boundary_zone(plane, plane_normal_, density, particle_spacing, + initial_condition, extrude_geometry, + open_boundary_layers, boundary_type) + + NDIMS = ndims(ic) + ELTYPE = eltype(ic) + if !(reference_velocity isa Function || isnothing(reference_velocity) || + (reference_velocity isa Vector && length(reference_velocity) == NDIMS)) + throw(ArgumentError("`reference_velocity` must be either a function mapping " * + "each particle's coordinates and time to its velocity, " * + "or, for a constant fluid velocity, a vector of length $NDIMS for a $(NDIMS)D problem holding this velocity")) + else + if reference_velocity isa Function + test_result = reference_velocity(zeros(NDIMS), 0.0) + if length(test_result) != NDIMS + throw(ArgumentError("`velocity` function must be of dimension $NDIMS")) + end + end + # We need this dummy for type stability reasons + velocity_dummy = SVector(ntuple(dim -> convert(ELTYPE, Inf), NDIMS)) + velocity_ref = wrap_reference_function(reference_velocity, velocity_dummy) + end - elseif boundary_type isa InFlow - # Unit vector pointing in downstream direction - flow_direction = plane_normal_ + if !(reference_pressure isa Function || reference_pressure isa Real || + isnothing(reference_pressure)) + throw(ArgumentError("`reference_pressure` must be either a function mapping " * + "each particle's coordinates and time to its pressure, " * + "or a scalar")) + else + if reference_pressure isa Function + test_result = reference_pressure(zeros(NDIMS), 0.0) + if length(test_result) != 1 + throw(ArgumentError("`reference_pressure` function must be a scalar function")) + end + end + # We need this dummy for type stability reasons + pressure_dummy = convert(ELTYPE, Inf) + pressure_ref = wrap_reference_function(reference_pressure, pressure_dummy) + end - elseif boundary_type isa OutFlow - # Unit vector pointing in downstream direction - flow_direction = -plane_normal_ + if !(reference_density isa Function || reference_density isa Real || + isnothing(reference_density)) + throw(ArgumentError("`reference_density` must be either a function mapping " * + "each particle's coordinates and time to its density, " * + "or a scalar")) + else + if reference_density isa Function + test_result = reference_density(zeros(NDIMS), 0.0) + if length(test_result) != 1 + throw(ArgumentError("`reference_density` function must be a scalar function")) + end + end + # We need this dummy for type stability reasons + density_dummy = convert(ELTYPE, Inf) + density_ref = wrap_reference_function(reference_density, density_dummy) end - ic, spanning_set_, zone_origin, - zone_width = set_up_boundary_zone(plane, plane_normal_, flow_direction, density, - particle_spacing, initial_condition, - extrude_geometry, open_boundary_layers; - boundary_type=boundary_type) + prescribed_pressure = isnothing(reference_pressure) ? false : true + prescribed_density = isnothing(reference_density) ? false : true + prescribed_velocity = isnothing(reference_velocity) ? false : true + + reference_values = (reference_velocity=velocity_ref, reference_pressure=pressure_ref, + reference_density=density_ref) + + coordinates_svector = reinterpret(reshape, SVector{NDIMS, ELTYPE}, ic.coordinates) + + if prescribed_pressure + ic.pressure .= pressure_ref.(coordinates_svector, 0) + end + if prescribed_density + ic.density .= density_ref.(coordinates_svector, 0) + ic.mass .= ic.density * ic.particle_spacing^NDIMS + end + if prescribed_velocity + ic.velocity .= stack(velocity_ref.(coordinates_svector, 0)) + end return BoundaryZone(ic, spanning_set_, zone_origin, zone_width, - flow_direction, plane_normal_, boundary_type, - average_inflow_velocity) + flow_direction, plane_normal_, reference_values, + average_inflow_velocity, prescribed_density, prescribed_pressure, + prescribed_velocity) +end + +function boundary_type_name(boundary_zone::BoundaryZone) + (; flow_direction, plane_normal) = boundary_zone + + if isnothing(flow_direction) + return "bidirectional_flow" + elseif signbit(dot(flow_direction, plane_normal)) + return "outflow" + else + return "inflow" + end +end + +function Base.show(io::IO, boundary_zone::BoundaryZone) + @nospecialize boundary_zone # reduce precompilation time + + print(io, "BoundaryZone(") + print(io, ") with ", nparticles(boundary_zone.initial_condition), " particles") +end + +function Base.show(io::IO, ::MIME"text/plain", boundary_zone::BoundaryZone) + @nospecialize boundary_zone # reduce precompilation time + + if get(io, :compact, false) + show(io, boundary_zone) + else + summary_header(io, "BoundaryZone") + summary_line(io, "boundary type", boundary_type_name(boundary_zone)) + summary_line(io, "#particles", nparticles(boundary_zone.initial_condition)) + summary_line(io, "width", round(boundary_zone.zone_width, digits=3)) + summary_footer(io) + end end -function set_up_boundary_zone(plane, plane_normal, flow_direction, density, - particle_spacing, initial_condition, extrude_geometry, - open_boundary_layers; boundary_type) +function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, + initial_condition, extrude_geometry, open_boundary_layers, + boundary_type) if boundary_type isa InFlow - extrude_direction = -flow_direction + # Unit vector pointing in downstream direction + flow_direction = plane_normal elseif boundary_type isa OutFlow - extrude_direction = flow_direction + # Unit vector pointing in downstream direction + flow_direction = -plane_normal elseif boundary_type isa BidirectionalFlow - # `plane_normal` is always pointing in the fluid domain - extrude_direction = -plane_normal + flow_direction = nothing end # Sample particles in boundary zone if isnothing(initial_condition) && isnothing(extrude_geometry) initial_condition = TrixiParticles.extrude_geometry(plane; particle_spacing, density, - direction=extrude_direction, + direction=(-plane_normal), n_extrude=open_boundary_layers) elseif !isnothing(extrude_geometry) initial_condition = TrixiParticles.extrude_geometry(extrude_geometry; particle_spacing, density, - direction=extrude_direction, + direction=(-plane_normal), n_extrude=open_boundary_layers) else initial_condition = initial_condition @@ -205,7 +354,7 @@ function set_up_boundary_zone(plane, plane_normal, flow_direction, density, # This check is only necessary when `initial_condition` or `extrude_geometry` are passed. ic = remove_outside_particles(initial_condition, spanning_set_, zone_origin) - return ic, spanning_set_, zone_origin, zone_width + return ic, flow_direction, spanning_set_, zone_origin, zone_width end function calculate_spanning_vectors(plane, zone_width) @@ -237,7 +386,7 @@ function spanning_vectors(plane_points::NTuple{3}, zone_width) return hcat(c, edge1, edge2) end -@inline function is_in_boundary_zone(boundary_zone::BoundaryZone, particle_coords) +@inline function is_in_boundary_zone(boundary_zone, particle_coords) (; zone_origin, spanning_set) = boundary_zone particle_position = particle_coords - zone_origin @@ -261,6 +410,27 @@ end return true end +function update_boundary_zone_indices!(system, u, boundary_zones, semi) + set_zero!(system.boundary_zone_indices) + + @threaded semi for particle in each_moving_particle(system) + particle_coords = current_coords(u, system, particle) + + for (zone_id, boundary_zone) in enumerate(boundary_zones) + # Check if boundary particle is in the boundary zone + if is_in_boundary_zone(boundary_zone, particle_coords) + system.boundary_zone_indices[particle] = zone_id + end + end + end + + return system +end + +function current_boundary_zone(system, particle) + return system.boundary_zones[system.boundary_zone_indices[particle]] +end + function remove_outside_particles(initial_condition, spanning_set, zone_origin) (; coordinates, density, particle_spacing) = initial_condition @@ -276,3 +446,64 @@ function remove_outside_particles(initial_condition, spanning_set, zone_origin) return InitialCondition(; coordinates=coordinates[:, in_zone], density=first(density), particle_spacing) end + +function wrap_reference_function(function_::Nothing, ref_dummy) + # Return a dummy value for type stability + return @inline((coords, t)->ref_dummy) +end + +function wrap_reference_function(function_::Function, ref_dummy) + # Already a function + return function_ +end + +function wrap_reference_function(constant_scalar::Number, ref_dummy) + return @inline((coords, t)->constant_scalar) +end + +function wrap_reference_function(constant_vector::AbstractVector, + ref_dummy::SVector{NDIMS, ELTYPE}) where {NDIMS, ELTYPE} + return @inline((coords, t)->SVector{NDIMS, ELTYPE}(constant_vector)) +end + +function reference_pressure(boundary_zone, v, system, particle, pos, t) + (; prescribed_pressure) = boundary_zone + (; pressure_reference_values) = system.cache + + if prescribed_pressure + zone_id = system.boundary_zone_indices[particle] + + # `pressure_reference_values[zone_id](pos, t)`, but in a type-stable way + return apply_ith_function(pressure_reference_values, zone_id, pos, t) + else + return current_pressure(v, system, particle) + end +end + +function reference_density(boundary_zone, v, system, particle, pos, t) + (; prescribed_density) = boundary_zone + (; density_reference_values) = system.cache + + if prescribed_density + zone_id = system.boundary_zone_indices[particle] + + # `density_reference_values[zone_id](pos, t)`, but in a type-stable way + return apply_ith_function(density_reference_values, zone_id, pos, t) + else + return current_density(v, system, particle) + end +end + +function reference_velocity(boundary_zone, v, system, particle, pos, t) + (; prescribed_velocity) = boundary_zone + (; velocity_reference_values) = system.cache + + if prescribed_velocity + zone_id = system.boundary_zone_indices[particle] + + # `velocity_reference_values[zone_id](pos, t)`, but in a type-stable way + return apply_ith_function(velocity_reference_values, zone_id, pos, t) + else + return current_velocity(v, system, particle) + end +end diff --git a/src/schemes/boundary/open_boundary/method_of_characteristics.jl b/src/schemes/boundary/open_boundary/method_of_characteristics.jl index a37a9cd605..4e25500462 100644 --- a/src/schemes/boundary/open_boundary/method_of_characteristics.jl +++ b/src/schemes/boundary/open_boundary/method_of_characteristics.jl @@ -1,5 +1,5 @@ @doc raw""" - BoundaryModelLastiwka(; extrapolate_reference_values=nothing) + BoundaryModelCharacteristicsLastiwka(; extrapolate_reference_values=nothing) Boundary model for [`OpenBoundarySPHSystem`](@ref). This model uses the characteristic variables to propagate the appropriate values @@ -19,58 +19,55 @@ For more information about the method see [description below](@ref method_of_cha Note that even without this extrapolation feature, the reference values don't need to be prescribed - they're computed from the characteristics. """ -struct BoundaryModelLastiwka{T} +struct BoundaryModelCharacteristicsLastiwka{T} extrapolate_reference_values::T - function BoundaryModelLastiwka(; extrapolate_reference_values=nothing) + function BoundaryModelCharacteristicsLastiwka(; extrapolate_reference_values=nothing) return new{typeof(extrapolate_reference_values)}(extrapolate_reference_values) end end # Called from update callback via `update_open_boundary_eachstep!` -@inline function update_boundary_quantities!(system, boundary_model::BoundaryModelLastiwka, +@inline function update_boundary_quantities!(system, + boundary_model::BoundaryModelCharacteristicsLastiwka, v, u, v_ode, u_ode, semi, t) - (; density, pressure, cache, boundary_zone, - reference_velocity, reference_pressure, reference_density) = system - (; flow_direction) = boundary_zone - - fluid_system = corresponding_fluid_system(system, semi) + (; density, pressure, cache, boundary_zones, fluid_system) = system sound_speed = system_sound_speed(fluid_system) if !isnothing(boundary_model.extrapolate_reference_values) - (; prescribed_pressure, prescribed_velocity, prescribed_density) = cache v_fluid = wrap_v(v_ode, fluid_system, semi) u_fluid = wrap_u(u_ode, fluid_system, semi) @trixi_timeit timer() "extrapolate and correct values" begin extrapolate_values!(system, boundary_model.extrapolate_reference_values, - v, v_fluid, u, u_fluid, semi, t; - prescribed_pressure, prescribed_velocity, - prescribed_density) + v, v_fluid, u, u_fluid, semi) end end # Update quantities based on the characteristic variables @threaded semi for particle in each_moving_particle(system) + boundary_zone = current_boundary_zone(system, particle) + (; flow_direction) = boundary_zone + particle_position = current_coords(u, system, particle) J1 = cache.characteristics[1, particle] J2 = cache.characteristics[2, particle] J3 = cache.characteristics[3, particle] - rho_ref = reference_value(reference_density, density[particle], - particle_position, t) + rho_ref = reference_density(boundary_zone, v, system, particle, + particle_position, t) + density[particle] = rho_ref + ((-J1 + (J2 + J3) / 2) / sound_speed^2) - p_ref = reference_value(reference_pressure, pressure[particle], - particle_position, t) + p_ref = reference_pressure(boundary_zone, v, system, particle, particle_position, t) + pressure[particle] = p_ref + (J2 + J3) / 2 - v_current = current_velocity(v, system, particle) - v_ref = reference_value(reference_velocity, v_current, - particle_position, t) - rho = density[particle] + v_ref = reference_velocity(boundary_zone, v, system, particle, particle_position, t) + + rho = current_density(v, system, particle) v_ = v_ref + ((J2 - J3) / (2 * sound_speed * rho)) * flow_direction for dim in 1:ndims(system) @@ -78,19 +75,21 @@ end end end - if boundary_zone.average_inflow_velocity - # Even if the velocity is prescribed, this boundary model computes the velocity for each particle individually. - # Thus, turbulent flows near the inflow can lead to a non-uniform buffer particle distribution, - # resulting in a potential numerical instability. Averaging mitigates these effects. - average_velocity!(v, u, system, boundary_model, boundary_zone, semi) + for boundary_zone in boundary_zones + if boundary_zone.average_inflow_velocity + # Even if the velocity is prescribed, this boundary model computes the velocity for each particle individually. + # Thus, turbulent flows near the inflow can lead to a non-uniform buffer particle distribution, + # resulting in a potential numerical instability. Averaging mitigates these effects. + average_velocity!(v, u, system, boundary_model, boundary_zone, semi) + end end return system end # Called from semidiscretization -function update_boundary_model!(system, ::BoundaryModelLastiwka, v, u, v_ode, u_ode, - semi, t) +function update_boundary_model!(system, ::BoundaryModelCharacteristicsLastiwka, + v, u, v_ode, u_ode, semi, t) @trixi_timeit timer() "evaluate characteristics" begin evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) end @@ -103,9 +102,8 @@ end # J2: Propagates downstream to the local flow # J3: Propagates upstream to the local flow function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) - (; volume, cache, boundary_zone) = system + (; volume, cache, fluid_system, density, pressure) = system (; characteristics, previous_characteristics) = cache - fluid_system = corresponding_fluid_system(system, semi) @threaded semi for particle in eachparticle(system) previous_characteristics[1, particle] = characteristics[1, particle] @@ -117,14 +115,56 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) set_zero!(volume) # Evaluate the characteristic variables with the fluid system - evaluate_characteristics!(system, fluid_system, v, u, v_ode, u_ode, semi, t) + v_fluid = wrap_v(v_ode, fluid_system, semi) + u_fluid = wrap_u(u_ode, fluid_system, semi) + + system_coords = current_coordinates(u, system) + fluid_coords = current_coordinates(u_fluid, fluid_system) + sound_speed = system_sound_speed(fluid_system) + + # Loop over all fluid neighbors within the kernel cutoff + foreach_point_neighbor(system, fluid_system, system_coords, fluid_coords, semi; + points=each_moving_particle(system)) do particle, neighbor, + pos_diff, distance + boundary_zone = current_boundary_zone(system, particle) + (; flow_direction) = boundary_zone + + neighbor_position = current_coords(u_fluid, fluid_system, neighbor) + + # Determine current and prescribed quantities + rho_b = current_density(v_fluid, fluid_system, neighbor) + + rho_ref = reference_density(boundary_zone, v, system, particle, + neighbor_position, t) + + p_b = current_pressure(v_fluid, fluid_system, neighbor) + + p_ref = reference_pressure(boundary_zone, v, system, particle, neighbor_position, t) + + v_b = current_velocity(v_fluid, fluid_system, neighbor) + + v_neighbor_ref = reference_velocity(boundary_zone, v, system, particle, + neighbor_position, t) + + # Determine characteristic variables + density_term = -sound_speed^2 * (rho_b - rho_ref) + pressure_term = p_b - p_ref + velocity_term = rho_b * sound_speed * (dot(v_b - v_neighbor_ref, flow_direction)) + + kernel_ = smoothing_kernel(fluid_system, distance, particle) + + characteristics[1, particle] += (density_term + pressure_term) * kernel_ + characteristics[2, particle] += (velocity_term + pressure_term) * kernel_ + characteristics[3, particle] += (-velocity_term + pressure_term) * kernel_ + + volume[particle] += kernel_ + end # Only some of the in-/outlet particles are in the influence of the fluid particles. # Thus, we compute the characteristics for the particles that are outside the influence # of fluid particles by using the average of the values of the previous time step. # See eq. 27 in Negi (2020) https://doi.org/10.1016/j.cma.2020.113119 @threaded semi for particle in each_moving_particle(system) - # Particle is outside of the influence of fluid particles if isapprox(volume[particle], 0) @@ -161,93 +201,43 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) characteristics[2, particle] /= volume[particle] characteristics[3, particle] /= volume[particle] end - prescribe_conditions!(characteristics, particle, boundary_zone) - end - - return system -end - -function evaluate_characteristics!(system, neighbor_system::FluidSystem, - v, u, v_ode, u_ode, semi, t) - (; volume, cache, boundary_zone, density, pressure, - reference_velocity, reference_pressure, reference_density) = system - (; flow_direction) = boundary_zone - (; characteristics) = cache - - v_neighbor_system = wrap_v(v_ode, neighbor_system, semi) - u_neighbor_system = wrap_u(u_ode, neighbor_system, semi) - - system_coords = current_coordinates(u, system) - neighbor_coords = current_coordinates(u_neighbor_system, neighbor_system) - sound_speed = system_sound_speed(neighbor_system) - - # Loop over all fluid neighbors within the kernel cutoff - foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance - neighbor_position = current_coords(u_neighbor_system, neighbor_system, neighbor) - - # Determine current and prescribed quantities - rho_b = current_density(v_neighbor_system, neighbor_system, neighbor) - rho_ref = reference_value(reference_density, density[particle], - neighbor_position, t) - - p_b = current_pressure(v_neighbor_system, neighbor_system, neighbor) - p_ref = reference_value(reference_pressure, pressure[particle], - neighbor_position, t) - - v_b = current_velocity(v_neighbor_system, neighbor_system, neighbor) - v_particle = current_velocity(v, system, particle) - v_neighbor_ref = reference_value(reference_velocity, v_particle, - neighbor_position, t) - - # Determine characteristic variables - density_term = -sound_speed^2 * (rho_b - rho_ref) - pressure_term = p_b - p_ref - velocity_term = rho_b * sound_speed * (dot(v_b - v_neighbor_ref, flow_direction)) - kernel_ = smoothing_kernel(neighbor_system, distance, particle) + boundary_zone = current_boundary_zone(system, particle) + (; flow_direction, plane_normal) = boundary_zone - characteristics[1, particle] += (density_term + pressure_term) * kernel_ - characteristics[2, particle] += (velocity_term + pressure_term) * kernel_ - characteristics[3, particle] += (-velocity_term + pressure_term) * kernel_ + # Outflow + if signbit(dot(flow_direction, plane_normal)) + # J3 is prescribed (i.e. determined from the exterior of the domain). + # J1 and J2 is transmitted from the domain interior. + characteristics[3, particle] = zero(eltype(characteristics)) - volume[particle] += kernel_ + else # Inflow + # Allow only J3 to propagate upstream to the boundary + characteristics[1, particle] = zero(eltype(characteristics)) + characteristics[2, particle] = zero(eltype(characteristics)) + end end return system end -@inline function prescribe_conditions!(characteristics, particle, ::BoundaryZone{OutFlow}) - # J3 is prescribed (i.e. determined from the exterior of the domain). - # J1 and J2 is transmitted from the domain interior. - characteristics[3, particle] = zero(eltype(characteristics)) +function average_velocity!(v, u, system, ::BoundaryModelCharacteristicsLastiwka, + boundary_zone, semi) + (; flow_direction, plane_normal) = boundary_zone - return characteristics -end - -@inline function prescribe_conditions!(characteristics, particle, ::BoundaryZone{InFlow}) - # Allow only J3 to propagate upstream to the boundary - characteristics[1, particle] = zero(eltype(characteristics)) - characteristics[2, particle] = zero(eltype(characteristics)) - - return characteristics -end + # This is an outflow. Only apply averaging at the inflow. + signbit(dot(flow_direction, plane_normal)) && return v -function average_velocity!(v, u, system, ::BoundaryModelLastiwka, boundary_zone, semi) - # Only apply averaging at the inflow - return v -end - -function average_velocity!(v, u, system, ::BoundaryModelLastiwka, ::BoundaryZone{InFlow}, - semi) + particles_in_zone = findall(particle -> boundary_zone == + current_boundary_zone(system, particle), + each_moving_particle(system)) # Division inside the `sum` closure to maintain GPU compatibility - avg_velocity = sum(each_moving_particle(system)) do particle - return current_velocity(v, system, particle) / system.buffer.active_particle_count[] + avg_velocity = sum(particles_in_zone) do particle + return current_velocity(v, system, particle) / length(particles_in_zone) end - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in particles_in_zone # Set the velocity of the ghost node to the average velocity of the fluid domain for dim in eachindex(avg_velocity) @inbounds v[dim, particle] = avg_velocity[dim] diff --git a/src/schemes/boundary/open_boundary/mirroring.jl b/src/schemes/boundary/open_boundary/mirroring.jl index 45a98ad7c2..e1f33fed7e 100644 --- a/src/schemes/boundary/open_boundary/mirroring.jl +++ b/src/schemes/boundary/open_boundary/mirroring.jl @@ -47,7 +47,7 @@ The interpolated values at the ghost nodes are then assigned to the correspondin struct ZerothOrderMirroring end @doc raw""" - BoundaryModelTafuni(; mirror_method=FirstOrderMirroring()) + BoundaryModelMirroringTafuni(; mirror_method=FirstOrderMirroring()) Boundary model for the `OpenBoundarySPHSystem`. This model implements the method of [Tafuni et al. (2018)](@cite Tafuni2018) to extrapolate the properties from the fluid domain @@ -61,62 +61,56 @@ We provide three different mirroring methods: - [`FirstOrderMirroring`](@ref): Uses a first order correction based on the gradient of the interpolated values . - [`SimpleMirroring`](@ref): Similar to the first order mirroring, but does not use the gradient of the interpolated values. """ -struct BoundaryModelTafuni{MM} +struct BoundaryModelMirroringTafuni{MM} mirror_method::MM end -function BoundaryModelTafuni(; mirror_method=FirstOrderMirroring()) - return BoundaryModelTafuni(mirror_method) +function BoundaryModelMirroringTafuni(; mirror_method=FirstOrderMirroring()) + return BoundaryModelMirroringTafuni(mirror_method) end -function update_boundary_quantities!(system, boundary_model::BoundaryModelTafuni, +function update_boundary_quantities!(system, boundary_model::BoundaryModelMirroringTafuni, v, u, v_ode, u_ode, semi, t) - (; reference_pressure, reference_density, reference_velocity, boundary_zone, - pressure, density, cache) = system - (; prescribed_pressure, prescribed_density, prescribed_velocity) = cache + (; boundary_zones, pressure, density, fluid_system, cache) = system @trixi_timeit timer() "extrapolate and correct values" begin - fluid_system = corresponding_fluid_system(system, semi) - v_fluid = wrap_v(v_ode, fluid_system, semi) u_fluid = wrap_u(u_ode, fluid_system, semi) - extrapolate_values!(system, boundary_model.mirror_method, v, v_fluid, - u, u_fluid, semi, t; prescribed_pressure, - prescribed_density, prescribed_velocity) - end - - if !prescribed_velocity && boundary_zone.average_inflow_velocity - # When no velocity is prescribed at the inflow, the velocity is extrapolated from the fluid domain. - # Thus, turbulent flows near the inflow can lead to a non-uniform buffer particle distribution, - # resulting in a potential numerical instability. Averaging mitigates these effects. - average_velocity!(v, u, system, boundary_zone, semi) + extrapolate_values!(system, boundary_model.mirror_method, + v, v_fluid, u, u_fluid, semi) end - if prescribed_pressure - @threaded semi for particle in each_moving_particle(system) - particle_coords = current_coords(u, system, particle) + for boundary_zone in boundary_zones + (; average_inflow_velocity, prescribed_velocity) = boundary_zone - pressure[particle] = reference_value(reference_pressure, pressure[particle], - particle_coords, t) + if !prescribed_velocity && average_inflow_velocity + # When no velocity is prescribed at the inflow, the velocity is extrapolated from the fluid domain. + # Thus, turbulent flows near the inflow can lead to a non-uniform buffer particle distribution, + # resulting in a potential numerical instability. Averaging mitigates these effects. + average_velocity!(v, u, system, boundary_zone, semi) end end - if prescribed_density - @threaded semi for particle in each_moving_particle(system) - particle_coords = current_coords(u, system, particle) + @threaded semi for particle in each_moving_particle(system) + boundary_zone = current_boundary_zone(system, particle) + (; prescribed_density, prescribed_pressure, prescribed_velocity) = boundary_zone + + particle_coords = current_coords(u, system, particle) - density[particle] = reference_value(reference_density, density[particle], - particle_coords, t) + if prescribed_pressure + pressure[particle] = reference_pressure(boundary_zone, v, system, particle, + particle_coords, t) end - end - if prescribed_velocity - @threaded semi for particle in each_moving_particle(system) - particle_coords = current_coords(u, system, particle) - v_particle = current_velocity(v, system, particle) + if prescribed_density + density[particle] = reference_density(boundary_zone, v, system, particle, + particle_coords, t) + end - v_ref = reference_value(reference_velocity, v_particle, particle_coords, t) + if prescribed_velocity + v_ref = reference_velocity(boundary_zone, v, system, particle, + particle_coords, t) for dim in eachindex(v_ref) @inbounds v[dim, particle] = v_ref[dim] @@ -125,16 +119,15 @@ function update_boundary_quantities!(system, boundary_model::BoundaryModelTafuni end end -update_boundary_model!(system, ::BoundaryModelTafuni, v, u, v_ode, u_ode, semi, t) = system +function update_boundary_model!(system, ::BoundaryModelMirroringTafuni, v, u, v_ode, u_ode, + semi, t) + return system +end function extrapolate_values!(system, mirror_method::Union{FirstOrderMirroring, SimpleMirroring}, - v_open_boundary, v_fluid, u_open_boundary, u_fluid, - semi, t; prescribed_density=false, - prescribed_pressure=false, prescribed_velocity=false) - (; pressure, density, boundary_zone) = system - - fluid_system = corresponding_fluid_system(system, semi) + v_open_boundary, v_fluid, u_open_boundary, u_fluid, semi) + (; pressure, density, fluid_system) = system # Static indices to avoid allocations two_to_end = SVector{ndims(system)}(2:(ndims(system) + 1)) @@ -149,6 +142,9 @@ function extrapolate_values!(system, # We can do this because we require the neighborhood search to support querying neighbors # of arbitrary positions (see `PointNeighbors.requires_update`). @threaded semi for particle in each_moving_particle(system) + boundary_zone = current_boundary_zone(system, particle) + (; prescribed_density, prescribed_pressure, prescribed_velocity) = boundary_zone + particle_coords = current_coords(u_open_boundary, system, particle) ghost_node_position = mirror_position(particle_coords, boundary_zone) @@ -280,12 +276,8 @@ function extrapolate_values!(system, end function extrapolate_values!(system, mirror_method::ZerothOrderMirroring, - v_open_boundary, v_fluid, u_open_boundary, u_fluid, semi, t; - prescribed_density=false, prescribed_pressure=false, - prescribed_velocity=false) - (; pressure, density, boundary_zone) = system - - fluid_system = corresponding_fluid_system(system, semi) + v_open_boundary, v_fluid, u_open_boundary, u_fluid, semi) + (; pressure, density, fluid_system) = system # Use the fluid-fluid nhs, since the boundary particles are mirrored into the fluid domain nhs = get_neighborhood_search(fluid_system, fluid_system, semi) @@ -297,6 +289,9 @@ function extrapolate_values!(system, mirror_method::ZerothOrderMirroring, # We can do this because we require the neighborhood search to support querying neighbors # of arbitrary positions (see `PointNeighbors.requires_update`). @threaded semi for particle in each_moving_particle(system) + boundary_zone = current_boundary_zone(system, particle) + (; prescribed_pressure, prescribed_density, prescribed_velocity) = boundary_zone + particle_coords = current_coords(u_open_boundary, system, particle) ghost_node_position = mirror_position(particle_coords, boundary_zone) @@ -486,10 +481,15 @@ function mirror_position(particle_coords, boundary_zone) return particle_coords - 2 * dist * boundary_zone.plane_normal end -average_velocity!(v, u, system, boundary_zone, semi) = v +# Only for inflow boundary zones +function average_velocity!(v, u, system, boundary_zone, semi) + (; plane_normal, zone_origin, initial_condition, flow_direction) = boundary_zone -function average_velocity!(v, u, system, boundary_zone::BoundaryZone{InFlow}, semi) - (; plane_normal, zone_origin, initial_condition) = boundary_zone + # Bidirectional flow + isnothing(flow_direction) && return v + + # Outflow + signbit(dot(flow_direction, plane_normal)) && return v # We only use the extrapolated velocity in the vicinity of the transition region. # Otherwise, if the boundary zone is too large, averaging would be excessively influenced @@ -497,15 +497,20 @@ function average_velocity!(v, u, system, boundary_zone::BoundaryZone{InFlow}, se max_dist = initial_condition.particle_spacing * 110 / 100 candidates = findall(x -> dot(x - zone_origin, -plane_normal) <= max_dist, - reinterpret(reshape, SVector{ndims(system), eltype(u)}, - active_coordinates(u, system))) + reinterpret(reshape, SVector{ndims(system), eltype(u)}, u)) + + particles_in_zone = findall(particle -> boundary_zone == + current_boundary_zone(system, particle), + each_moving_particle(system)) + + intersect!(candidates, particles_in_zone) # Division inside the `sum` closure to maintain GPU compatibility avg_velocity = sum(candidates) do particle return current_velocity(v, system, particle) / length(candidates) end - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in particles_in_zone # Set the velocity of the ghost node to the average velocity of the fluid domain for dim in eachindex(avg_velocity) @inbounds v[dim, particle] = avg_velocity[dim] @@ -515,10 +520,16 @@ function average_velocity!(v, u, system, boundary_zone::BoundaryZone{InFlow}, se return v end -project_velocity_on_plane_normal!(v, system, particle, boundary_zone) = v +# Only for inflow boundary zones +function project_velocity_on_plane_normal!(v, system, particle, boundary_zone) + (; plane_normal, flow_direction) = boundary_zone + + # Bidirectional flow + isnothing(flow_direction) && return v + + # Outflow + signbit(dot(flow_direction, plane_normal)) && return v -function project_velocity_on_plane_normal!(v, system, particle, - boundary_zone::BoundaryZone{InFlow}) # Project `vel` on the normal direction of the boundary zone # See https://doi.org/10.1016/j.jcp.2020.110029 Section 3.3.: # "Because flow from the inlet interface occurs perpendicular to the boundary, diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index 4cadc62119..a7fe53308d 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -1,10 +1,7 @@ @doc raw""" OpenBoundarySPHSystem(boundary_zone::BoundaryZone; fluid_system::FluidSystem, buffer_size::Integer, - boundary_model, - reference_velocity=nothing, - reference_pressure=nothing, - reference_density=nothing) + boundary_model) Open boundary system for in- and outflow particles. @@ -15,184 +12,136 @@ Open boundary system for in- and outflow particles. - `fluid_system`: The corresponding fluid system - `boundary_model`: Boundary model (see [Open Boundary Models](@ref open_boundary_models)) - `buffer_size`: Number of buffer particles. -- `reference_velocity`: Reference velocity is either a function mapping each particle's coordinates - and time to its velocity, an array where the ``i``-th column holds - the velocity of particle ``i`` or, for a constant fluid velocity, - a vector holding this velocity. -- `reference_pressure`: Reference pressure is either a function mapping each particle's coordinates - and time to its pressure, a vector holding the pressure of each particle, - or a scalar for a constant pressure over all particles. -- `reference_density`: Reference density is either a function mapping each particle's coordinates - and time to its density, a vector holding the density of each particle, - or a scalar for a constant density over all particles. - -!!! note "Note" - The reference values (`reference_velocity`, `reference_pressure`, `reference_density`) - can also be set to `nothing`. - In this case, they will either be extrapolated from the fluid domain ([BoundaryModelTafuni](@ref BoundaryModelTafuni)) - or evolved using the characteristic flow variables ([BoundaryModelLastiwka](@ref BoundaryModelLastiwka)). !!! warning "Experimental Implementation" - This is an experimental feature and may change in future releases. - It is GPU-compatible (e.g., with CUDA.jl and AMDGPU.jl), but currently **not** supported with Metal.jl. + This is an experimental feature and may change in any future releases. """ -struct OpenBoundarySPHSystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZ, RV, - RP, RD, B, C} <: System{NDIMS} - boundary_model :: BM - initial_condition :: IC - fluid_system :: FS - fluid_system_index :: FSI - smoothing_length :: ELTYPE - mass :: ARRAY1D # Array{ELTYPE, 1}: [particle] - density :: ARRAY1D # Array{ELTYPE, 1}: [particle] - volume :: ARRAY1D # Array{ELTYPE, 1}: [particle] - pressure :: ARRAY1D # Array{ELTYPE, 1}: [particle] - boundary_candidates :: BC # Array{UInt32, 1}: [particle] - fluid_candidates :: FC # Array{UInt32, 1}: [particle] - boundary_zone :: BZ - reference_velocity :: RV - reference_pressure :: RP - reference_density :: RD - buffer :: B - cache :: C +struct OpenBoundarySPHSystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZI, BZ, + B, C} <: System{NDIMS} + boundary_model :: BM + initial_condition :: IC + fluid_system :: FS + fluid_system_index :: FSI + smoothing_length :: ELTYPE + mass :: ARRAY1D # Array{ELTYPE, 1}: [particle] + density :: ARRAY1D # Array{ELTYPE, 1}: [particle] + volume :: ARRAY1D # Array{ELTYPE, 1}: [particle] + pressure :: ARRAY1D # Array{ELTYPE, 1}: [particle] + boundary_candidates :: BC # Array{Bool, 1}: [particle] + fluid_candidates :: FC # Array{Bool, 1}: [particle] + boundary_zone_indices :: BZI # Array{UInt8, 1}: [particle] + boundary_zones :: BZ + buffer :: B + cache :: C end function OpenBoundarySPHSystem(boundary_model, initial_condition, fluid_system, fluid_system_index, smoothing_length, mass, density, volume, pressure, boundary_candidates, fluid_candidates, - boundary_zone, reference_velocity, - reference_pressure, reference_density, buffer, cache) + boundary_zone_indices, boundary_zone, buffer, cache) OpenBoundarySPHSystem{typeof(boundary_model), eltype(mass), ndims(initial_condition), typeof(initial_condition), typeof(fluid_system), typeof(fluid_system_index), typeof(mass), typeof(boundary_candidates), typeof(fluid_candidates), - typeof(boundary_zone), typeof(reference_velocity), - typeof(reference_pressure), typeof(reference_density), + typeof(boundary_zone_indices), typeof(boundary_zone), typeof(buffer), typeof(cache)}(boundary_model, initial_condition, fluid_system, fluid_system_index, smoothing_length, mass, density, volume, pressure, boundary_candidates, - fluid_candidates, boundary_zone, - reference_velocity, reference_pressure, - reference_density, buffer, cache) + fluid_candidates, boundary_zone_indices, + boundary_zone, buffer, cache) end -function OpenBoundarySPHSystem(boundary_zone::BoundaryZone; - fluid_system::FluidSystem, - buffer_size::Integer, boundary_model, - reference_velocity=nothing, - reference_pressure=nothing, - reference_density=nothing) - (; initial_condition) = boundary_zone +function OpenBoundarySPHSystem(boundary_zones::Union{BoundaryZone, Nothing}...; + fluid_system::FluidSystem, buffer_size::Integer, + boundary_model) + boundary_zones_ = filter(bz -> !isnothing(bz), boundary_zones) + reference_values_ = map(bz -> bz.reference_values, boundary_zones_) - buffer = SystemBuffer(nparticles(initial_condition), buffer_size) + initial_conditions = union((bz.initial_condition for bz in boundary_zones)...) - initial_condition = allocate_buffer(initial_condition, buffer) + buffer = SystemBuffer(nparticles(initial_conditions), buffer_size) - NDIMS = ndims(initial_condition) + initial_conditions = allocate_buffer(initial_conditions, buffer) - pressure = copy(initial_condition.pressure) - mass = copy(initial_condition.mass) - density = copy(initial_condition.density) - volume = similar(initial_condition.density) + pressure = copy(initial_conditions.pressure) + mass = copy(initial_conditions.mass) + density = copy(initial_conditions.density) + volume = similar(initial_conditions.density) - if !(reference_velocity isa Function || isnothing(reference_velocity) || - (reference_velocity isa Vector && length(reference_velocity) == NDIMS)) - throw(ArgumentError("`reference_velocity` must be either a function mapping " * - "each particle's coordinates and time to its velocity, " * - "an array where the ``i``-th column holds the velocity of particle ``i`` " * - "or, for a constant fluid velocity, a vector of length $NDIMS for a $(NDIMS)D problem holding this velocity")) - else - if reference_velocity isa Function - test_result = reference_velocity(zeros(NDIMS), 0.0) - if length(test_result) != NDIMS - throw(ArgumentError("`reference_velocity` function must be of dimension $NDIMS")) - end - end - reference_velocity_ = wrap_reference_function(reference_velocity, Val(NDIMS)) - end - - if !(reference_pressure isa Function || reference_pressure isa Real || - isnothing(reference_pressure)) - throw(ArgumentError("`reference_pressure` must be either a function mapping " * - "each particle's coordinates and time to its pressure, " * - "a vector holding the pressure of each particle, or a scalar")) - else - if reference_pressure isa Function - test_result = reference_pressure(zeros(NDIMS), 0.0) - if length(test_result) != 1 - throw(ArgumentError("`reference_pressure` function must be a scalar function")) - end - end - reference_pressure_ = wrap_reference_function(reference_pressure, Val(NDIMS)) - end - - if !(reference_density isa Function || reference_density isa Real || - isnothing(reference_density)) - throw(ArgumentError("`reference_density` must be either a function mapping " * - "each particle's coordinates and time to its density, " * - "a vector holding the density of each particle, or a scalar")) - else - if reference_density isa Function - test_result = reference_density(zeros(NDIMS), 0.0) - if length(test_result) != 1 - throw(ArgumentError("`reference_density` function must be a scalar function")) - end - end - reference_density_ = wrap_reference_function(reference_density, Val(NDIMS)) - end - - cache = create_cache_open_boundary(boundary_model, initial_condition, - reference_density, reference_velocity, - reference_pressure) + cache = create_cache_open_boundary(boundary_model, initial_conditions, + reference_values_) fluid_system_index = Ref(0) smoothing_length = initial_smoothing_length(fluid_system) - boundary_candidates = fill(false, nparticles(initial_condition)) + boundary_candidates = fill(false, nparticles(initial_conditions)) fluid_candidates = fill(false, nparticles(fluid_system)) - return OpenBoundarySPHSystem(boundary_model, initial_condition, fluid_system, + boundary_zone_indices = zeros(Int, nparticles(initial_conditions)) + + # Create new `BoundaryZone`s with `reference_values` set to `nothing` for type stability. + # `reference_values` are only used as API feature to temporarily store the reference values + # in the `BoundaryZone`, but they are not used in the actual simulation. + boundary_zones_new = map(zone -> BoundaryZone(zone.initial_condition, + zone.spanning_set, + zone.zone_origin, + zone.zone_width, + zone.flow_direction, + zone.plane_normal, + nothing, + zone.average_inflow_velocity, + zone.prescribed_density, + zone.prescribed_pressure, + zone.prescribed_velocity), + boundary_zones) + + return OpenBoundarySPHSystem(boundary_model, initial_conditions, fluid_system, fluid_system_index, smoothing_length, mass, density, volume, pressure, boundary_candidates, fluid_candidates, - boundary_zone, reference_velocity_, - reference_pressure_, reference_density_, buffer, cache) + boundary_zone_indices, boundary_zones_new, buffer, cache) end -function create_cache_open_boundary(boundary_model, initial_condition, - reference_density, reference_velocity, - reference_pressure) - ELTYPE = eltype(initial_condition) +function initialize!(system::OpenBoundarySPHSystem, semi) + (; boundary_zones) = system - prescribed_pressure = isnothing(reference_pressure) ? false : true - prescribed_velocity = isnothing(reference_velocity) ? false : true - prescribed_density = isnothing(reference_density) ? false : true + update_boundary_zone_indices!(system, initial_coordinates(system), boundary_zones, semi) - if boundary_model isa BoundaryModelTafuni - return (; prescribed_pressure=prescribed_pressure, - prescribed_density=prescribed_density, - prescribed_velocity=prescribed_velocity) - end + return system +end + +function create_cache_open_boundary(boundary_model, initial_condition, reference_values) + ELTYPE = eltype(initial_condition) + + # Separate `reference_values` into pressure, density and velocity reference values + pressure_reference_values = map(ref -> ref.reference_pressure, reference_values) + density_reference_values = map(ref -> ref.reference_density, reference_values) + velocity_reference_values = map(ref -> ref.reference_velocity, reference_values) - characteristics = zeros(ELTYPE, 3, nparticles(initial_condition)) - previous_characteristics = zeros(ELTYPE, 3, nparticles(initial_condition)) + if boundary_model isa BoundaryModelCharacteristicsLastiwka + characteristics = zeros(ELTYPE, 3, nparticles(initial_condition)) + previous_characteristics = zeros(ELTYPE, 3, nparticles(initial_condition)) - return (; characteristics=characteristics, - previous_characteristics=previous_characteristics, - prescribed_pressure=prescribed_pressure, - prescribed_density=prescribed_density, prescribed_velocity=prescribed_velocity) + return (; characteristics=characteristics, + previous_characteristics=previous_characteristics, + pressure_reference_values=pressure_reference_values, + density_reference_values=density_reference_values, + velocity_reference_values=velocity_reference_values) + else + return (; pressure_reference_values=pressure_reference_values, + density_reference_values=density_reference_values, + velocity_reference_values=velocity_reference_values) + end end timer_name(::OpenBoundarySPHSystem) = "open_boundary" vtkname(system::OpenBoundarySPHSystem) = "open_boundary" -boundary_type_name(::BoundaryZone{ZT}) where {ZT} = string(nameof(ZT)) function Base.show(io::IO, system::OpenBoundarySPHSystem) @nospecialize system # reduce precompilation time print(io, "OpenBoundarySPHSystem{", ndims(system), "}(") - print(io, boundary_type_name(system.boundary_zone)) print(io, ") with ", nparticles(system), " particles") end @@ -205,13 +154,9 @@ function Base.show(io::IO, ::MIME"text/plain", system::OpenBoundarySPHSystem) summary_header(io, "OpenBoundarySPHSystem{$(ndims(system))}") summary_line(io, "#particles", nparticles(system)) summary_line(io, "#buffer_particles", system.buffer.buffer_size) + summary_line(io, "#boundary_zones", length(system.boundary_zones)) summary_line(io, "fluid system", type2string(system.fluid_system)) summary_line(io, "boundary model", type2string(system.boundary_model)) - summary_line(io, "boundary type", boundary_type_name(system.boundary_zone)) - summary_line(io, "prescribed velocity", type2string(system.reference_velocity)) - summary_line(io, "prescribed pressure", type2string(system.reference_pressure)) - summary_line(io, "prescribed density", type2string(system.reference_density)) - summary_line(io, "width", round(system.boundary_zone.zone_width, digits=3)) summary_footer(io) end end @@ -220,13 +165,11 @@ end return ELTYPE end +@inline buffer(system::OpenBoundarySPHSystem) = system.buffer + # The `UpdateCallback` is required to update particle positions between time steps @inline requires_update_callback(system::OpenBoundarySPHSystem) = true -function corresponding_fluid_system(system::OpenBoundarySPHSystem, semi) - return system.fluid_system -end - function smoothing_length(system::OpenBoundarySPHSystem, particle) return system.smoothing_length end @@ -263,19 +206,17 @@ end # This function is called by the `UpdateCallback`, as the integrator array might be modified function update_open_boundary_eachstep!(system::OpenBoundarySPHSystem, v_ode, u_ode, semi, t) + (; boundary_model) = system + u = wrap_u(u_ode, system, semi) v = wrap_v(v_ode, system, semi) @trixi_timeit timer() "check domain" check_domain!(system, v, u, v_ode, u_ode, semi) - # Update density, pressure and velocity based on the characteristic variables. - # See eq. 13-15 in Lastiwka (2009) https://doi.org/10.1002/fld.1971 - @trixi_timeit timer() "update boundary quantities" update_boundary_quantities!(system, - system.boundary_model, - v, u, - v_ode, - u_ode, - semi, t) + # Update density, pressure and velocity based on the specific boundary model + @trixi_timeit timer() "update boundary quantities" begin + update_boundary_quantities!(system, boundary_model, v, u, v_ode, u_ode, semi, t) + end return system end @@ -283,8 +224,7 @@ end update_open_boundary_eachstep!(system, v_ode, u_ode, semi, t) = system function check_domain!(system, v, u, v_ode, u_ode, semi) - (; boundary_zone, boundary_candidates, fluid_candidates) = system - fluid_system = corresponding_fluid_system(system, semi) + (; boundary_zones, boundary_candidates, fluid_candidates, fluid_system) = system u_fluid = wrap_u(u_ode, fluid_system, semi) v_fluid = wrap_v(v_ode, fluid_system, semi) @@ -296,6 +236,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) particle_coords = current_coords(u, system, particle) # Check if boundary particle is outside the boundary zone + boundary_zone = current_boundary_zone(system, particle) if !is_in_boundary_zone(boundary_zone, particle_coords) boundary_candidates[particle] = true end @@ -311,6 +252,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) particle = crossed_boundary_particles[i] particle_new = available_fluid_particles[i] + boundary_zone = current_boundary_zone(system, particle) convert_particle!(system, fluid_system, boundary_zone, particle, particle_new, v, u, v_fluid, u_fluid) end @@ -324,9 +266,11 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) @threaded semi for fluid_particle in each_moving_particle(fluid_system) fluid_coords = current_coords(u_fluid, fluid_system, fluid_particle) - # Check if fluid particle is in boundary zone - if is_in_boundary_zone(boundary_zone, fluid_coords) - fluid_candidates[fluid_particle] = true + # Check if fluid particle is in any boundary zone + for boundary_zone in boundary_zones + if is_in_boundary_zone(boundary_zone, fluid_coords) + fluid_candidates[fluid_particle] = true + end end end @@ -340,7 +284,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) particle = crossed_fluid_particles[i] particle_new = available_boundary_particles[i] - convert_particle!(fluid_system, system, boundary_zone, particle, particle_new, + convert_particle!(fluid_system, system, particle, particle_new, v, u, v_fluid, u_fluid) end @@ -350,39 +294,15 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) # Since particles have been transferred, the neighborhood searches must be updated update_nhs!(semi, u_ode) - return system -end - -# Outflow particle is outside the boundary zone -@inline function convert_particle!(system::OpenBoundarySPHSystem, fluid_system, - boundary_zone::BoundaryZone{OutFlow}, particle, - particle_new, v, u, v_fluid, u_fluid) - deactivate_particle!(system, particle, u) - - return system -end - -# Inflow particle is outside the boundary zone -@inline function convert_particle!(system::OpenBoundarySPHSystem, fluid_system, - boundary_zone::BoundaryZone{InFlow}, particle, - particle_new, v, u, v_fluid, u_fluid) - (; spanning_set) = boundary_zone - - # Activate a new particle in simulation domain - transfer_particle!(fluid_system, system, particle, particle_new, v_fluid, u_fluid, v, u) - - # Reset position of boundary particle - for dim in 1:ndims(system) - u[dim, particle] += spanning_set[1][dim] - end + update_boundary_zone_indices!(system, u, boundary_zones, semi) return system end # Buffer particle is outside the boundary zone @inline function convert_particle!(system::OpenBoundarySPHSystem, fluid_system, - boundary_zone::BoundaryZone{BidirectionalFlow}, - particle, particle_new, v, u, v_fluid, u_fluid) + boundary_zone, particle, particle_new, + v, u, v_fluid, u_fluid) relative_position = current_coords(u, system, particle) - boundary_zone.zone_origin # Check if particle is in- or outside the fluid domain. @@ -406,8 +326,7 @@ end # Fluid particle is in boundary zone @inline function convert_particle!(fluid_system::FluidSystem, system, - boundary_zone, particle, particle_new, - v, u, v_fluid, u_fluid) + particle, particle_new, v, u, v_fluid, u_fluid) # Activate particle in boundary zone transfer_particle!(system, fluid_system, particle, particle_new, v, u, v_fluid, u_fluid) @@ -457,32 +376,6 @@ function write_u0!(u0, system::OpenBoundarySPHSystem) return u0 end -wrap_reference_function(::Nothing, ::Val) = nothing - -function wrap_reference_function(function_::Function, ::Val) - # Already a function - return function_ -end - -# Name the function so that the summary box does know which kind of function this is -function wrap_reference_function(constant_scalar_::Number, ::Val) - return constant_scalar(coords, t) = constant_scalar_ -end - -# For vectors and tuples -# Name the function so that the summary box does know which kind of function this is -function wrap_reference_function(constant_vector_, ::Val{NDIMS}) where {NDIMS} - return constant_vector(coords, t) = SVector{NDIMS}(constant_vector_) -end - -function reference_value(value::Function, quantity, position, t) - return value(position, t) -end - -# This method is used when extrapolating quantities from the domain -# instead of using the method of characteristics -reference_value(value::Nothing, quantity, position, t) = quantity - # To account for boundary effects in the viscosity term of the RHS, use the viscosity model # of the neighboring particle systems. @inline function viscosity_model(system::OpenBoundarySPHSystem, diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index 70dfd0cc72..a20ed70ac1 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -245,6 +245,8 @@ end return ndims(system) + 2 end +@inline buffer(system::EntropicallyDampedSPHSystem) = system.buffer + system_correction(system::EntropicallyDampedSPHSystem) = system.correction @inline function current_pressure(v, system::EntropicallyDampedSPHSystem, particle) diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index dda2ead5f1..46a51cdb8c 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -238,6 +238,8 @@ end return ndims(system) + 1 end +@inline buffer(system::WeaklyCompressibleSPHSystem) = system.buffer + system_correction(system::WeaklyCompressibleSPHSystem) = system.correction @inline function current_velocity(v, system::WeaklyCompressibleSPHSystem) diff --git a/src/schemes/solid/discrete_element_method/system.jl b/src/schemes/solid/discrete_element_method/system.jl index bc93aa8c2f..c2b27ca75b 100644 --- a/src/schemes/solid/discrete_element_method/system.jl +++ b/src/schemes/solid/discrete_element_method/system.jl @@ -37,7 +37,6 @@ struct DEMSystem{NDIMS, ELTYPE <: Real, IC, ARRAY1D, ST, CM} <: SolidSystem{NDIM acceleration :: SVector{NDIMS, ELTYPE} source_terms :: ST contact_model :: CM - buffer :: Nothing function DEMSystem(initial_condition, contact_model; damping_coefficient=0.0001, acceleration=ntuple(_ -> 0.0, @@ -65,7 +64,7 @@ struct DEMSystem{NDIMS, ELTYPE <: Real, IC, ARRAY1D, ST, CM} <: SolidSystem{NDIM typeof(mass), typeof(source_terms), typeof(contact_model)}(initial_condition, mass, radius, damping_coefficient, acceleration_, source_terms, - contact_model, nothing) + contact_model) end end diff --git a/src/schemes/solid/total_lagrangian_sph/system.jl b/src/schemes/solid/total_lagrangian_sph/system.jl index c0ef99b30a..60fc7a3990 100644 --- a/src/schemes/solid/total_lagrangian_sph/system.jl +++ b/src/schemes/solid/total_lagrangian_sph/system.jl @@ -78,7 +78,6 @@ struct TotalLagrangianSPHSystem{BM, NDIMS, ELTYPE <: Real, IC, ARRAY1D, ARRAY2D, penalty_force :: PF viscosity :: V source_terms :: ST - buffer :: Nothing end function TotalLagrangianSPHSystem(initial_condition, @@ -123,7 +122,7 @@ function TotalLagrangianSPHSystem(initial_condition, n_moving_particles, young_modulus, poisson_ratio, lame_lambda, lame_mu, smoothing_kernel, smoothing_length, acceleration_, boundary_model, - penalty_force, viscosity, source_terms, nothing) + penalty_force, viscosity, source_terms) end function Base.show(io::IO, system::TotalLagrangianSPHSystem) diff --git a/src/util.jl b/src/util.jl index 88be2096a2..48f5fe4be6 100644 --- a/src/util.jl +++ b/src/util.jl @@ -11,6 +11,17 @@ end @inline foreach_noalloc(func, collection::Tuple{}) = nothing +# Returns `functions[index](args...)`, but in a type-stable way for a heterogeneous tuple `functions` +@inline function apply_ith_function(functions, index, args...) + if index == 1 + # Found the function to apply, apply it and return + return first(functions)(args...) + end + + # Process remaining functions + apply_ith_function(Base.tail(functions), index - 1, args...) +end + # Print informative message at startup function print_startup_message() s = """ diff --git a/test/examples/examples_fluid.jl b/test/examples/examples_fluid.jl index a84a69317d..54085d2005 100644 --- a/test/examples/examples_fluid.jl +++ b/test/examples/examples_fluid.jl @@ -302,62 +302,52 @@ @test count_rhs_allocations(sol, semi) == 0 end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelLastiwka (WCSPH)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelCharacteristicsLastiwka (WCSPH)" begin @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), joinpath(examples_dir(), "fluid", - "pipe_flow_2d.jl"), - wcsph=true) + "pipe_flow_2d.jl")) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelLastiwka (EDAC)" begin - @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelCharacteristicsLastiwka (EDAC)" begin + @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), wcsph=false, + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl")) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelTafuni (EDAC)" begin - @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelMirroringTafuni (EDAC)" begin + @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), wcsph=false, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), - open_boundary_model=BoundaryModelTafuni(), boundary_type_in=BidirectionalFlow(), - boundary_type_out=BidirectionalFlow(), - reference_density_in=nothing, - reference_pressure_in=nothing, - reference_density_out=nothing, - reference_velocity_out=nothing) + boundary_type_out=BidirectionalFlow()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelTafuni (WCSPH)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelMirroringTafuni (WCSPH)" begin @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), - wcsph=true, sound_speed=20.0, pressure=0.0, - open_boundary_model=BoundaryModelTafuni(), boundary_type_in=BidirectionalFlow(), - boundary_type_out=BidirectionalFlow(), - reference_density_in=nothing, - reference_pressure_in=nothing, - reference_density_out=nothing, - reference_pressure_out=nothing, - reference_velocity_out=nothing) + boundary_type_out=BidirectionalFlow()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end @trixi_testset "fluid/pipe_flow_2d.jl - steady state reached (`dt`)" begin - steady_state_reached = SteadyStateReachedCallback(; dt=0.002, interval_size=10, + steady_state_reached = SteadyStateReachedCallback(; dt=0.002, interval_size=5, reltol=1e-3) @trixi_test_nowarn trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), extra_callback=steady_state_reached, tspan=(0.0, 1.5), viscosity_boundary=nothing) @@ -367,11 +357,12 @@ end @trixi_testset "fluid/pipe_flow_2d.jl - steady state reached (`interval`)" begin - steady_state_reached = SteadyStateReachedCallback(; interval=1, interval_size=10, + steady_state_reached = SteadyStateReachedCallback(; interval=1, interval_size=5, reltol=1e-3) @trixi_test_nowarn trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), extra_callback=steady_state_reached, dtmax=2e-3, tspan=(0.0, 1.5), viscosity_boundary=nothing) @@ -381,7 +372,7 @@ end @trixi_testset "fluid/pipe_flow_3d.jl" begin - @trixi_test_nowarn trixi_include(@__MODULE__, tspan=(0.0, 0.5), + @trixi_test_nowarn trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_3d.jl")) @test sol.retcode == ReturnCode.Success diff --git a/test/examples/gpu.jl b/test/examples/gpu.jl index 038e0a2b89..3c4db90621 100644 --- a/test/examples/gpu.jl +++ b/test/examples/gpu.jl @@ -305,7 +305,7 @@ end end # Test open boundaries and steady-state callback - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelLastiwka (WCSPH)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelCharacteristicsLastiwka (WCSPH)" begin @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, tspan=(0.0f0, 0.5f0), joinpath(examples_dir(), @@ -318,7 +318,7 @@ end @test backend == Main.parallelization_backend end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelLastiwka (EDAC)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelCharacteristicsLastiwka (EDAC)" begin @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, tspan=(0.0f0, 0.5f0), joinpath(examples_dir(), @@ -330,13 +330,13 @@ end @test backend == Main.parallelization_backend end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelTafuni (EDAC)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelMirroringTafuni (EDAC)" begin @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, tspan=(0.0f0, 0.5f0), joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), - open_boundary_model=BoundaryModelTafuni(), + open_boundary_model=BoundaryModelMirroringTafuni(), boundary_type_in=BidirectionalFlow(), boundary_type_out=BidirectionalFlow(), reference_density_in=nothing, @@ -349,16 +349,15 @@ end @test backend == Main.parallelization_backend end - @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelTafuni (WCSPH)" begin + @trixi_testset "fluid/pipe_flow_2d.jl - BoundaryModelMirroringTafuni (WCSPH)" begin @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, tspan=(0.0f0, 0.5f0), joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), wcsph=true, sound_speed=20.0f0, - pressure=0.0f0, - open_boundary_model=BoundaryModelTafuni(; - mirror_method=ZerothOrderMirroring()), + open_boundary_model=BoundaryModelMirroringTafuni(; + mirror_method=ZerothOrderMirroring()), boundary_type_in=BidirectionalFlow(), boundary_type_out=BidirectionalFlow(), reference_density_in=nothing, @@ -373,14 +372,14 @@ end end @trixi_testset "fluid/pipe_flow_2d.jl - steady state reached (`dt`)" begin - steady_state_reached = SteadyStateReachedCallback(; dt=0.002f0, - interval_size=10, + steady_state_reached = SteadyStateReachedCallback(; dt=0.002f0, interval_size=5, reltol=1.0f-3) @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), extra_callback=steady_state_reached, tspan=(0.0f0, 1.5f0), parallelization_backend=Main.parallelization_backend, @@ -393,13 +392,14 @@ end @trixi_testset "fluid/pipe_flow_2d.jl - steady state reached (`interval`)" begin steady_state_reached = SteadyStateReachedCallback(; interval=1, - interval_size=10, + interval_size=5, reltol=1.0f-3) @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), extra_callback=steady_state_reached, + open_boundary_model=BoundaryModelCharacteristicsLastiwka(), dtmax=2.0f-3, tspan=(0.0f0, 1.5f0), parallelization_backend=Main.parallelization_backend, diff --git a/test/general/buffer.jl b/test/general/buffer.jl index 18758590ea..b953d7cfa8 100644 --- a/test/general/buffer.jl +++ b/test/general/buffer.jl @@ -6,15 +6,13 @@ zone = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.2, open_boundary_layers=2, density=1.0, plane_normal=[1.0, 0.0], - boundary_type=InFlow()) + reference_density=1.0, reference_pressure=0.0, + reference_velocity=[0, 0], boundary_type=InFlow()) system = OpenBoundarySPHSystem(zone; fluid_system=FluidSystemMock3(), - reference_density=0.0, reference_pressure=0.0, - reference_velocity=[0, 0], - boundary_model=BoundaryModelLastiwka(), buffer_size=0) + boundary_model=BoundaryModelCharacteristicsLastiwka(), + buffer_size=0) system_buffer = OpenBoundarySPHSystem(zone; buffer_size=5, - reference_density=0.0, reference_pressure=0.0, - reference_velocity=[0, 0], - boundary_model=BoundaryModelLastiwka(), + boundary_model=BoundaryModelCharacteristicsLastiwka(), fluid_system=FluidSystemMock3()) n_particles = nparticles(system) diff --git a/test/general/semidiscretization.jl b/test/general/semidiscretization.jl index dea8c356ab..862f2ba37a 100644 --- a/test/general/semidiscretization.jl +++ b/test/general/semidiscretization.jl @@ -140,7 +140,7 @@ v_ode = vcat(vec(v1), v2) # Avoid `SystemBuffer` barrier - TrixiParticles.each_moving_particle(system::Union{System1, System2}) = TrixiParticles.eachparticle(system) + TrixiParticles.each_moving_particle(system::Union{System1, System2}) = Base.OneTo(nparticles(system)) TrixiParticles.add_source_terms!(dv_ode, v_ode, u_ode, semi, 0.0) diff --git a/test/schemes/boundary/open_boundary/boundary_zone.jl b/test/schemes/boundary/open_boundary/boundary_zone.jl index b14906b630..ce80fced84 100644 --- a/test/schemes/boundary/open_boundary/boundary_zone.jl +++ b/test/schemes/boundary/open_boundary/boundary_zone.jl @@ -1,4 +1,94 @@ @testset verbose=true "Boundary Zone" begin + @testset "`show`" begin + inflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, + plane_normal=(1.0, 0.0), density=1.0, + reference_density=0.0, + reference_pressure=0.0, + reference_velocity=[0.0, 0.0], + open_boundary_layers=4, boundary_type=InFlow()) + + show_compact = "BoundaryZone() with 80 particles" + @test repr(inflow) == show_compact + show_box = """ + ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ BoundaryZone │ + │ ════════════ │ + │ boundary type: ………………………………………… inflow │ + │ #particles: ………………………………………………… 80 │ + │ width: ……………………………………………………………… 0.2 │ + └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" + + @test repr("text/plain", inflow) == show_box + + outflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, + reference_density=0.0, + reference_pressure=0.0, + reference_velocity=[0.0, 0.0], + plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, + boundary_type=OutFlow()) + + show_compact = "BoundaryZone() with 80 particles" + @test repr(outflow) == show_compact + show_box = """ + ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ BoundaryZone │ + │ ════════════ │ + │ boundary type: ………………………………………… outflow │ + │ #particles: ………………………………………………… 80 │ + │ width: ……………………………………………………………… 0.2 │ + └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" + + @test repr("text/plain", outflow) == show_box + end + + @testset verbose=true "Illegal Inputs" begin + plane = ([0.0, 0.0], [0.0, 1.0]) + flow_direction = (1.0, 0.0) + + error_str = "`reference_velocity` must be either a function mapping " * + "each particle's coordinates and time to its velocity, " * + "or, for a constant fluid velocity, a vector of length 2 for a 2D problem holding this velocity" + + reference_velocity = 1.0 + @test_throws ArgumentError(error_str) BoundaryZone(; plane, particle_spacing=0.1, + plane_normal=flow_direction, + density=1.0, + reference_density=0, + reference_pressure=0, + reference_velocity, + open_boundary_layers=2, + boundary_type=InFlow()) + + error_str = "`reference_pressure` must be either a function mapping " * + "each particle's coordinates and time to its pressure, " * + "or a scalar" + + reference_pressure = [1.0, 1.0] + + @test_throws ArgumentError(error_str) BoundaryZone(; plane, particle_spacing=0.1, + plane_normal=flow_direction, + density=1.0, + reference_density=0, + reference_velocity=[1.0, + 1.0], reference_pressure, + open_boundary_layers=2, + boundary_type=InFlow()) + + error_str = "`reference_density` must be either a function mapping " * + "each particle's coordinates and time to its density, " * + "or a scalar" + + reference_density = [1.0, 1.0] + @test_throws ArgumentError(error_str) BoundaryZone(; plane, particle_spacing=0.1, + plane_normal=flow_direction, + density=1.0, + reference_density, + reference_velocity=[1.0, + 1.0], + reference_pressure=0, + open_boundary_layers=2, + boundary_type=InFlow()) + end @testset verbose=true "Boundary Zone 2D" begin particle_spacing = 0.2 open_boundary_layers = 4 @@ -35,8 +125,8 @@ zone_width = open_boundary_layers * boundary_zone.initial_condition.particle_spacing - sign_ = (first(typeof(boundary_zone).parameters) === - TrixiParticles.InFlow) ? -1 : 1 + sign_ = (TrixiParticles.boundary_type_name(boundary_zone) == "inflow") ? + -1 : 1 @test plane_points_1[i] == boundary_zone.zone_origin @test plane_points_2[i] - boundary_zone.zone_origin == @@ -99,8 +189,8 @@ zone_width = open_boundary_layers * boundary_zone.initial_condition.particle_spacing - sign_ = (first(typeof(boundary_zone).parameters) === - TrixiParticles.InFlow) ? -1 : 1 + sign_ = (TrixiParticles.boundary_type_name(boundary_zone) == "inflow") ? + -1 : 1 @test plane_points_1[i] == boundary_zone.zone_origin @test plane_points_2[i] - boundary_zone.zone_origin == @@ -137,7 +227,7 @@ @testset verbose=true "$(TrixiParticles.boundary_type_name(boundary_zone))" for boundary_zone in boundary_zones - perturb_ = first(typeof(boundary_zone).parameters) === TrixiParticles.InFlow ? + perturb_ = TrixiParticles.boundary_type_name(boundary_zone) == "inflow" ? sqrt(eps()) : -sqrt(eps()) @@ -183,7 +273,7 @@ @testset verbose=true "$(TrixiParticles.boundary_type_name(boundary_zone))" for boundary_zone in boundary_zones - perturb_ = first(typeof(boundary_zone).parameters) === TrixiParticles.InFlow ? + perturb_ = TrixiParticles.boundary_type_name(boundary_zone) == "inflow" ? eps() : -eps() point4 = boundary_zone.spanning_set[1] + boundary_zone.zone_origin diff --git a/test/schemes/boundary/open_boundary/characteristic_variables.jl b/test/schemes/boundary/open_boundary/characteristic_variables.jl index 570818f8f9..1920521caa 100644 --- a/test/schemes/boundary/open_boundary/characteristic_variables.jl +++ b/test/schemes/boundary/open_boundary/characteristic_variables.jl @@ -15,7 +15,8 @@ # Prescribed quantities reference_velocity = (pos, t) -> SVector(t, 0.0) reference_pressure = (pos, t) -> 50_000.0 * t - reference_density = (pos, t) -> 1000.0 * t + # Add small offset to avoid "ArgumentError: density must be positive and larger than `eps()`" + reference_density = (pos, t) -> 1000.0 * (t + sqrt(eps())) # Plane points of open boundary plane_points_1 = [[0.0, 0.0], [0.5, -0.5], [1.0, 0.5]] @@ -36,10 +37,12 @@ flow_direction = flow_directions[j] inflow = BoundaryZone(; plane=plane_points, particle_spacing, density, plane_normal=flow_direction, open_boundary_layers, - boundary_type=InFlow()) + boundary_type=InFlow(), reference_velocity, + reference_pressure, reference_density) outflow = BoundaryZone(; plane=plane_points, particle_spacing, density, plane_normal=(-flow_direction), open_boundary_layers, - boundary_type=OutFlow()) + boundary_type=OutFlow(), reference_velocity, + reference_pressure, reference_density) boundary_zones = [ inflow, @@ -49,7 +52,7 @@ @testset "`$(TrixiParticles.boundary_type_name(boundary_zone))`" for boundary_zone in boundary_zones - sign_ = (first(typeof(boundary_zone).parameters) === TrixiParticles.InFlow) ? + sign_ = (TrixiParticles.boundary_type_name(boundary_zone) == "inflow") ? 1 : -1 fluid = extrude_geometry(plane_points; particle_spacing, n_extrude=4, density, pressure, @@ -62,10 +65,7 @@ boundary_system = OpenBoundarySPHSystem(boundary_zone; fluid_system, buffer_size=0, - boundary_model=BoundaryModelLastiwka(), - reference_velocity, - reference_pressure, - reference_density) + boundary_model=BoundaryModelCharacteristicsLastiwka()) semi = Semidiscretization(fluid_system, boundary_system) @@ -101,13 +101,13 @@ v, u, v0_ode, u0_ode, semi, t1) evaluated_vars1 = boundary_system.cache.characteristics - if first(typeof(boundary_zone).parameters) === TrixiParticles.InFlow + if TrixiParticles.boundary_type_name(boundary_zone) == "inflow" @test all(isapprox.(evaluated_vars1[1, :], 0.0)) @test all(isapprox.(evaluated_vars1[2, :], 0.0)) @test all(isapprox.(evaluated_vars1[3, 1:n_influenced], J3(t1))) @test all(isapprox.(evaluated_vars1[3, (n_influenced + 1):end], 0.0)) - elseif first(typeof(boundary_zone).parameters) === TrixiParticles.OutFlow + elseif TrixiParticles.boundary_type_name(boundary_zone) == "outflow" @test all(isapprox.(evaluated_vars1[1, 1:n_influenced], J1(t1))) @test all(isapprox.(evaluated_vars1[2, 1:n_influenced], J2(t1))) @test all(isapprox.(evaluated_vars1[1:2, (n_influenced + 1):end], 0.0)) @@ -121,13 +121,13 @@ v, u, v0_ode, u0_ode, semi, t2) evaluated_vars2 = boundary_system.cache.characteristics - if first(typeof(boundary_zone).parameters) === TrixiParticles.InFlow + if TrixiParticles.boundary_type_name(boundary_zone) == "inflow" @test all(isapprox.(evaluated_vars2[1, :], 0.0)) @test all(isapprox.(evaluated_vars2[2, :], 0.0)) @test all(isapprox.(evaluated_vars2[3, 1:n_influenced], J3(t2))) @test all(isapprox.(evaluated_vars2[3, (n_influenced + 1):end], J3(t1))) - elseif first(typeof(boundary_zone).parameters) === TrixiParticles.OutFlow + elseif TrixiParticles.boundary_type_name(boundary_zone) == "outflow" @test all(isapprox.(evaluated_vars2[1, 1:n_influenced], J1(t2))) @test all(isapprox.(evaluated_vars2[2, 1:n_influenced], J2(t2))) @test all(isapprox.(evaluated_vars2[1, (n_influenced + 1):end], J1(t1))) diff --git a/test/schemes/boundary/open_boundary/mirroring.jl b/test/schemes/boundary/open_boundary/mirroring.jl index 6e3fb9cd6f..ae7afc26bf 100644 --- a/test/schemes/boundary/open_boundary/mirroring.jl +++ b/test/schemes/boundary/open_boundary/mirroring.jl @@ -61,11 +61,12 @@ open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelTafuni(), + boundary_model=BoundaryModelMirroringTafuni(), buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary) TrixiParticles.initialize_neighborhood_searches!(semi) + TrixiParticles.initialize!(open_boundary, semi) v_open_boundary = zero(inflow.initial_condition.velocity) v_fluid = vcat(domain_fluid.velocity, domain_fluid.pressure') @@ -75,9 +76,7 @@ TrixiParticles.extrapolate_values!(open_boundary, FirstOrderMirroring(), v_open_boundary, v_fluid, inflow.initial_condition.coordinates, - domain_fluid.coordinates, semi, 0.0; - prescribed_pressure=false, - prescribed_velocity=false) + domain_fluid.coordinates, semi) # Checked visually in ParaView: # trixi2vtk(fluid_system.initial_condition, filename="fluid", # v=domain_fluid.velocity, p=domain_fluid.pressure) @@ -158,11 +157,12 @@ open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelTafuni(), + boundary_model=BoundaryModelMirroringTafuni(), buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary) TrixiParticles.initialize_neighborhood_searches!(semi) + TrixiParticles.initialize!(open_boundary, semi) v_open_boundary = zero(inflow.initial_condition.velocity) v_fluid = vcat(domain_fluid.velocity, domain_fluid.pressure') @@ -172,9 +172,7 @@ TrixiParticles.extrapolate_values!(open_boundary, FirstOrderMirroring(), v_open_boundary, v_fluid, inflow.initial_condition.coordinates, - domain_fluid.coordinates, semi, 0.0; - prescribed_pressure=false, - prescribed_velocity=false) + domain_fluid.coordinates, semi) # Checked visually in ParaView: # trixi2vtk(fluid_system.initial_condition, filename="fluid", # v=domain_fluid.velocity, p=domain_fluid.pressure) @@ -231,11 +229,12 @@ open_boundary_layers=open_boundary_layers, density=1000.0, particle_spacing, average_inflow_velocity=true) open_boundary_in = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelTafuni(), + boundary_model=BoundaryModelMirroringTafuni(), buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary_in) TrixiParticles.initialize_neighborhood_searches!(semi) + TrixiParticles.initialize!(open_boundary_in, semi) v_open_boundary = zero(inflow.initial_condition.velocity) u_open_boundary = inflow.initial_condition.coordinates @@ -246,10 +245,10 @@ TrixiParticles.extrapolate_values!(open_boundary_in, FirstOrderMirroring(), v_open_boundary, v_fluid, inflow.initial_condition.coordinates, - domain_fluid.coordinates, semi, 0.0) + domain_fluid.coordinates, semi) TrixiParticles.average_velocity!(v_open_boundary, u_open_boundary, open_boundary_in, - inflow, semi) + first(open_boundary_in.boundary_zones), semi) # Since the velocity profile increases linearly in positive x-direction, # we can use the first velocity entry as a representative value. @@ -282,12 +281,13 @@ open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary_out = OpenBoundarySPHSystem(outflow; fluid_system, - boundary_model=BoundaryModelTafuni(), + boundary_model=BoundaryModelMirroringTafuni(), buffer_size=0) # Temporary semidiscretization just to extrapolate the pressure into the outflow system semi = Semidiscretization(fluid_system, open_boundary_out) TrixiParticles.initialize_neighborhood_searches!(semi) + TrixiParticles.initialize!(open_boundary_out, semi) v_open_boundary = zero(outflow.initial_condition.velocity) v_fluid = vcat(domain_fluid.velocity, domain_fluid.pressure') @@ -297,8 +297,7 @@ TrixiParticles.extrapolate_values!(open_boundary_out, mirror_method, v_open_boundary, v_fluid, outflow.initial_condition.coordinates, - domain_fluid.coordinates, semi, 0.0; - prescribed_pressure=false) + domain_fluid.coordinates, semi) plane_in = ([0.0, 0.0], [0.0, domain_size[2]]) @@ -306,12 +305,13 @@ plane_normal=[1.0, 0.0], open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary_in = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelTafuni(), + boundary_model=BoundaryModelMirroringTafuni(), buffer_size=0) # Temporary semidiscretization just to extrapolate the pressure into the outflow system semi = Semidiscretization(fluid_system, open_boundary_in) TrixiParticles.initialize_neighborhood_searches!(semi) + TrixiParticles.initialize!(open_boundary_in, semi) v_open_boundary = zero(inflow.initial_condition.velocity) @@ -320,8 +320,7 @@ TrixiParticles.extrapolate_values!(open_boundary_in, mirror_method, v_open_boundary, v_fluid, inflow.initial_condition.coordinates, - domain_fluid.coordinates, semi, 0.0; - prescribed_pressure=false) + domain_fluid.coordinates, semi) return fluid_system, open_boundary_in, open_boundary_out, v_fluid end @@ -335,7 +334,7 @@ v_fluid = mirror(pressure_func, mirror_method) p_fluid = [TrixiParticles.current_pressure(v_fluid, fluid_system, particle) - for particle in TrixiParticles.active_particles(fluid_system)] + for particle in TrixiParticles.eachparticle(fluid_system)] fluid_system.initial_condition.pressure .= p_fluid open_boundary_in.initial_condition.pressure .= open_boundary_in.pressure diff --git a/test/systems/open_boundary_system.jl b/test/systems/open_boundary_system.jl index 0fdf98f907..3ce14e7634 100644 --- a/test/systems/open_boundary_system.jl +++ b/test/systems/open_boundary_system.jl @@ -1,71 +1,19 @@ @testset verbose=true "`OpenBoundarySPHSystem`" begin - @testset verbose=true "Illegal Inputs" begin - plane = ([0.0, 0.0], [0.0, 1.0]) - flow_direction = (1.0, 0.0) + @testset "`show`" begin # Mock fluid system struct FluidSystemMock2 <: TrixiParticles.FluidSystem{2} end TrixiParticles.initial_smoothing_length(system::FluidSystemMock2) = 1.0 TrixiParticles.nparticles(system::FluidSystemMock2) = 1 - inflow = BoundaryZone(; plane, particle_spacing=0.1, - plane_normal=flow_direction, density=1.0, - open_boundary_layers=2, boundary_type=InFlow()) - - error_str = "`reference_velocity` must be either a function mapping " * - "each particle's coordinates and time to its velocity, " * - "an array where the ``i``-th column holds the velocity of particle ``i`` " * - "or, for a constant fluid velocity, a vector of length 2 for a 2D problem holding this velocity" - - reference_velocity = 1.0 - @test_throws ArgumentError(error_str) OpenBoundarySPHSystem(inflow; - boundary_model=BoundaryModelLastiwka(), - buffer_size=0, - fluid_system=FluidSystemMock2(), - reference_density=0, - reference_pressure=0, - reference_velocity) - - error_str = "`reference_pressure` must be either a function mapping " * - "each particle's coordinates and time to its pressure, " * - "a vector holding the pressure of each particle, or a scalar" - - reference_pressure = [1.0, 1.0] - @test_throws ArgumentError(error_str) OpenBoundarySPHSystem(inflow; - boundary_model=BoundaryModelLastiwka(), - buffer_size=0, - fluid_system=FluidSystemMock2(), - reference_density=0, - reference_velocity=[1.0, - 1.0], - reference_pressure) - - error_str = "`reference_density` must be either a function mapping " * - "each particle's coordinates and time to its density, " * - "a vector holding the density of each particle, or a scalar" - - reference_density = [1.0, 1.0] - @test_throws ArgumentError(error_str) OpenBoundarySPHSystem(inflow; - boundary_model=BoundaryModelLastiwka(), - buffer_size=0, - fluid_system=FluidSystemMock2(), - reference_density, - reference_velocity=[1.0, - 1.0], - reference_pressure=0) - end - @testset "`show`" begin inflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=InFlow()) system = OpenBoundarySPHSystem(inflow; buffer_size=0, - boundary_model=BoundaryModelLastiwka(), - reference_density=0.0, - reference_pressure=0.0, - reference_velocity=[0.0, 0.0], + boundary_model=BoundaryModelCharacteristicsLastiwka(), fluid_system=FluidSystemMock2()) - show_compact = "OpenBoundarySPHSystem{2}(InFlow) with 80 particles" + show_compact = "OpenBoundarySPHSystem{2}() with 80 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -73,28 +21,21 @@ │ ════════════════════════ │ │ #particles: ………………………………………………… 80 │ │ #buffer_particles: ……………………………… 0 │ + │ #boundary_zones: …………………………………… 1 │ │ fluid system: …………………………………………… FluidSystemMock2 │ - │ boundary model: ……………………………………… BoundaryModelLastiwka │ - │ boundary type: ………………………………………… InFlow │ - │ prescribed velocity: ………………………… constant_vector │ - │ prescribed pressure: ………………………… constant_scalar │ - │ prescribed density: …………………………… constant_scalar │ - │ width: ……………………………………………………………… 0.2 │ + │ boundary model: ……………………………………… BoundaryModelCharacteristicsLastiwka │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" @test repr("text/plain", system) == show_box - outflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, + outflow = BoundaryZone(; plane=([5.0, 0.0], [5.0, 1.0]), particle_spacing=0.05, plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=OutFlow()) system = OpenBoundarySPHSystem(outflow; buffer_size=0, - boundary_model=BoundaryModelLastiwka(), - reference_density=0.0, - reference_pressure=0.0, - reference_velocity=[0.0, 0.0], + boundary_model=BoundaryModelMirroringTafuni(), fluid_system=FluidSystemMock2()) - show_compact = "OpenBoundarySPHSystem{2}(OutFlow) with 80 particles" + show_compact = "OpenBoundarySPHSystem{2}() with 80 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -102,13 +43,28 @@ │ ════════════════════════ │ │ #particles: ………………………………………………… 80 │ │ #buffer_particles: ……………………………… 0 │ + │ #boundary_zones: …………………………………… 1 │ + │ fluid system: …………………………………………… FluidSystemMock2 │ + │ boundary model: ……………………………………… BoundaryModelMirroringTafuni │ + └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" + + @test repr("text/plain", system) == show_box + + system = OpenBoundarySPHSystem(outflow, inflow; buffer_size=0, + boundary_model=BoundaryModelMirroringTafuni(), + fluid_system=FluidSystemMock2()) + + show_compact = "OpenBoundarySPHSystem{2}() with 160 particles" + @test repr(system) == show_compact + show_box = """ + ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ OpenBoundarySPHSystem{2} │ + │ ════════════════════════ │ + │ #particles: ………………………………………………… 160 │ + │ #buffer_particles: ……………………………… 0 │ + │ #boundary_zones: …………………………………… 2 │ │ fluid system: …………………………………………… FluidSystemMock2 │ - │ boundary model: ……………………………………… BoundaryModelLastiwka │ - │ boundary type: ………………………………………… OutFlow │ - │ prescribed velocity: ………………………… constant_vector │ - │ prescribed pressure: ………………………… constant_scalar │ - │ prescribed density: …………………………… constant_scalar │ - │ width: ……………………………………………………………… 0.2 │ + │ boundary model: ……………………………………… BoundaryModelMirroringTafuni │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘""" @test repr("text/plain", system) == show_box From 601068987ad3b9aded08215319b57adb74b8e3f0 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:20:05 +0200 Subject: [PATCH 05/15] Implement consistent shifting by Sun et al. 2019 (#888) * Implement consistent shifting by Sun et al. 2019 * Fix tests * Add option to switch between dynamic and static v_max * Fix docs * Add more constructor tests * Fix `dv_shifting` * Add reference * Remove doctest output * Fix ugly spaces * Fix tests * Fix tests * Update NEWS.md * Reformat * Add #879 to NEWS.md * Fix tests * Revise docstring * Make API more flexible * Fix tests --- NEWS.md | 18 +- docs/src/refs.bib | 11 + examples/fluid/lid_driven_cavity_2d.jl | 4 +- .../fluid/periodic_array_of_cylinders_2d.jl | 2 +- examples/fluid/pipe_flow_2d.jl | 2 +- examples/fluid/taylor_green_vortex_2d.jl | 6 +- src/TrixiParticles.jl | 4 +- .../boundary/open_boundary/boundary_zones.jl | 2 +- .../fluid/entropically_damped_sph/rhs.jl | 6 +- src/schemes/fluid/shifting_techniques.jl | 456 ++++++++++++++++-- .../fluid/weakly_compressible_sph/rhs.jl | 32 +- test/examples/examples_fluid.jl | 18 +- test/schemes/fluid/fluid.jl | 1 + test/schemes/fluid/shifting_techniques.jl | 31 ++ test/systems/edac_system.jl | 2 +- 15 files changed, 531 insertions(+), 64 deletions(-) create mode 100644 test/schemes/fluid/shifting_techniques.jl diff --git a/NEWS.md b/NEWS.md index e88701f1a6..37f2cf53c3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -11,14 +11,28 @@ used in the Julia ecosystem. Notable changes will be documented in this file for - API for `OpenBoundarySPHSystem` and `BoundaryZone` changed. It is now possible to pass multiple `BoundaryZone`s to a single `OpenBoundarySPHSystem`. Reference values are now assigned individually to each `BoundaryZone`. (#866) + +- The argument of `TransportVelocityAdami` is now a keyword argument. + `TransportVelocityAdami(1000.0)` now becomes + `TransportVelocityAdami(background_pressure=1000.0)` (#884). + - Combined transport velocity formulation (TVF) and particle shifting technique (PST) into one unified framework. The keyword argument `transport_velocity` now changed to `shifting_technique`. The `ParticleShiftingCallback` has been removed. To use PST, use the `UpdateCallback` - instead, and pass `shifting_technique=ParticleShiftingTechnique()` to the system. + instead, and pass `shifting_technique=ParticleShiftingTechniqueSun2017()` to the system (#884). - Renamed the keyword argument `tlsph` to `place_on_shell` for `ParticlePackingSystem`, - `sample_boundary`, `extrude_geometry`, `RectangularShape`, and `SphereShape`. + `sample_boundary`, `extrude_geometry`, `RectangularShape`, and `SphereShape` (#814). + +- Custom quantity functions passed to `SolutionSavingCallback` or `PostprocessCallback` + that were not using the documented API but were functions of + `(system, v_ode, u_ode, semi, t)` now need to be functions + of `(system, dv_ode, du_ode, v_ode, u_ode, semi, t)` (#879). + +### Features + +- Added consistent particle shifting by Sun et al. (2019) as `ConsistentShiftingSun2019` (#888). ## Version 0.3.1 diff --git a/docs/src/refs.bib b/docs/src/refs.bib index a31156a278..3b853a5250 100644 --- a/docs/src/refs.bib +++ b/docs/src/refs.bib @@ -707,6 +707,17 @@ @article{Sun2018 doi = {10.1016/j.cpc.2017.11.016}, } +@article{Sun2019, + author = {Sun, P. N. and Colagrossi, A. and Marrone, S. and Antuono, M. and Zhang, A. -M.}, + title = {A consistent approach to particle shifting in the δPlus-{SPH} model}, + journal = {Computer Methods in Applied Mechanics and Engineering}, + volume = {348}, + year = {2019}, + issn = {0045-7825}, + pages = {912--934}, + doi = {10.1016/j.cma.2019.01.045}, +} + @Article{Tafuni2018, author = {A. Tafuni and J.M. Dom{\'{\i}}nguez and R. Vacondio and A.J.C. Crespo}, journal = {Computer Methods in Applied Mechanics and Engineering}, diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl index 1b8b15cda4..b377980b18 100644 --- a/examples/fluid/lid_driven_cavity_2d.jl +++ b/examples/fluid/lid_driven_cavity_2d.jl @@ -65,7 +65,7 @@ if wcsph state_equation, smoothing_kernel, pressure_acceleration=TrixiParticles.inter_particle_averaged_pressure, smoothing_length, viscosity=viscosity, - shifting_technique=TransportVelocityAdami(pressure)) + shifting_technique=TransportVelocityAdami(background_pressure=pressure)) else state_equation = nothing density_calculator = ContinuityDensity() @@ -73,7 +73,7 @@ else smoothing_length, density_calculator=density_calculator, sound_speed, viscosity=viscosity, - shifting_technique=TransportVelocityAdami(pressure)) + shifting_technique=TransportVelocityAdami(background_pressure=pressure)) end # ========================================================================================== diff --git a/examples/fluid/periodic_array_of_cylinders_2d.jl b/examples/fluid/periodic_array_of_cylinders_2d.jl index 648a98be11..214f456baa 100644 --- a/examples/fluid/periodic_array_of_cylinders_2d.jl +++ b/examples/fluid/periodic_array_of_cylinders_2d.jl @@ -60,7 +60,7 @@ smoothing_length = 1.2 * particle_spacing smoothing_kernel = SchoenbergQuarticSplineKernel{2}() fluid_system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, smoothing_length, sound_speed, viscosity=ViscosityAdami(; nu), - shifting_technique=TransportVelocityAdami(pressure), + shifting_technique=TransportVelocityAdami(background_pressure=pressure), acceleration=(acceleration_x, 0.0)) # ========================================================================================== diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl index c206f76484..fab2f467d3 100644 --- a/examples/fluid/pipe_flow_2d.jl +++ b/examples/fluid/pipe_flow_2d.jl @@ -83,7 +83,7 @@ if wcsph state_equation, smoothing_kernel, density_diffusion=density_diffusion, smoothing_length, viscosity=viscosity, - shifting_technique=ParticleShiftingTechnique(), + shifting_technique=ParticleShiftingTechnique(v_max_factor=1.5), buffer_size=n_buffer_particles) else # Alternatively the EDAC scheme can be used diff --git a/examples/fluid/taylor_green_vortex_2d.jl b/examples/fluid/taylor_green_vortex_2d.jl index 8a99c455c6..0e13d3738d 100644 --- a/examples/fluid/taylor_green_vortex_2d.jl +++ b/examples/fluid/taylor_green_vortex_2d.jl @@ -86,13 +86,15 @@ if wcsph pressure_acceleration=TrixiParticles.inter_particle_averaged_pressure, smoothing_length, viscosity=ViscosityAdami(; nu), - shifting_technique=TransportVelocityAdami(background_pressure)) + shifting_technique=TransportVelocityAdami(; + background_pressure)) else density_calculator = SummationDensity() fluid_system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, smoothing_length, sound_speed, density_calculator=density_calculator, - shifting_technique=TransportVelocityAdami(background_pressure), + shifting_technique=TransportVelocityAdami(; + background_pressure), viscosity=ViscosityAdami(; nu)) end diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 0aff00a73a..87ce6d43f9 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -68,7 +68,9 @@ export BoundaryZone, InFlow, OutFlow, BidirectionalFlow export InfoCallback, SolutionSavingCallback, DensityReinitializationCallback, PostprocessCallback, StepsizeCallback, UpdateCallback, SteadyStateReachedCallback export ContinuityDensity, SummationDensity -export PenaltyForceGanzenmueller, TransportVelocityAdami, ParticleShiftingTechnique +export PenaltyForceGanzenmueller, TransportVelocityAdami, ParticleShiftingTechnique, + ParticleShiftingTechniqueSun2017, ConsistentShiftingSun2019, + ContinuityEquationTermSun2019, MomentumEquationTermSun2019 export SchoenbergCubicSplineKernel, SchoenbergQuarticSplineKernel, SchoenbergQuinticSplineKernel, GaussianKernel, WendlandC2Kernel, WendlandC4Kernel, WendlandC6Kernel, SpikyKernel, Poly6Kernel diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl index 6738552f4b..00ae2cfe39 100644 --- a/src/schemes/boundary/open_boundary/boundary_zones.jl +++ b/src/schemes/boundary/open_boundary/boundary_zones.jl @@ -80,7 +80,7 @@ There are three ways to specify the actual shape of the boundary zone: or evolved using the characteristic flow variables ([BoundaryModelCharacteristicsLastiwka](@ref BoundaryModelCharacteristicsLastiwka)). # Examples -```jldoctest; output = false +```jldoctest; output=false # 2D plane_points = ([0.0, 0.0], [0.0, 1.0]) plane_normal = [1.0, 0.0] diff --git a/src/schemes/fluid/entropically_damped_sph/rhs.jl b/src/schemes/fluid/entropically_damped_sph/rhs.jl index a4d7d280a4..15d0a0991c 100644 --- a/src/schemes/fluid/entropically_damped_sph/rhs.jl +++ b/src/schemes/fluid/entropically_damped_sph/rhs.jl @@ -53,10 +53,10 @@ function interact!(dv, v_particle_system, u_particle_system, # Extra terms in the momentum equation when using a shifting technique dv_tvf = dv_shifting(shifting_technique(particle_system), - particle_system, neighbor_system, particle, neighbor, + particle_system, neighbor_system, v_particle_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) + particle, neighbor, m_a, m_b, rho_a, rho_b, + pos_diff, distance, grad_kernel, correction) dv_surface_tension = surface_tension_force(surface_tension_a, surface_tension_b, particle_system, neighbor_system, diff --git a/src/schemes/fluid/shifting_techniques.jl b/src/schemes/fluid/shifting_techniques.jl index 98775b3be0..6584e5f93f 100644 --- a/src/schemes/fluid/shifting_techniques.jl +++ b/src/schemes/fluid/shifting_techniques.jl @@ -42,60 +42,402 @@ end # Additional term in the momentum equation due to the shifting technique @inline function dv_shifting(shifting, system, neighbor_system, - particle, neighbor, v_system, v_neighbor_system, + v_system, v_neighbor_system, particle, neighbor, m_a, m_b, rho_a, rho_b, pos_diff, distance, grad_kernel, correction) return zero(grad_kernel) end +# Additional term(s) in the continuity equation due to the shifting technique +function continuity_equation_shifting!(dv, shifting, + particle_system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) + return dv +end + @doc raw""" - ParticleShiftingTechnique() + ParticleShiftingTechnique(; integrate_shifting_velocity=true, + update_everystage=false, + modify_continuity_equation=true, + second_continuity_equation_term=ContinuityEquationTermSun2019(), + momentum_equation_term=MomentumEquationTermSun2019(), + v_max_factor=1, sound_speed_factor=0) + +Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017) +and [Sun et al. (2019)](@cite Sun2019). +The keyword arguments allow to choose between the original methods from the two papers +and variants in between. + +The default values of the keyword arguments provide the version of shifting +that we recommend based on our experiments. +The default values are subject to change in future releases. + +See [Particle Shifting Technique](@ref shifting) for more information on the method. + +We provide the following convenience constructors for common variants of the method: +- [`ParticleShiftingTechniqueSun2017()`](@ref): + Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017). + Shifting is applied as a position correction in a callback after each time step. + No additional terms are added to the momentum or continuity equations. +- [`ConsistentShiftingSun2019()`](@ref): + Consistent Particle Shifting Technique by [Sun et al. (2019)](@cite Sun2019). + Shifting is applied with a shifting velocity in each stage of the time integration. + Additional terms are added to the momentum and continuity equations, most importantly + to guarantee conservation of volume in closed systems, which is not the case for the + original method by [Sun et al. (2017)](@cite Sun2017). + +# Keywords +- `integrate_shifting_velocity`: If `true`, the shifting is applied in each stage of the + time integration method as a shifting velocity that is + added to the physical velocity in the time integration. + If `false`, the shifting is applied as a position correction + in a callback after each time step. +- `update_everystage`: If `true`, the shifting velocity is updated in every stage + of a multi-stage time integration method. + This requires `integrate_shifting_velocity=true`. + If `false`, the shifting velocity is only updated once + per time step in a callback, and the same shifting velocity + is used for all stages. + `update_everystage=false` reduces the computational cost, + but may reduce the stability of the scheme and require + a smaller time step. +- `modify_continuity_equation`: If `true`, the continuity equation is modified to be based + on the transport velocity instead of the physical velocity. + This guarantees conservation of volume in closed systems, + but is unstable at solid wall boundaries, according to our + experiments. + This requires `integrate_shifting_velocity=true`. +- `second_continuity_equation_term`: Additional term to be added to the + continuity equation to solve the stability problems with + the modified continuity equation at solid wall boundaries. + This requires `modify_continuity_equation=true`. + See [`ContinuityEquationTermSun2019`](@ref). +- `momentum_equation_term`: Additional term to be added to the momentum equation + to account for the shifting velocity. + This requires `integrate_shifting_velocity=true`. + See [`MomentumEquationTermSun2019`](@ref). +- `v_max_factor`: Factor to scale the expected maximum velocity used in the + shifting velocity. The maximum expected velocity is computed as + `v_max_factor * max(|v|)`, where `v` is the physical velocity. + As opposed to `sound_speed_factor`, the computed expected + maximum velocity depends on the current flow field + and can change over time. + Only one of `v_max_factor` and `sound_speed_factor` + can be non-zero. +- `sound_speed_factor`: Factor to compute the maximum expected velocity used in the + shifting velocity from the speed of sound. + The maximum expected velocity is computed as + `sound_speed_factor * c`, where `c` is the speed of sound. + Only one of `v_max_factor` and `sound_speed_factor` + can be non-zero. + +!!! warning + The Particle Shifting Technique needs to be disabled close to the free surface + and therefore requires a free surface detection method. This is not yet implemented. + **This technique cannot be used in a free surface simulation.** +""" +struct ParticleShiftingTechnique{integrate_shifting_velocity, + update_everystage, + modify_continuity_equation, + compute_v_max, + ELTYPE, S, M} <: AbstractShiftingTechnique + v_factor :: ELTYPE + second_continuity_equation_term :: S + momentum_equation_term :: M + + function ParticleShiftingTechnique(; integrate_shifting_velocity=true, + update_everystage=false, + modify_continuity_equation=true, + second_continuity_equation_term=ContinuityEquationTermSun2019(), + momentum_equation_term=MomentumEquationTermSun2019(), + v_max_factor=1, sound_speed_factor=0) + if !integrate_shifting_velocity && update_everystage + throw(ArgumentError("ParticleShiftingTechnique: " * + "integrate_shifting_velocity=false requires " * + "update_everystage=false")) + end + + if !integrate_shifting_velocity && modify_continuity_equation + throw(ArgumentError("ParticleShiftingTechnique: " * + "modify_continuity_equation=true requires " * + "integrate_shifting_velocity=true")) + end + + if !modify_continuity_equation && !isnothing(second_continuity_equation_term) + throw(ArgumentError("ParticleShiftingTechnique: " * + "a second_continuity_equation_term requires " * + "modify_continuity_equation=true")) + end + + if !integrate_shifting_velocity && !isnothing(momentum_equation_term) + throw(ArgumentError("ParticleShiftingTechnique: " * + "a momentum_equation_term requires " * + "integrate_shifting_velocity=true")) + end + + if v_max_factor > 0 && sound_speed_factor > 0 + throw(ArgumentError("ParticleShiftingTechnique: " * + "Only one of v_max_factor and sound_speed_factor " * + "can be non-zero")) + end + + if v_max_factor <= 0 && sound_speed_factor <= 0 + throw(ArgumentError("ParticleShiftingTechnique: " * + "One of v_max_factor and sound_speed_factor " * + "must be positive")) + end + + v_factor = max(v_max_factor, sound_speed_factor) + compute_v_max = v_max_factor > 0 + + new{integrate_shifting_velocity, + update_everystage, + modify_continuity_equation, + compute_v_max, typeof(v_factor), + typeof(second_continuity_equation_term), + typeof(momentum_equation_term)}(v_factor, + second_continuity_equation_term, + momentum_equation_term) + end +end + +""" + ParticleShiftingTechniqueSun2017(; kwargs...) Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017). Following the original paper, the callback is applied in every time step and not -in every stage of a multi-stage time integration method to reduce the computational -cost and improve the stability of the scheme. +in every stage of a multi-stage time integration method to reduce the computational cost. -See [Particle Shifting Technique](@ref shifting) for more information on the method. +This is a convenience constructor for: +```jldoctest; output = false +ParticleShiftingTechnique(integrate_shifting_velocity=false, + update_everystage=false, + modify_continuity_equation=false, + second_continuity_equation_term=nothing, + momentum_equation_term=nothing, + v_max_factor=1, sound_speed_factor=0) + +# output +ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing}(1, nothing, nothing) +``` + +See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all available options. + +# Keywords +- `kwargs...`: All keywords are passed to the main constructor. + +# Examples +```jldoctest; output = false +shifting_technique = ParticleShiftingTechniqueSun2017() + +# output +ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing}(1, nothing, nothing) +``` + +!!! warning + The Particle Shifting Technique needs to be disabled close to the free surface + and therefore requires a free surface detection method. This is not yet implemented. + **This technique cannot be used in a free surface simulation.** +""" +function ParticleShiftingTechniqueSun2017(; kwargs...) + return ParticleShiftingTechnique(; integrate_shifting_velocity=false, + update_everystage=false, + modify_continuity_equation=false, + second_continuity_equation_term=nothing, + momentum_equation_term=nothing, + v_max_factor=1, sound_speed_factor=0, + kwargs...) +end + +""" + ConsistentShiftingSun2019(; sound_speed_factor=0.1, kwargs...) + +Consistent Particle Shifting Technique by [Sun et al. (2019)](@cite Sun2019). + +This is a convenience constructor for: +```jldoctest; output = false +ParticleShiftingTechnique(integrate_shifting_velocity=true, + update_everystage=true, + modify_continuity_equation=true, + second_continuity_equation_term=ContinuityEquationTermSun2019(), + momentum_equation_term=MomentumEquationTermSun2019(), + v_max_factor=0, sound_speed_factor=0.1) + +# output +ParticleShiftingTechnique{true, true, true, false, Float64, ContinuityEquationTermSun2019, MomentumEquationTermSun2019}(0.1, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019()) +``` + +See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all available options. + +# Keywords +- `sound_speed_factor`: Factor to compute the maximum expected velocity used in the + shifting velocity from the speed of sound. + The maximum expected velocity is computed as + `sound_speed_factor * c`, where `c` is the speed of sound. + Since the speed of sound is usually chosen as 10 times the maximum + expected velocity in weakly compressible SPH, a value of 0.1 + corresponds to the maximum expected velocity. +- `kwargs...`: All keywords are passed to the main constructor. + +# Examples +```jldoctest; output = false +shifting_technique = ConsistentShiftingSun2019() + +# output +ParticleShiftingTechnique{true, true, true, false, Float64, ContinuityEquationTermSun2019, MomentumEquationTermSun2019}(0.1, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019()) +``` !!! warning The Particle Shifting Technique needs to be disabled close to the free surface and therefore requires a free surface detection method. This is not yet implemented. **This technique cannot be used in a free surface simulation.** """ -struct ParticleShiftingTechnique <: AbstractShiftingTechnique end +function ConsistentShiftingSun2019(; kwargs...) + return ParticleShiftingTechnique(; integrate_shifting_velocity=true, + update_everystage=true, + modify_continuity_equation=true, + second_continuity_equation_term=ContinuityEquationTermSun2019(), + momentum_equation_term=MomentumEquationTermSun2019(), + v_max_factor=0, sound_speed_factor=0.1, + kwargs...) +end -# Zero because PST is applied in a callback -@inline function delta_v(system, ::ParticleShiftingTechnique, particle) +# `ParticleShiftingTechnique{false}` means `integrate_shifting_velocity=false`. +# Zero if PST is applied in a callback as a position correction +# and not with a shifting velocity in the time integration stages +# (which would be `integrate_shifting_velocity=false`). +@inline function delta_v(system, ::ParticleShiftingTechnique{false}, particle) return zero(SVector{ndims(system), eltype(system)}) end -function particle_shifting_from_callback!(u_ode, shifting::ParticleShiftingTechnique, - system, v_ode, semi, dt) - @trixi_timeit timer() "particle shifting" begin - v = wrap_v(v_ode, system, semi) - u = wrap_u(u_ode, system, semi) +""" + MomentumEquationTermSun2019() - # Update the shifting velocity - update_shifting_from_callback!(system, shifting, v, u, v_ode, u_ode, semi) +A term by [Sun et al. (2019)](@cite Sun2019) to be added to the momentum equation. - # Update the particle positions with the shifting velocity - particle_shifting!(u_ode, shifting, system, semi, dt) - end +See [`ParticleShiftingTechnique`](@ref). +""" +struct MomentumEquationTermSun2019 end + +# Additional term in the momentum equation due to the shifting technique +@inline function dv_shifting(shifting::ParticleShiftingTechnique, system, neighbor_system, + v_system, v_neighbor_system, particle, neighbor, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) + return dv_shifting(shifting.momentum_equation_term, system, neighbor_system, + v_system, v_neighbor_system, particle, neighbor, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) end -function update_shifting_from_callback!(system, ::ParticleShiftingTechnique, - v, u, v_ode, u_ode, semi) - (; cache) = system - (; delta_v) = cache +@propagate_inbounds function dv_shifting(::MomentumEquationTermSun2019, + system, neighbor_system, + v_system, v_neighbor_system, + particle, neighbor, m_a, m_b, rho_a, rho_b, + pos_diff, distance, grad_kernel, correction) + delta_v_a = delta_v(system, particle) + delta_v_b = delta_v(neighbor_system, neighbor) - set_zero!(delta_v) + v_a = current_velocity(v_system, system, particle) + v_b = current_velocity(v_neighbor_system, neighbor_system, neighbor) + + tensor_product = v_a * delta_v_a' + v_b * delta_v_b' + return m_b / rho_b * + (tensor_product * grad_kernel + v_a * dot(delta_v_a - delta_v_b, grad_kernel)) +end + +# `ParticleShiftingTechnique{<:Any, <:Any, true}` means `modify_continuity_equation=true` +function continuity_equation_shifting!(dv, + shifting::ParticleShiftingTechnique{<:Any, <:Any, + true}, + system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) + delta_v_diff = delta_v(system, particle) - + delta_v(neighbor_system, neighbor) + + dv[end, particle] += rho_a / rho_b * m_b * dot(delta_v_diff, grad_kernel) + second_continuity_equation_term!(dv, shifting.second_continuity_equation_term, + system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) + + return dv +end + +""" + ContinuityEquationTermSun2019() + +A second term by [Sun et al. (2019)](@cite Sun2019) to be added to the continuity equation +to solve the stability problems with the modified continuity equation at solid wall boundaries. + +See [`ParticleShiftingTechnique`](@ref). +""" +struct ContinuityEquationTermSun2019 end + +@inline function second_continuity_equation_term!(dv, + ::ContinuityEquationTermSun2019, + system, neighbor_system, + particle, neighbor, grad_kernel, + rho_a, rho_b, m_b) + rho_v = rho_a * delta_v(system, particle) + rho_b * delta_v(neighbor_system, neighbor) + + dv[end, particle] += m_b / rho_b * dot(rho_v, grad_kernel) + + return dv +end + +@inline function second_continuity_equation_term!(dv, second_continuity_equation_term, + system, neighbor_system, + particle, neighbor, grad_kernel, + rho_a, rho_b, m_b) + return dv +end + +# `ParticleShiftingTechnique{<:Any, true}` means `update_everystage=true` +function update_shifting!(system, shifting::ParticleShiftingTechnique{<:Any, true}, + v, u, v_ode, u_ode, semi) + update_shifting_inner!(system, shifting, v, u, v_ode, u_ode, semi) +end + +# `ParticleShiftingTechnique{<:Any, false}` means `update_everystage=false` +function update_shifting_from_callback!(system, + shifting::ParticleShiftingTechnique{<:Any, false}, + v_ode, u_ode, semi) + v = wrap_v(v_ode, system, semi) + u = wrap_u(u_ode, system, semi) + + update_shifting_inner!(system, shifting, v, u, v_ode, u_ode, semi) +end + +# `ParticleShiftingTechnique{<:Any, <:Any, <:Any, true}` +# means `compute_v_max=true` +function v_max(shifting::ParticleShiftingTechnique{<:Any, <:Any, <:Any, true}, + v, system) # This has similar performance to `maximum(..., eachparticle(system))`, # but is GPU-compatible. v_max = maximum(x -> sqrt(dot(x, x)), reinterpret(reshape, SVector{ndims(system), eltype(v)}, current_velocity(v, system))) + return shifting.v_factor * v_max +end + +# `ParticleShiftingTechnique{<:Any, <:Any, <:Any, false}` +# means `compute_v_max=false` +function v_max(shifting::ParticleShiftingTechnique{<:Any, <:Any, <:Any, false}, + v, system) + sound_speed = system_sound_speed(system) + + return shifting.v_factor * sound_speed +end + +function update_shifting_inner!(system, shifting::ParticleShiftingTechnique, + v, u, v_ode, u_ode, semi) + (; cache) = system + (; delta_v) = cache + + set_zero!(delta_v) + + v_max_ = v_max(shifting, v, system) # TODO this needs to be adapted to multi-resolution. # Section 3.2 explains what else needs to be changed. @@ -127,7 +469,8 @@ function update_shifting_from_callback!(system, ::ParticleShiftingTechnique, # Eq. 7 in Sun et al. (2017). # According to the paper, CFL * Ma can be rewritten as Δt * v_max / h - # (see p. 29, right above Eq. 9), but this does not work when scaling h. + # (see p. 29, right above Eq. 9), but this does not yield the same amount + # of shifting when scaling h. # When setting CFL * Ma = Δt * v_max / (2 * Δx), PST works as expected # for both small and large smoothing length factors. # We need to scale @@ -135,7 +478,7 @@ function update_shifting_from_callback!(system, ::ParticleShiftingTechnique, # - linearly with the particle spacing, # - linearly with the time step. # See https://github.com/trixi-framework/TrixiParticles.jl/pull/834. - delta_v_ = -v_max * (2 * h)^2 / (2 * dx) * (1 + R * (kernel / Wdx)^n) * + delta_v_ = -v_max_ * (2 * h)^2 / (2 * dx) * (1 + R * (kernel / Wdx)^n) * m_b / (rho_a + rho_b) * grad_kernel # Write into the buffer @@ -148,7 +491,28 @@ function update_shifting_from_callback!(system, ::ParticleShiftingTechnique, return system end -function particle_shifting!(u_ode, ::ParticleShiftingTechnique, system, semi, dt) +# `ParticleShiftingTechnique{<:Any, false}` means `update_everystage=false`. +# Only update shifting from callback if `update_everystage=false`. +# Only apply shifting from callback if PST is to be applied in a callback +# (`integrate_shifting_velocity=false`), but this also requires `update_everystage=false`. +function particle_shifting_from_callback!(u_ode, + shifting::ParticleShiftingTechnique{<:Any, false}, + system, v_ode, semi, dt) + @trixi_timeit timer() "particle shifting" begin + # Update the shifting velocity + update_shifting_from_callback!(system, shifting, v_ode, u_ode, semi) + + # Update the particle positions with the shifting velocity + apply_particle_shifting!(u_ode, shifting, system, semi, dt) + end +end + +# `ParticleShiftingTechnique{false}` means `integrate_shifting_velocity=false`. +# Only apply shifting from callback if PST is to be applied in a callback +# and not with a shifting velocity in the time integration stages +# (which would be `integrate_shifting_velocity=false`). +function apply_particle_shifting!(u_ode, ::ParticleShiftingTechnique{false}, + system, semi, dt) (; cache) = system (; delta_v) = cache @@ -164,30 +528,45 @@ function particle_shifting!(u_ode, ::ParticleShiftingTechnique, system, semi, dt return u end +function apply_particle_shifting!(u_ode, ::ParticleShiftingTechnique{true}, + system, semi, dt) + return u_ode +end + """ - TransportVelocityAdami(background_pressure::Real) + TransportVelocityAdami(; background_pressure::Real, modify_continuity_equation=false) Transport Velocity Formulation (TVF) by [Adami et al. (2013)](@cite Adami2013) to suppress pairing and tensile instability. See [TVF](@ref transport_velocity_formulation) for more details of the method. -# Arguments +# Keywords - `background_pressure`: Background pressure. Suggested is a background pressure which is on the order of the reference pressure. +- `modify_continuity_equation`: If `true`, the continuity equation is modified to be based + on the transport velocity instead of the physical velocity. + This guarantees conservation of volume in closed systems, + but is unstable at solid wall boundaries, according to our + experiments. !!! warning The Transport Velocity Formulation needs to be disabled close to the free surface and therefore requires a free surface detection method. This is not yet implemented. **This technique cannot be used in a free surface simulation.** """ -struct TransportVelocityAdami{T <: Real} <: AbstractShiftingTechnique +struct TransportVelocityAdami{modify_continuity_equation, T <: Real} <: + AbstractShiftingTechnique background_pressure::T + + function TransportVelocityAdami(; background_pressure, modify_continuity_equation=false) + new{modify_continuity_equation, typeof(background_pressure)}(background_pressure) + end end -@inline function dv_shifting(::TransportVelocityAdami, system, neighbor_system, - particle, neighbor, v_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) +@propagate_inbounds function dv_shifting(::TransportVelocityAdami, system, neighbor_system, + v_system, v_neighbor_system, particle, neighbor, + m_a, m_b, rho_a, rho_b, pos_diff, distance, + grad_kernel, correction) v_a = current_velocity(v_system, system, particle) delta_v_a = delta_v(system, particle) @@ -209,6 +588,17 @@ end distance, grad_kernel, correction) end +function continuity_equation_shifting!(dv, shifting::TransportVelocityAdami{true}, + particle_system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) + delta_v_diff = delta_v(particle_system, particle) - + delta_v(neighbor_system, neighbor) + + dv[end, particle] += rho_a / rho_b * m_b * dot(delta_v_diff, grad_kernel) + + return dv +end + function update_shifting!(system, shifting::TransportVelocityAdami, v, u, v_ode, u_ode, semi) (; cache, correction) = system diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl index b9cd27f079..1b0a577429 100644 --- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl +++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl @@ -71,11 +71,11 @@ function interact!(dv, v_particle_system, u_particle_system, grad_kernel) # Extra terms in the momentum equation when using a shifting technique - dv_tvf = dv_shifting(shifting_technique(particle_system), - particle_system, neighbor_system, particle, neighbor, - v_particle_system, v_neighbor_system, - m_a, m_b, rho_a, rho_b, pos_diff, distance, - grad_kernel, correction) + dv_tvf = @inbounds dv_shifting(shifting_technique(particle_system), + particle_system, neighbor_system, + v_particle_system, v_neighbor_system, + particle, neighbor, m_a, m_b, rho_a, rho_b, + pos_diff, distance, grad_kernel, correction) dv_surface_tension = surface_tension_correction * surface_tension_force(surface_tension_a, surface_tension_b, @@ -96,10 +96,10 @@ function interact!(dv, v_particle_system, u_particle_system, # TODO If variable smoothing_length is used, this should use the neighbor smoothing length # Propagate `@inbounds` to the continuity equation, which accesses particle data - @inbounds continuity_equation!(dv, density_calculator, v_particle_system, + @inbounds continuity_equation!(dv, density_calculator, particle_system, + neighbor_system, v_particle_system, v_neighbor_system, particle, neighbor, - pos_diff, distance, m_b, rho_a, rho_b, - particle_system, neighbor_system, grad_kernel) + pos_diff, distance, m_b, rho_a, rho_b, grad_kernel) end # Debug example # periodic_box = neighborhood_search.periodic_box @@ -113,20 +113,20 @@ end # With 'SummationDensity', density is calculated in wcsph/system.jl:compute_density! @inline function continuity_equation!(dv, density_calculator::SummationDensity, + particle_system, neighbor_system, v_particle_system, v_neighbor_system, particle, neighbor, pos_diff, distance, - m_b, rho_a, rho_b, - particle_system, neighbor_system, grad_kernel) + m_b, rho_a, rho_b, grad_kernel) return dv end # This formulation was chosen to be consistent with the used pressure_acceleration formulations. @propagate_inbounds function continuity_equation!(dv, density_calculator::ContinuityDensity, + particle_system::WeaklyCompressibleSPHSystem, + neighbor_system, v_particle_system, v_neighbor_system, particle, neighbor, pos_diff, distance, - m_b, rho_a, rho_b, - particle_system::WeaklyCompressibleSPHSystem, - neighbor_system, grad_kernel) + m_b, rho_a, rho_b, grad_kernel) (; density_diffusion) = particle_system vdiff = current_velocity(v_particle_system, particle_system, particle) - @@ -134,7 +134,7 @@ end dv[end, particle] += rho_a / rho_b * m_b * dot(vdiff, grad_kernel) - # Artificial density diffusion should only be applied to system(s) representing a fluid + # Artificial density diffusion should only be applied to systems representing a fluid # with the same physical properties i.e. density and viscosity. # TODO: shouldn't be applied to particles on the interface (depends on PR #539) if particle_system === neighbor_system @@ -142,6 +142,10 @@ end pos_diff, distance, m_b, rho_a, rho_b, particle_system, grad_kernel) end + + continuity_equation_shifting!(dv, shifting_technique(particle_system), + particle_system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) end @propagate_inbounds function particle_neighbor_pressure(v_particle_system, diff --git a/test/examples/examples_fluid.jl b/test/examples/examples_fluid.jl index 54085d2005..71084aa5e1 100644 --- a/test/examples/examples_fluid.jl +++ b/test/examples/examples_fluid.jl @@ -273,7 +273,7 @@ joinpath(examples_dir(), "fluid", "periodic_channel_2d.jl"), tspan=(0.0, 0.2), - shifting_technique=ParticleShiftingTechnique(), + shifting_technique=ParticleShiftingTechniqueSun2017(), extra_callback=UpdateCallback()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 @@ -284,7 +284,7 @@ joinpath(examples_dir(), "fluid", "periodic_channel_2d.jl"), tspan=(0.0, 0.2), - shifting_technique=TransportVelocityAdami(50_000.0), + shifting_technique=TransportVelocityAdami(background_pressure=50_000.0), extra_callback=UpdateCallback()) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 @@ -295,7 +295,19 @@ joinpath(examples_dir(), "fluid", "periodic_channel_2d.jl"), tspan=(0.0, 0.2), - shifting_technique=ParticleShiftingTechnique(), + shifting_technique=ParticleShiftingTechniqueSun2017(), + pressure_acceleration=tensile_instability_control, + extra_callback=UpdateCallback()) + @test sol.retcode == ReturnCode.Success + @test count_rhs_allocations(sol, semi) == 0 + end + + @trixi_testset "fluid/periodic_channel_2d.jl with consistent PST and TIC" begin + @trixi_test_nowarn trixi_include(@__MODULE__, + joinpath(examples_dir(), "fluid", + "periodic_channel_2d.jl"), + tspan=(0.0, 0.2), + shifting_technique=ConsistentShiftingSun2019(), pressure_acceleration=tensile_instability_control, extra_callback=UpdateCallback()) @test sol.retcode == ReturnCode.Success diff --git a/test/schemes/fluid/fluid.jl b/test/schemes/fluid/fluid.jl index 664325a1ad..c234bb1a3b 100644 --- a/test/schemes/fluid/fluid.jl +++ b/test/schemes/fluid/fluid.jl @@ -4,3 +4,4 @@ include("pressure_acceleration.jl") include("surface_normal_sph.jl") include("surface_tension.jl") include("viscosity.jl") +include("shifting_techniques.jl") diff --git a/test/schemes/fluid/shifting_techniques.jl b/test/schemes/fluid/shifting_techniques.jl new file mode 100644 index 0000000000..20ef2372fa --- /dev/null +++ b/test/schemes/fluid/shifting_techniques.jl @@ -0,0 +1,31 @@ +@testset verbose=true "Shifting Techniques" begin + @testset "Constructors" begin + @test_nowarn TransportVelocityAdami(background_pressure=1.0) + @test_nowarn ParticleShiftingTechniqueSun2017() + pst = @test_nowarn ParticleShiftingTechniqueSun2017(v_max_factor=1.2) + @test pst.v_factor == 1.2 + @test_nowarn ConsistentShiftingSun2019() + pst = @test_nowarn ConsistentShiftingSun2019(sound_speed_factor=0.2) + @test pst.v_factor == 0.2 + + # Can't use both `v_max_factor` and `sound_speed_factor` + @test_throws ArgumentError ParticleShiftingTechnique(v_max_factor=1.0, + sound_speed_factor=0.5) + # At least one of `v_max_factor` and `sound_speed_factor` must be positive + @test_throws ArgumentError ParticleShiftingTechnique(v_max_factor=0.0, + sound_speed_factor=0.0) + # Can't update every stage if not integrating shifting velocity + @test_throws ArgumentError ParticleShiftingTechnique(integrate_shifting_velocity=false, + update_everystage=true) + # Can't modify continuity equation if not integrating shifting velocity + @test_throws ArgumentError ParticleShiftingTechnique(integrate_shifting_velocity=false, + modify_continuity_equation=true) + # Can't modify momentum equation if not integrating shifting velocity + @test_throws ArgumentError ParticleShiftingTechnique(integrate_shifting_velocity=false, + momentum_equation_term=MomentumEquationTermSun2019()) + # Can't use second continuity equation term if not modifying continuity equation + @test_throws ArgumentError ParticleShiftingTechnique(integrate_shifting_velocity=true, + modify_continuity_equation=false, + second_continuity_equation_term=ContinuityEquationTermSun2019()) + end +end diff --git a/test/systems/edac_system.jl b/test/systems/edac_system.jl index ed37b9ea55..8f184fcd20 100644 --- a/test/systems/edac_system.jl +++ b/test/systems/edac_system.jl @@ -221,7 +221,7 @@ fluid = rectangular_patch(particle_spacing, (3, 3), seed=1) - transport_velocity = [nothing, TransportVelocityAdami(10000.0)] + transport_velocity = [nothing, TransportVelocityAdami(background_pressure=10000.0)] names = ["No TVF", "TransportVelocityAdami"] @testset "$(names[i])" for i in eachindex(transport_velocity) system = EntropicallyDampedSPHSystem(fluid, smoothing_kernel, From 2d2b43732b9740ab30088d2a7f5619a8699c5ebd Mon Sep 17 00:00:00 2001 From: Marcel Schurer <153067321+marcelschurer@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:06:27 +0200 Subject: [PATCH 06/15] Write metadata to a dedicated JSON file (#737) * Integrate write2json for initial metadata export * Remove old `write_meta_data` parameter from VTK writing functions and callbacks * Add `solver_name` to metadata and format files * update `write2json.jl` to match merge * rename file * update references * rename function * Refactor write2vtk! * formatting * refactor functions * implement suggestions * fix bugs * add add_meta_data! function for `system::ParticlePackingSystem` * relocate `add_meta_data!` to `system.jl` * formatting * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * dispatch `FluidSystem` * Write one unified metadata file for all systems * improve JSON-file structure * improve consistency * Enhance structure of the JSON-File * Fix `rho0` in `AkinciFreeSurfaceCorrection` * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Refactor metadata handling * Add settable filename * Add filename display to SolutionSavingCallback output * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * add function for `ParticlePackingSystem` * improve structure * Formatting Code * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * fix failing tests * Rename 'tlsph' to 'place_on_shell' (#814) * rename * format * forgot some * format * naming * forgot some more * fix test * incorporate review comments * format --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> * Combine PST and TVF into a unified framework (#884) * Combine PST and TVF into a unified framework * Require update callback for PST * Fix WCSPH * Update PST only in callback * Fix EDAC * Update docs * Fix alle example files * Fix tests * Fix periodic channel * Fix docs * Update news * Fix tests * Fix example file * improve structure and fixing problems * add `shifting_technique` to `io.jl` * change filename of the JSON-File * add suggestions * Enhance data handling for `OpenBoundarySPHSystem` * correct formatting issue * Remove autoinfiltrate * rename `number_of_threads` --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Co-authored-by: Sven Berger Co-authored-by: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> --- examples/fluid/falling_water_spheres_2d.jl | 2 +- examples/fsi/falling_sphere_3d.jl | 1 - examples/fsi/falling_spheres_2d.jl | 3 +- examples/n_body/n_body_system.jl | 6 +- src/TrixiParticles.jl | 2 +- src/callbacks/post_process.jl | 16 +- src/callbacks/solution_saving.jl | 26 +- src/io/io.jl | 319 +++++++++++++++++++ src/io/write_vtk.jl | 137 +++----- src/preprocessing/particle_packing/system.jl | 5 +- 10 files changed, 384 insertions(+), 133 deletions(-) diff --git a/examples/fluid/falling_water_spheres_2d.jl b/examples/fluid/falling_water_spheres_2d.jl index 68eae04574..667264d668 100644 --- a/examples/fluid/falling_water_spheres_2d.jl +++ b/examples/fluid/falling_water_spheres_2d.jl @@ -94,7 +94,7 @@ ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=1000) saving_callback = SolutionSavingCallback(dt=0.01, output_directory="out", - prefix="", write_meta_data=true) + prefix="") callbacks = CallbackSet(info_callback, saving_callback) diff --git a/examples/fsi/falling_sphere_3d.jl b/examples/fsi/falling_sphere_3d.jl index 53a709b243..0f49aa7b29 100644 --- a/examples/fsi/falling_sphere_3d.jl +++ b/examples/fsi/falling_sphere_3d.jl @@ -16,5 +16,4 @@ trixi_include(@__MODULE__, solid_smoothing_kernel=WendlandC2Kernel{3}(), sphere_type=RoundSphere(), output_directory="out", prefix="", - write_meta_data=false, # Files with meta data can't be read by meshio tspan=(0.0, 1.0), abstol=1e-6, reltol=1e-3) diff --git a/examples/fsi/falling_spheres_2d.jl b/examples/fsi/falling_spheres_2d.jl index 8c76b763eb..d59766befe 100644 --- a/examples/fsi/falling_spheres_2d.jl +++ b/examples/fsi/falling_spheres_2d.jl @@ -124,8 +124,7 @@ semi = Semidiscretization(fluid_system, boundary_system, solid_system_1, solid_s ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=50) -saving_callback = SolutionSavingCallback(dt=0.02, output_directory="out", prefix="", - write_meta_data=true) +saving_callback = SolutionSavingCallback(dt=0.02, output_directory="out", prefix="") callbacks = CallbackSet(info_callback, saving_callback) diff --git a/examples/n_body/n_body_system.jl b/examples/n_body/n_body_system.jl index 9493f2333f..b877059a41 100644 --- a/examples/n_body/n_body_system.jl +++ b/examples/n_body/n_body_system.jl @@ -105,7 +105,7 @@ end TrixiParticles.vtkname(system::NBodySystem) = "n-body" -function TrixiParticles.write2vtk!(vtk, v, u, t, system::NBodySystem; write_meta_data=true) +function TrixiParticles.write2vtk!(vtk, v, u, t, system::NBodySystem) (; mass) = system vtk["velocity"] = v @@ -114,6 +114,10 @@ function TrixiParticles.write2vtk!(vtk, v, u, t, system::NBodySystem; write_meta return vtk end +function TrixiParticles.add_system_data!(system_data, system::NBodySystem) + return system_data +end + function Base.show(io::IO, system::NBodySystem) print(io, "NBodySystem{", ndims(system), "}() with ") print(io, TrixiParticles.nparticles(system), " particles") diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 87ce6d43f9..2dffe6a847 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -56,8 +56,8 @@ include("callbacks/callbacks.jl") # included separately. `gpu.jl` in turn depends on the semidiscretization type. include("general/semidiscretization.jl") include("general/gpu.jl") -include("io/io.jl") include("preprocessing/preprocessing.jl") +include("io/io.jl") include("visualization/recipes_plots.jl") export Semidiscretization, semidiscretize, restart_with! diff --git a/src/callbacks/post_process.jl b/src/callbacks/post_process.jl index 808b2eafc3..40b31998fa 100644 --- a/src/callbacks/post_process.jl +++ b/src/callbacks/post_process.jl @@ -266,7 +266,7 @@ function (pp::PostprocessCallback)(integrator) if isfinished(integrator) || (pp.write_file_interval > 0 && backup_condition(pp, integrator)) - write_postprocess_callback(pp) + write_postprocess_callback(pp, integrator) end # Tell OrdinaryDiffEq that `u` has not been modified @@ -285,13 +285,14 @@ end end # After the simulation has finished, this function is called to write the data to a JSON file -function write_postprocess_callback(pp::PostprocessCallback) +function write_postprocess_callback(pp::PostprocessCallback, integrator) isempty(pp.data) && return mkpath(pp.output_directory) data = Dict{String, Any}() - write_meta_data!(data, pp.git_hash[]) + data["meta"] = create_meta_data_dict(pp, integrator) + prepare_series_data!(data, pp) time_stamp = "" @@ -341,15 +342,6 @@ function create_series_dict(values, times, system_name="") "time" => times) end -function write_meta_data!(data, git_hash) - meta_data = Dict("solver_name" => "TrixiParticles.jl", - "solver_version" => git_hash, - "julia_version" => string(VERSION)) - - data["meta"] = meta_data - return data -end - function write_csv(abs_file_path, data) times = Float64[] diff --git a/src/callbacks/solution_saving.jl b/src/callbacks/solution_saving.jl index 39a0925210..0d017249c0 100644 --- a/src/callbacks/solution_saving.jl +++ b/src/callbacks/solution_saving.jl @@ -2,8 +2,7 @@ SolutionSavingCallback(; interval::Integer=0, dt=0.0, save_times=Array{Float64, 1}([]), save_initial_solution=true, save_final_solution=true, output_directory="out", append_timestamp=false, prefix="", - verbose=false, write_meta_data=true, max_coordinates=2^15, - custom_quantities...) + verbose=false, max_coordinates=2^15, custom_quantities...) Callback to save the current numerical solution in VTK format in regular intervals. @@ -26,9 +25,8 @@ To ignore a custom quantity for a specific system, return `nothing`. - `save_final_solution=true`: Save the final solution. - `output_directory="out"`: Directory to save the VTK files. - `append_timestamp=false`: Append current timestamp to the output directory. -- 'prefix=""': Prefix added to the filename. +- `prefix=""`: Prefix added to the filename. - `custom_quantities...`: Additional user-defined quantities. -- `write_meta_data=true`: Write meta data. - `verbose=false`: Print to standard IO when a file is written. - `max_coordinates=2^15`: The coordinates of particles will be clipped if their absolute values exceed this threshold. @@ -70,7 +68,6 @@ mutable struct SolutionSavingCallback{I, CQ} save_times :: Vector{Float64} save_initial_solution :: Bool save_final_solution :: Bool - write_meta_data :: Bool verbose :: Bool output_directory :: String prefix :: String @@ -84,8 +81,9 @@ function SolutionSavingCallback(; interval::Integer=0, dt=0.0, save_times=Float64[], save_initial_solution=true, save_final_solution=true, output_directory="out", append_timestamp=false, - prefix="", verbose=false, write_meta_data=true, - max_coordinates=Float64(2^15), custom_quantities...) + prefix="", verbose=false, + max_coordinates=Float64(2^15), + custom_quantities...) if (dt > 0 && interval > 0) || (length(save_times) > 0 && (dt > 0 || interval > 0)) throw(ArgumentError("Setting multiple save times for the same solution " * "callback is not possible. Use either `dt`, `interval` or `save_times`.")) @@ -101,8 +99,8 @@ function SolutionSavingCallback(; interval::Integer=0, dt=0.0, solution_callback = SolutionSavingCallback(interval, Float64.(save_times), save_initial_solution, save_final_solution, - write_meta_data, verbose, output_directory, - prefix, max_coordinates, custom_quantities, + verbose, output_directory, prefix, + max_coordinates, custom_quantities, -1, Ref("UnknownVersion")) if length(save_times) > 0 @@ -133,6 +131,8 @@ function initialize_save_cb!(solution_callback::SolutionSavingCallback, u, t, in solution_callback.latest_saved_iter = -1 solution_callback.git_hash[] = compute_git_hash() + write_meta_data(solution_callback, integrator) + # Save initial solution if solution_callback.save_initial_solution solution_callback(integrator) @@ -151,8 +151,8 @@ end # `affect!` function (solution_callback::SolutionSavingCallback)(integrator) - (; interval, output_directory, custom_quantities, write_meta_data, git_hash, - verbose, prefix, latest_saved_iter, max_coordinates) = solution_callback + (; interval, output_directory, custom_quantities, git_hash, verbose, + prefix, latest_saved_iter, max_coordinates) = solution_callback dvdu_ode = get_du(integrator) vu_ode = integrator.u @@ -176,8 +176,8 @@ function (solution_callback::SolutionSavingCallback)(integrator) @trixi_timeit timer() "save solution" trixi2vtk(dvdu_ode, vu_ode, semi, integrator.t; iter, output_directory, prefix, - write_meta_data, git_hash=git_hash[], - max_coordinates, custom_quantities...) + git_hash=git_hash[], max_coordinates, + custom_quantities...) # Tell OrdinaryDiffEq that `u` has not been modified u_modified!(integrator, false) diff --git a/src/io/io.jl b/src/io/io.jl index 380151bcff..680bbe9ac2 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -1,2 +1,321 @@ include("write_vtk.jl") include("read_vtk.jl") + +# Handle "_" on optional prefix strings +add_underscore_to_optional_prefix(str) = (str === "" ? "" : "$(str)_") +# Same for optional postfix strings +add_underscore_to_optional_postfix(str) = (str === "" ? "" : "_$(str)") + +function write_meta_data(callback::SolutionSavingCallback, integrator) + prefix = callback.prefix + + meta_data = create_meta_data_dict(callback, integrator) + + # write JSON file + output_directory = callback.output_directory + mkpath(output_directory) + json_file = joinpath(output_directory, + "meta" * add_underscore_to_optional_postfix(prefix) * ".json") + + open(json_file, "w") do file + JSON.print(file, meta_data, 2) + end +end + +function create_meta_data_dict(callback, integrator) + git_hash = callback.git_hash + prefix = hasproperty(callback, :prefix) ? callback.prefix : "" + semi = integrator.p + names = system_names(semi.systems) + + meta_data = Dict{String, Any}() + + info = Dict{String, Any}() + add_simulation_info!(info, git_hash, integrator) + meta_data["simulation_info"] = info + + systems = Dict{String, Any}() + foreach_system(semi) do system + idx = system_indices(system, semi) + name = add_underscore_to_optional_prefix(prefix) * names[idx] + + system_data = Dict{String, Any}() + add_system_data!(system_data, system) + + systems[name] = system_data + end + meta_data["system_data"] = systems + + return meta_data +end + +function add_simulation_info!(info, git_hash, integrator) + info["solver_name"] = "TrixiParticles.jl" + info["solver_version"] = git_hash[] + info["julia_version"] = string(VERSION) + + info["time_integrator"] = Dict{String, Any}() + info["time_integrator"]["integrator_type"] = type2string(integrator.alg) + info["time_integrator"]["start_time"] = first(integrator.sol.prob.tspan) + info["time_integrator"]["final_time"] = last(integrator.sol.prob.tspan) + info["time_integrator"]["adaptive"] = integrator.opts.adaptive + if integrator.opts.adaptive + info["time_integrator"]["abstol"] = integrator.opts.abstol + info["time_integrator"]["reltol"] = integrator.opts.reltol + info["time_integrator"]["controller"] = type2string(integrator.opts.controller) + else + info["time_integrator"]["dt"] = integrator.dt + info["time_integrator"]["dt_max"] = integrator.opts.dtmax + end + + info["technical_setup"] = Dict{String, Any}() + info["technical_setup"]["parallelization_backend"] = type2string(integrator.p.parallelization_backend) + info["technical_setup"]["number_of_threads"] = Threads.nthreads() +end + +add_system_data!(system_data, data::Nothing) = system_data + +function add_system_data!(system_data, system::FluidSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = particle_spacing(system, 1) + system_data["density_calculator"] = type2string(system.density_calculator) + system_data["smoothing_kernel"] = type2string(system.smoothing_kernel) + system_data["smoothing_length"] = system.cache.smoothing_length + system_data["acceleration"] = system.acceleration + system_data["sound_speed"] = system_sound_speed(system) + system_data["pressure_acceleration_formulation"] = nameof(system.pressure_acceleration_formulation) + add_system_data!(system_data, shifting_technique(system)) + add_system_data!(system_data, system.surface_tension) + add_system_data!(system_data, system.surface_normal_method) + add_system_data!(system_data, system.viscosity) + add_system_data!(system_data, system.correction) + add_system_data!(system_data, system_state_equation(system)) + if hasfield(typeof(system), :density_diffusion) + add_system_data!(system_data, system.density_diffusion) + end + if hasfield(typeof(system), :alpha) + system_data["alpha"] = system.alpha + end +end + +function add_system_data!(system_data, system::TotalLagrangianSPHSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = particle_spacing(system, 1) + system_data["smoothing_kernel"] = type2string(system.smoothing_kernel) + system_data["smoothing_length"] = system.smoothing_length + system_data["acceleration"] = system.acceleration + add_system_data!(system_data, system.boundary_model) + add_system_data!(system_data, system.viscosity) + add_system_data!(system_data, system.penalty_force) +end + +function add_system_data!(system_data, system::BoundarySPHSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = particle_spacing(system, 1) + system_data["adhesion_coefficient"] = system.adhesion_coefficient + add_system_data!(system_data, system.boundary_model) + add_system_data!(system_data, system.movement) +end + +function add_system_data!(system_data, system::BoundaryDEMSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = particle_spacing(system, 1) + system_data["normal_stiffness"] = system.normal_stiffness +end + +function add_system_data!(system_data, system::DEMSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = particle_spacing(system, 1) + system_data["damping_coefficient"] = system.damping_coefficient + system_data["acceleration"] = system.acceleration + add_system_data!(system_data, system.contact_model) +end + +function add_system_data!(system_data, system::OpenBoundarySPHSystem) + system_data["system_type"] = type2string(system) + system_data["fluid_system_index"] = system.fluid_system_index[] + system_data["smoothing_length"] = system.smoothing_length + add_system_data!(system_data, system.boundary_model) + + system_data["boundary_zones"] = Dict{String, Any}() + for (indice, boundary_zone) in enumerate(system.boundary_zones) + add_system_data!(system_data["boundary_zones"], boundary_zone, indice) + end +end + +function add_system_data!(system_data, system::ParticlePackingSystem) + system_data["system_type"] = type2string(system) + system_data["particle_spacing"] = system.particle_spacing + system_data["smoothing_kernel"] = type2string(system.smoothing_kernel) + system_data["smoothing_length_interpolation"] = system.smoothing_length_interpolation + system_data["background_pressure"] = system.background_pressure + system_data["place_on_shell"] = system.place_on_shell + system_data["shift_length"] = system.shift_length +end + +function add_system_data!(system_data, boundary_model::BoundaryModelDummyParticles) + system_data["boundary_model"] = Dict{String, Any}() + system_data["boundary_model"]["model"] = type2string(boundary_model) + system_data["boundary_model"]["smoothing_kernel"] = type2string(boundary_model.smoothing_kernel) + system_data["boundary_model"]["smoothing_length"] = boundary_model.smoothing_length + system_data["boundary_model"]["density_calculator"] = type2string(boundary_model.density_calculator) + add_system_data!(system_data["boundary_model"], boundary_model.state_equation) + add_system_data!(system_data["boundary_model"], boundary_model.viscosity) + add_system_data!(system_data["boundary_model"], boundary_model.correction) +end + +function add_system_data!(system_data, boundary_model::BoundaryModelMonaghanKajtar) + system_data["boundary_model"] = Dict{String, Any}() + system_data["boundary_model"]["model"] = type2string(boundary_model) + system_data["boundary_model"]["beta"] = boundary_model.beta + system_data["boundary_model"]["K"] = boundary_model.K + add_system_data!(system_data["boundary_model"], boundary_model.viscosity) +end + +function add_system_data!(system_data, boundary_model::BoundaryModelMirroringTafuni) + system_data["boundary_model"] = Dict{String, Any}() + system_data["boundary_model"]["model"] = type2string(boundary_model) + system_data["boundary_model"]["mirror_method"] = type2string(boundary_model.mirror_method) +end + +function add_system_data!(system_data, boundary_model::BoundaryModelCharacteristicsLastiwka) + system_data["boundary_model"] = Dict{String, Any}() + system_data["boundary_model"]["model"] = type2string(boundary_model) + system_data["boundary_model"]["extrapolate_reference_values"] = boundary_model.extrapolate_reference_values +end + +function add_system_data!(system_data, contact_model::HertzContactModel) + system_data["contact_model"] = Dict{String, Any}() + system_data["contact_model"]["model"] = type2string(contact_model) + system_data["contact_model"]["elastic_modulus"] = contact_model.elastic_modulus + system_data["contact_model"]["poissons_ratio"] = contact_model.poissons_ratio +end + +function add_system_data!(system_data, contact_model::LinearContactModel) + system_data["contact_model"] = Dict{String, Any}() + system_data["contact_model"]["model"] = type2string(contact_model) + system_data["contact_model"]["normal_stiffness"] = contact_model.normal_stiffness +end + +function add_system_data!(system_data, state_equation::StateEquationCole) + system_data["state_equation"] = Dict{String, Any}() + system_data["state_equation"]["model"] = type2string(state_equation) + system_data["state_equation"]["reference_density"] = state_equation.reference_density + system_data["state_equation"]["background_pressure"] = state_equation.background_pressure + system_data["state_equation"]["exponent"] = state_equation.exponent +end + +function add_system_data!(system_data, state_equation::StateEquationIdealGas) + system_data["state_equation"] = Dict{String, Any}() + system_data["state_equation"]["model"] = type2string(state_equation) + system_data["state_equation"]["reference_density"] = state_equation.reference_density + system_data["state_equation"]["background_pressure"] = state_equation.background_pressure + system_data["state_equation"]["gamma"] = state_equation.gamma +end + +function add_system_data!(system_data, viscosity::Union{ViscosityAdami, ViscosityMorris}) + system_data["viscosity_model"] = Dict{String, Any}() + system_data["viscosity_model"]["model"] = type2string(viscosity) + system_data["viscosity_model"]["nu"] = viscosity.nu + system_data["viscosity_model"]["epsilon"] = viscosity.epsilon +end + +function add_system_data!(system_data, + viscosity::Union{ViscosityAdamiSGS, ViscosityMorrisSGS}) + system_data["viscosity_model"] = Dict{String, Any}() + system_data["viscosity_model"]["model"] = type2string(viscosity) + system_data["viscosity_model"]["nu"] = viscosity.nu + system_data["viscosity_model"]["C_S"] = viscosity.C_S + system_data["viscosity_model"]["epsilon"] = viscosity.epsilon +end + +function add_system_data!(system_data, viscosity::ArtificialViscosityMonaghan) + system_data["viscosity_model"] = Dict{String, Any}() + system_data["viscosity_model"]["model"] = type2string(viscosity) + system_data["viscosity_model"]["alpha"] = viscosity.alpha + system_data["viscosity_model"]["beta"] = viscosity.beta + system_data["viscosity_model"]["epsilon"] = viscosity.epsilon +end + +function add_system_data!(system_data, + density_diffusion::Union{DensityDiffusionAntuono, + DensityDiffusionMolteniColagrossi}) + system_data["density_diffusion"] = Dict{String, Any}() + system_data["density_diffusion"]["model"] = type2string(density_diffusion) + system_data["density_diffusion"]["delta"] = density_diffusion.delta +end + +function add_system_data!(system_data, density_diffusion::DensityDiffusionFerrari) + system_data["density_diffusion"] = Dict{String, Any}() + system_data["density_diffusion"]["model"] = type2string(density_diffusion) +end + +function add_system_data!(system_data, correction::AkinciFreeSurfaceCorrection) + system_data["correction_method"] = Dict{String, Any}() + system_data["correction_method"]["model"] = type2string(correction) + system_data["correction_method"]["rho0"] = correction.rho0 +end + +function add_system_data!(system_data, + correction::Union{BlendedGradientCorrection, GradientCorrection, + KernelCorrection, MixedKernelGradientCorrection, + ShepardKernelCorrection}) + system_data["correction_method"] = Dict{String, Any}() + system_data["correction_method"]["model"] = type2string(correction) +end + +function add_system_data!(system_data, + surface_tension::Union{CohesionForceAkinci, SurfaceTensionAkinci, + SurfaceTensionMorris, + SurfaceTensionMomentumMorris}) + system_data["surface_tension"] = Dict{String, Any}() + system_data["surface_tension"]["model"] = type2string(surface_tension) + system_data["surface_tension"]["surface_tension_coefficient"] = surface_tension.surface_tension_coefficient +end + +function add_system_data!(system_data, surface_normal_method::ColorfieldSurfaceNormal) + system_data["surface_normal_method"] = Dict{String, Any}() + system_data["surface_normal_method"]["model"] = type2string(surface_normal_method) + system_data["surface_normal_method"]["boundary_contact_threshold"] = surface_normal_method.boundary_contact_threshold + system_data["surface_normal_method"]["ideal_density_threshold"] = surface_normal_method.ideal_density_threshold +end + +function add_system_data!(system_data, boundary_zone::BoundaryZone, indice) + zone_name = "boundary_zone_" * string(indice) + system_data[zone_name] = Dict{String, Any}() + system_data[zone_name]["spanning_set"] = boundary_zone.spanning_set + system_data[zone_name]["zone_origin"] = boundary_zone.zone_origin + system_data[zone_name]["zone_width"] = boundary_zone.zone_width + system_data[zone_name]["flow_direction"] = boundary_zone.flow_direction + system_data[zone_name]["plane_normal"] = boundary_zone.plane_normal + system_data[zone_name]["reference_values"] = boundary_zone.reference_values + system_data[zone_name]["average_inflow_velocity"] = boundary_zone.average_inflow_velocity + system_data[zone_name]["prescribed_density"] = boundary_zone.prescribed_density + system_data[zone_name]["prescribed_pressure"] = boundary_zone.prescribed_pressure + system_data[zone_name]["prescribed_velocity"] = boundary_zone.prescribed_velocity +end + +function add_system_data!(system_data, movement::BoundaryMovement) + system_data["movement"] = Dict{String, Any}() + system_data["movement"]["model"] = type2string(movement) + system_data["movement"]["movement_function"] = type2string(movement.movement_function) + system_data["movement"]["is_moving"] = type2string(movement.is_moving) + system_data["movement"]["moving_particles"] = movement.moving_particles +end + +function add_system_data!(system_data, penalty_force::PenaltyForceGanzenmueller) + system_data["penalty_force"] = Dict{String, Any}() + system_data["penalty_force"]["model"] = type2string(penalty_force) + system_data["penalty_force"]["alpha"] = penalty_force.alpha +end + +function add_system_data!(system_data, shifting_technique::TransportVelocityAdami) + system_data["shifting_technique"] = Dict{String, Any}() + system_data["shifting_technique"]["model"] = type2string(shifting_technique) + system_data["shifting_technique"]["background_pressure"] = shifting_technique.background_pressure +end + +function add_system_data!(system_data, shifting_technique::ParticleShiftingTechnique) + system_data["shifting_technique"] = Dict{String, Any}() + system_data["shifting_technique"]["model"] = type2string(shifting_technique) +end diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index c17410d698..6f0b772189 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -10,7 +10,7 @@ end """ trixi2vtk(vu_ode, semi, t; iter=nothing, output_directory="out", prefix="", - write_meta_data=true, max_coordinates=Inf, custom_quantities...) + max_coordinates=Inf, custom_quantities...) Convert Trixi simulation data to VTK format. @@ -25,7 +25,6 @@ Convert Trixi simulation data to VTK format. separate files. This number is just appended to the filename. - `output_directory="out"`: Output directory path. - `prefix=""`: Prefix for output files. -- `write_meta_data=true`: Write meta data. - `max_coordinates=Inf` The coordinates of particles will be clipped if their absolute values exceed this threshold. - `custom_quantities...`: Additional custom quantities to include in the VTK output. @@ -49,20 +48,19 @@ trixi2vtk(sol.u[end], semi, 0.0, iter=1, my_custom_quantity=kinetic_energy) ``` """ function trixi2vtk(vu_ode, semi, t; iter=nothing, output_directory="out", - prefix="", write_meta_data=true, git_hash=compute_git_hash(), - max_coordinates=Inf, custom_quantities...) + prefix="", git_hash=compute_git_hash(), max_coordinates=Inf, + custom_quantities...) # The first argument is not necessary in most cases. Since it is usually not available to the user, # this API wrapper makes it optional. # Note that custom quantities using the fluid acceleration will not work and return NaN acceleration. return trixi2vtk(fill!(similar(vu_ode), NaN), vu_ode, semi, t; iter, output_directory, - prefix, write_meta_data, git_hash, max_coordinates, - custom_quantities...) + prefix, git_hash, max_coordinates, custom_quantities...) end function trixi2vtk(dvdu_ode, vu_ode, semi, t; iter=nothing, output_directory="out", - prefix="", write_meta_data=true, git_hash=compute_git_hash(), - max_coordinates=Inf, custom_quantities...) + prefix="", git_hash=compute_git_hash(), max_coordinates=Inf, + custom_quantities...) (; systems) = semi # Update quantities that are stored in the systems. These quantities (e.g. pressure) @@ -81,14 +79,14 @@ function trixi2vtk(dvdu_ode, vu_ode, semi, t; iter=nothing, output_directory="ou trixi2vtk(system, dvdu_ode, vu_ode, semi, t, periodic_box; system_name=filenames[system_index], output_directory, iter, prefix, - write_meta_data, git_hash, max_coordinates, custom_quantities...) + git_hash, max_coordinates, custom_quantities...) end end # Convert data for a single TrixiParticle system to VTK format function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; output_directory="out", prefix="", iter=nothing, - system_name=vtkname(system_), write_meta_data=true, max_coordinates=Inf, + system_name=vtkname(system_), max_coordinates=Inf, git_hash=compute_git_hash(), custom_quantities...) mkpath(output_directory) @@ -105,16 +103,12 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) - # handle "_" on optional pre/postfix strings - add_opt_str_pre(str) = (str === "" ? "" : "$(str)_") - add_opt_str_post(str) = (str === nothing ? "" : "_$(str)") - file = joinpath(output_directory, - add_opt_str_pre(prefix) * "$system_name" - * add_opt_str_post(iter)) + add_underscore_to_optional_prefix(prefix) * "$system_name" + * add_underscore_to_optional_postfix(iter)) collection_file = joinpath(output_directory, - add_opt_str_pre(prefix) * "$system_name") + add_underscore_to_optional_prefix(prefix) * "$system_name") # Reset the collection when the iteration is 0 pvd = paraview_collection(collection_file; append=iter > 0) @@ -134,7 +128,7 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; @trixi_timeit timer() "write to vtk" vtk_grid(file, points, cells) do vtk # Dispatches based on the different system types e.g. FluidSystem, TotalLagrangianSPHSystem - write2vtk!(vtk, v, u, t, system, write_meta_data=write_meta_data) + write2vtk!(vtk, v, u, t, system) # Store particle index vtk["index"] = eachparticle(system) @@ -142,12 +136,7 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; vtk["ndims"] = ndims(system) vtk["particle_spacing"] = [particle_spacing(system, particle) - for particle in eachparticle(system)] - - if write_meta_data - vtk["solver_version"] = git_hash - vtk["julia_version"] = string(VERSION) - end + for particle in active_particles(system)] # Extract custom quantities for this system if !isempty(custom_quantities) @@ -285,13 +274,13 @@ function trixi2vtk(initial_condition::InitialCondition; output_directory="out", pressure=pressure, custom_quantities...) end -function write2vtk!(vtk, v, u, t, system; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system) vtk["velocity"] = view(v, 1:ndims(system), :) return vtk end -function write2vtk!(vtk, v, u, t, system::DEMSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::DEMSystem) vtk["velocity"] = view(v, 1:ndims(system), :) vtk["mass"] = [hydrodynamic_mass(system, particle) for particle in eachparticle(system)] @@ -300,7 +289,7 @@ function write2vtk!(vtk, v, u, t, system::DEMSystem; write_meta_data=true) return vtk end -function write2vtk!(vtk, v, u, t, system::FluidSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::FluidSystem) vtk["velocity"] = [current_velocity(v, system, particle) for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) @@ -334,9 +323,14 @@ function write2vtk!(vtk, v, u, t, system::FluidSystem; write_meta_data=true) surface_tension[1:ndims(system), particle] .+= surface_tension_force(surface_tension_a, surface_tension_b, - system, system, particle, - neighbor, pos_diff, - distance, rho_a, rho_b, + system, + system, + particle, + neighbor, + pos_diff, + distance, + rho_a, + rho_b, grad_kernel) end vtk["surface_tension"] = surface_tension @@ -349,41 +343,6 @@ function write2vtk!(vtk, v, u, t, system::FluidSystem; write_meta_data=true) end end - if write_meta_data - vtk["acceleration"] = system.acceleration - vtk["viscosity"] = type2string(system.viscosity) - write2vtk!(vtk, system.viscosity) - vtk["smoothing_kernel"] = type2string(system.smoothing_kernel) - vtk["smoothing_length_factor"] = system.cache.smoothing_length_factor - vtk["density_calculator"] = type2string(system.density_calculator) - - if system isa WeaklyCompressibleSPHSystem - vtk["solver"] = "WCSPH" - - vtk["correction_method"] = type2string(system.correction) - if system.correction isa AkinciFreeSurfaceCorrection - vtk["correction_rho0"] = system.correction.rho0 - end - - if system.state_equation isa StateEquationCole - vtk["state_equation_exponent"] = system.state_equation.exponent - end - - if system.state_equation isa StateEquationIdealGas - vtk["state_equation_gamma"] = system.state_equation.gamma - end - - vtk["state_equation"] = type2string(system.state_equation) - vtk["state_equation_rho0"] = system.state_equation.reference_density - vtk["state_equation_pa"] = system.state_equation.background_pressure - vtk["state_equation_c"] = system.state_equation.sound_speed - vtk["solver"] = "WCSPH" - else - vtk["solver"] = "EDAC" - vtk["sound_speed"] = system.sound_speed - end - end - return vtk end @@ -402,7 +361,7 @@ function write2vtk!(vtk, viscosity::ArtificialViscosityMonaghan) vtk["viscosity_epsilon"] = viscosity.epsilon end -function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem) n_fixed_particles = nparticles(system) - n_moving_particles(system) vtk["velocity"] = [current_velocity(v, system, particle) @@ -416,6 +375,11 @@ function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem; write_meta_d initial_coords(system, particle) for particle in eachparticle(system)] + vtk["lame_lambda"] = system.lame_lambda + vtk["lame_mu"] = system.lame_mu + vtk["young_modulus"] = system.young_modulus + vtk["poisson_ratio"] = system.poisson_ratio + sigma = cauchy_stress(system) vtk["sigma_11"] = sigma[1, 1, :] vtk["sigma_22"] = sigma[2, 2, :] @@ -425,18 +389,10 @@ function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem; write_meta_d vtk["material_density"] = system.material_density - if write_meta_data - vtk["lame_lambda"] = system.lame_lambda - vtk["lame_mu"] = system.lame_mu - vtk["smoothing_kernel"] = type2string(system.smoothing_kernel) - vtk["smoothing_length_factor"] = initial_smoothing_length(system) / - particle_spacing(system, 1) - end - - write2vtk!(vtk, v, u, t, system.boundary_model, system, write_meta_data=write_meta_data) + write2vtk!(vtk, v, u, t, system.boundary_model, system) end -function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem) vtk["velocity"] = [current_velocity(v, system, particle) for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) @@ -447,34 +403,19 @@ function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem; write_meta_data return vtk end -function write2vtk!(vtk, v, u, t, system::BoundarySPHSystem; write_meta_data=true) - write2vtk!(vtk, v, u, t, system.boundary_model, system, write_meta_data=write_meta_data) +function write2vtk!(vtk, v, u, t, system::BoundarySPHSystem) + write2vtk!(vtk, v, u, t, system.boundary_model, system) end -function write2vtk!(vtk, v, u, t, model::Nothing, system; write_meta_data=true) +function write2vtk!(vtk, v, u, t, model::Nothing, system) return vtk end -function write2vtk!(vtk, v, u, t, model::BoundaryModelMonaghanKajtar, system; - write_meta_data=true) - if write_meta_data - vtk["boundary_model"] = "BoundaryModelMonaghanKajtar" - vtk["boundary_spacing_ratio"] = model.beta - vtk["boundary_K"] = model.K - end +function write2vtk!(vtk, v, u, t, model::BoundaryModelMonaghanKajtar, system) + return vtk end -function write2vtk!(vtk, v, u, t, model::BoundaryModelDummyParticles, system; - write_meta_data=true) - if write_meta_data - vtk["boundary_model"] = "BoundaryModelDummyParticles" - vtk["smoothing_kernel"] = type2string(model.smoothing_kernel) - vtk["smoothing_length"] = model.smoothing_length - vtk["density_calculator"] = type2string(model.density_calculator) - vtk["state_equation"] = type2string(model.state_equation) - vtk["viscosity_model"] = type2string(model.viscosity) - end - +function write2vtk!(vtk, v, u, t, model::BoundaryModelDummyParticles, system) vtk["hydrodynamic_density"] = current_density(v, system) vtk["pressure"] = model.pressure @@ -489,6 +430,6 @@ function write2vtk!(vtk, v, u, t, model::BoundaryModelDummyParticles, system; end end -function write2vtk!(vtk, v, u, t, system::BoundaryDEMSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::BoundaryDEMSystem) return vtk end diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl index 100d0f4963..42502e9afc 100644 --- a/src/preprocessing/particle_packing/system.jl +++ b/src/preprocessing/particle_packing/system.jl @@ -216,12 +216,9 @@ end @inline requires_update_callback(system::ParticlePackingSystem) = true -function write2vtk!(vtk, v, u, t, system::ParticlePackingSystem; write_meta_data=true) +function write2vtk!(vtk, v, u, t, system::ParticlePackingSystem) vtk["velocity"] = [advection_velocity(v, system, particle) for particle in eachparticle(system)] - if write_meta_data - vtk["signed_distances"] = system.signed_distances - end end # Skip for fixed systems From 8fa0dd2d4a72a6aa876a95b57d54cc7e505fdac4 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:01:31 +0200 Subject: [PATCH 07/15] Renamings 1 (#903) * Rename "solid" to "structure" * Rename all abstract types to `Abstract*` * Rename "fixed particles" to "clamped particles" * Rename `BoundaryMovement` to `PrescribedMotion` * Reformat * Fix docs * Fix tests * Fix docs * Fix tests * Fix merge * Implement suggestions * Export `AbstractDensityDiffusion` * Link to unexported `AbstractDensityDiffusion` * Fix --- docs/src/systems/boundary.md | 6 +- docs/src/systems/dem.md | 4 +- docs/src/systems/entropically_damped_sph.md | 3 +- docs/src/systems/total_lagrangian_sph.md | 4 +- docs/src/systems/weakly_compressible_sph.md | 3 +- docs/src/tutorials_template/tut_beam.md | 4 +- docs/src/tutorials_template/tut_setup.md | 4 +- examples/fluid/accelerated_tank_2d.jl | 2 +- examples/fluid/lid_driven_cavity_2d.jl | 2 +- examples/fluid/moving_wall_2d.jl | 2 +- examples/fsi/dam_break_gate_2d.jl | 67 ++++++++-------- examples/fsi/dam_break_plate_2d.jl | 65 +++++++-------- examples/fsi/falling_sphere_2d.jl | 2 +- examples/fsi/falling_sphere_3d.jl | 4 +- examples/fsi/falling_spheres_2d.jl | 79 ++++++++++--------- examples/fsi/falling_water_column_2d.jl | 26 +++--- examples/n_body/n_body_system.jl | 2 +- .../oscillating_beam_2d.jl | 32 ++++---- src/TrixiParticles.jl | 7 +- src/callbacks/post_process.jl | 2 +- src/general/{system.jl => abstract_system.jl} | 46 +++++------ src/general/corrections.jl | 15 ++-- src/general/custom_quantities.jl | 17 ++-- src/general/general.jl | 2 +- src/general/gpu.jl | 6 +- src/general/interpolation.jl | 9 ++- src/general/semidiscretization.jl | 58 +++++++------- src/general/smoothing_kernels.jl | 26 +++--- src/io/io.jl | 14 ++-- src/io/write_vtk.jl | 6 +- src/preprocessing/particle_packing/system.jl | 2 +- .../dummy_particles/dummy_particles.jl | 14 ++-- src/schemes/boundary/open_boundary/system.jl | 12 +-- src/schemes/boundary/rhs.jl | 4 +- src/schemes/boundary/system.jl | 36 ++++----- .../fluid/entropically_damped_sph/system.jl | 3 +- src/schemes/fluid/fluid.jl | 40 +++++----- src/schemes/fluid/surface_normal_sph.jl | 26 +++--- src/schemes/fluid/surface_tension.jl | 43 +++++----- .../density_diffusion.jl | 40 +++++----- .../fluid/weakly_compressible_sph/system.jl | 8 +- src/schemes/schemes.jl | 8 +- .../discrete_element_method/contact_models.jl | 6 +- .../discrete_element_method.jl | 0 .../discrete_element_method/rhs.jl | 0 .../discrete_element_method/system.jl | 3 +- .../total_lagrangian_sph/penalty_force.jl | 0 .../total_lagrangian_sph/rhs.jl | 40 +++++----- .../total_lagrangian_sph/system.jl | 33 ++++---- .../total_lagrangian_sph.jl | 0 .../total_lagrangian_sph/viscosity.jl | 0 test/examples/examples.jl | 10 +-- test/examples/gpu.jl | 16 ++-- test/general/buffer.jl | 2 +- test/general/semidiscretization.jl | 25 +++--- test/io/read_vtk.jl | 14 ++-- test/schemes/boundary/dummy_particles/rhs.jl | 18 ++--- test/schemes/schemes.jl | 2 +- .../total_lagrangian_sph/rhs.jl | 6 +- .../total_lagrangian_sph.jl | 0 test/systems/boundary_system.jl | 6 +- test/systems/open_boundary_system.jl | 2 +- test/systems/systems.jl | 2 +- .../{solid_system.jl => tlsph_system.jl} | 4 +- .../validation_lid_driven_cavity_2d.jl | 10 +-- .../plot_oscillating_beam_results.jl | 4 +- .../validation_oscillating_beam_2d.jl | 18 ++--- .../validation_reference_17.json | 8 +- .../validation_reference_33.json | 8 +- .../validation_reference_5.json | 8 +- .../validation_reference_65.json | 8 +- .../validation_reference_9.json | 8 +- 72 files changed, 526 insertions(+), 490 deletions(-) rename examples/{solid => structure}/oscillating_beam_2d.jl (76%) rename src/general/{system.jl => abstract_system.jl} (86%) rename src/schemes/{solid => structure}/discrete_element_method/contact_models.jl (97%) rename src/schemes/{solid => structure}/discrete_element_method/discrete_element_method.jl (100%) rename src/schemes/{solid => structure}/discrete_element_method/rhs.jl (100%) rename src/schemes/{solid => structure}/discrete_element_method/system.jl (98%) rename src/schemes/{solid => structure}/total_lagrangian_sph/penalty_force.jl (100%) rename src/schemes/{solid => structure}/total_lagrangian_sph/rhs.jl (86%) rename src/schemes/{solid => structure}/total_lagrangian_sph/system.jl (93%) rename src/schemes/{solid => structure}/total_lagrangian_sph/total_lagrangian_sph.jl (100%) rename src/schemes/{solid => structure}/total_lagrangian_sph/viscosity.jl (100%) rename test/schemes/{solid => structure}/total_lagrangian_sph/rhs.jl (97%) rename test/schemes/{solid => structure}/total_lagrangian_sph/total_lagrangian_sph.jl (100%) rename test/systems/{solid_system.jl => tlsph_system.jl} (98%) diff --git a/docs/src/systems/boundary.md b/docs/src/systems/boundary.md index f898e50171..3d7e014ba5 100644 --- a/docs/src/systems/boundary.md +++ b/docs/src/systems/boundary.md @@ -9,7 +9,7 @@ ``` ```@docs - BoundaryMovement + PrescribedMotion ``` @@ -61,7 +61,7 @@ We provide six options to compute the boundary density and pressure, determined This option usually yields the best results of the options listed here. 2. (Only relevant for FSI) With [`BernoulliPressureExtrapolation`](@ref), the pressure is extrapolated from the pressure similar to the [`AdamiPressureExtrapolation`](@ref), but a relative velocity-dependent pressure part - is calculated between moving solids and fluids, which increases the boundary pressure in areas prone to + is calculated between moving bodies and fluids, which increases the boundary pressure in areas prone to penetrations. 3. With [`SummationDensity`](@ref), the density is calculated by summation over the neighboring particles, and the pressure is computed from the density with the state equation. @@ -103,7 +103,7 @@ Identical to the pressure ``p_b `` calculated via [`AdamiPressureExtrapolation`] p_b = \frac{\sum_f (p_f + \frac{1}{2} \, \rho_{\text{neighbor}} \left( \frac{ (\mathbf{v}_f - \mathbf{v}_{\text{body}}) \cdot (\mathbf{x}_f - \mathbf{x}_{\text{neighbor}}) }{ \left\| \mathbf{x}_f - \mathbf{x}_{\text{neighbor}} \right\| } \right)^2 \times \text{factor} +\rho_f (\bm{g} - \bm{a}_b) \cdot \bm{r}_{bf}) W(\Vert r_{bf} \Vert, h)}{\sum_f W(\Vert r_{bf} \Vert, h)} ``` where ``\mathbf{v}_f`` is the velocity of the fluid and ``\mathbf{v}_{\text{body}}`` is the velocity of the body. -This adjustment provides a higher boundary pressure for solid bodies moving with a relative velocity to the fluid to prevent penetration. +This adjustment provides a higher boundary pressure for bodies moving with a relative velocity to the fluid to prevent penetration. This modification is original and not derived from any literature source. ```@docs diff --git a/docs/src/systems/dem.md b/docs/src/systems/dem.md index 10cfb00b6f..dced109f4b 100644 --- a/docs/src/systems/dem.md +++ b/docs/src/systems/dem.md @@ -19,12 +19,12 @@ and moments acting upon them. ```@autodocs Modules = [TrixiParticles] -Pages = [joinpath("schemes", "solid", "discrete_element_method", "system.jl")] +Pages = [joinpath("schemes", "structure", "discrete_element_method", "system.jl")] ``` ### Contact Models ```@autodocs Modules = [TrixiParticles] -Pages = [joinpath("schemes", "solid", "discrete_element_method", "contact_models.jl")] +Pages = [joinpath("schemes", "structure", "discrete_element_method", "contact_models.jl")] ``` diff --git a/docs/src/systems/entropically_damped_sph.md b/docs/src/systems/entropically_damped_sph.md index d035fb6fc1..96acbad352 100644 --- a/docs/src/systems/entropically_damped_sph.md +++ b/docs/src/systems/entropically_damped_sph.md @@ -7,7 +7,8 @@ this scheme uses a pressure evolution equation to calculate the pressure ``` which is derived by [Clausen (2013)](@cite Clausen2013). This equation is similar to the continuity equation (first term, see [`ContinuityDensity`](@ref)), but also contains a pressure damping term (second term, similar to density diffusion -see [`DensityDiffusion`](@ref)), which reduces acoustic pressure waves through an entropy-generation mechanism. +see [`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion)), +which reduces acoustic pressure waves through an entropy-generation mechanism. The pressure evolution is discretized with the SPH method by [Ramachandran (2019)](@cite Ramachandran2019) as following: diff --git a/docs/src/systems/total_lagrangian_sph.md b/docs/src/systems/total_lagrangian_sph.md index 7193a28a26..4ae46cead2 100644 --- a/docs/src/systems/total_lagrangian_sph.md +++ b/docs/src/systems/total_lagrangian_sph.md @@ -59,7 +59,7 @@ The term $\bm{f}_a^{PF}$ is an optional penalty force. See e.g. [`PenaltyForceGa ```@autodocs Modules = [TrixiParticles] -Pages = [joinpath("schemes", "solid", "total_lagrangian_sph", "system.jl")] +Pages = [joinpath("schemes", "structure", "total_lagrangian_sph", "system.jl")] ``` ## Penalty Force @@ -100,7 +100,7 @@ where the error vector is defined as ```@autodocs Modules = [TrixiParticles] -Pages = [joinpath("schemes", "solid", "total_lagrangian_sph", "penalty_force.jl")] +Pages = [joinpath("schemes", "structure", "total_lagrangian_sph", "penalty_force.jl")] ``` ## Viscosity diff --git a/docs/src/systems/weakly_compressible_sph.md b/docs/src/systems/weakly_compressible_sph.md index 62a1bc8d0c..2c69f4b3c9 100644 --- a/docs/src/systems/weakly_compressible_sph.md +++ b/docs/src/systems/weakly_compressible_sph.md @@ -58,7 +58,8 @@ by an additional term + \delta h c \sum_{b} V_b \psi_{ab} \cdot \nabla W_{ab}, ``` where ``V_b = m_b / \rho_b`` is the volume of particle ``b`` and ``\psi_{ab}`` depends on -the density diffusion method (see [`DensityDiffusion`](@ref) for available terms). +the density diffusion method (see +[`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) for available terms). Also, ``\rho_a`` denotes the density of particle ``a`` and ``r_{ab} = r_a - r_b`` is the difference of the coordinates, ``v_{ab} = v_a - v_b`` of the velocities of particles ``a`` and ``b``. diff --git a/docs/src/tutorials_template/tut_beam.md b/docs/src/tutorials_template/tut_beam.md index e1e80e7e65..d85b61127d 100644 --- a/docs/src/tutorials_template/tut_beam.md +++ b/docs/src/tutorials_template/tut_beam.md @@ -1,5 +1,5 @@ # Example file ```julia -!!include:examples/solid/oscillating_beam_2d.jl!! +!!include:examples/structure/oscillating_beam_2d.jl!! -``` \ No newline at end of file +``` diff --git a/docs/src/tutorials_template/tut_setup.md b/docs/src/tutorials_template/tut_setup.md index 411ce1592e..98e1153052 100644 --- a/docs/src/tutorials_template/tut_setup.md +++ b/docs/src/tutorials_template/tut_setup.md @@ -240,12 +240,12 @@ and modify them as needed. ### Custom smoothing kernel To implement a custom smoothing kernel, we define a struct extending -`TrixiParticles.SmoothingKernel`. +`TrixiParticles.AbstractSmoothingKernel`. This abstract struct has a type parameter for the number of dimensions, which we set to 2 in this case. ```@example tut_setup -struct MyGaussianKernel <: TrixiParticles.SmoothingKernel{2} end +struct MyGaussianKernel <: TrixiParticles.AbstractSmoothingKernel{2} end ``` This kernel is going to be an implementation of the Gaussian kernel with a cutoff for compact support, which reads diff --git a/examples/fluid/accelerated_tank_2d.jl b/examples/fluid/accelerated_tank_2d.jl index 26a408520e..fe9e1cbae4 100644 --- a/examples/fluid/accelerated_tank_2d.jl +++ b/examples/fluid/accelerated_tank_2d.jl @@ -16,7 +16,7 @@ movement_function(t) = SVector(0.0, 0.5 * 9.81 * t^2) is_moving(t) = true -boundary_movement = BoundaryMovement(movement_function, is_moving) +boundary_movement = PrescribedMotion(movement_function, is_moving) trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "hydrostatic_water_column_2d.jl"), diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl index b377980b18..07c0efd645 100644 --- a/examples/fluid/lid_driven_cavity_2d.jl +++ b/examples/fluid/lid_driven_cavity_2d.jl @@ -83,7 +83,7 @@ lid_movement_function(t) = SVector(VELOCITY_LID * t, 0.0) is_moving(t) = true -lid_movement = BoundaryMovement(lid_movement_function, is_moving) +lid_movement = PrescribedMotion(lid_movement_function, is_moving) boundary_model_cavity = BoundaryModelDummyParticles(cavity.boundary.density, cavity.boundary.mass, diff --git a/examples/fluid/moving_wall_2d.jl b/examples/fluid/moving_wall_2d.jl index 44265a2b99..a3b319372b 100644 --- a/examples/fluid/moving_wall_2d.jl +++ b/examples/fluid/moving_wall_2d.jl @@ -40,7 +40,7 @@ movement_function(t) = SVector(0.5t^2, 0.0) is_moving(t) = t < 1.5 -boundary_movement = BoundaryMovement(movement_function, is_moving, +boundary_movement = PrescribedMotion(movement_function, is_moving, moving_particles=tank.face_indices[2]) # ========================================================================================== diff --git a/examples/fsi/dam_break_gate_2d.jl b/examples/fsi/dam_break_gate_2d.jl index 44adf7bc59..a18147abee 100644 --- a/examples/fsi/dam_break_gate_2d.jl +++ b/examples/fsi/dam_break_gate_2d.jl @@ -63,14 +63,14 @@ gate = RectangularShape(boundary_particle_spacing, movement_function(t) = SVector(0.0, -285.115t^3 + 72.305t^2 + 0.1463t) is_moving(t) = t < 0.1 -gate_movement = BoundaryMovement(movement_function, is_moving) +gate_movement = PrescribedMotion(movement_function, is_moving) # Elastic plate/beam. # The paper is using a thickness of 0.004, which only works properly when a similar fluid # resolution is used. Increase resolution and change to 0.004 to reproduce the results. length_beam = 0.09 thickness = 0.004 * 10 -solid_density = 1161.54 +structure_density = 1161.54 # Young's modulus and Poisson ratio E = 3.5e6 / 10 @@ -78,25 +78,25 @@ nu = 0.45 # The structure starts at the position of the first particle and ends # at the position of the last particle. -solid_particle_spacing = thickness / (n_particles_x - 1) +structure_particle_spacing = thickness / (n_particles_x - 1) -n_particles_y = round(Int, length_beam / solid_particle_spacing) + 1 +n_particles_y = round(Int, length_beam / structure_particle_spacing) + 1 # The bottom layer is sampled separately below. Note that the `RectangularShape` puts the # first particle half a particle spacing away from the shell of the shape, which is -# correct for fluids, but not for solids. We therefore need to pass `place_on_shell=true`. +# correct for fluids, but not for structures. We therefore need to pass `place_on_shell=true`. # # The right end of the plate is 0.2 from the right end of the tank. -plate_position = 0.6 - n_particles_x * solid_particle_spacing -plate = RectangularShape(solid_particle_spacing, +plate_position = 0.6 - n_particles_x * structure_particle_spacing +plate = RectangularShape(structure_particle_spacing, (n_particles_x, n_particles_y - 1), - (plate_position, solid_particle_spacing), - density=solid_density, place_on_shell=true) -fixed_particles = RectangularShape(solid_particle_spacing, - (n_particles_x, 1), (plate_position, 0.0), - density=solid_density, place_on_shell=true) + (plate_position, structure_particle_spacing), + density=structure_density, place_on_shell=true) +clamped_particles = RectangularShape(structure_particle_spacing, + (n_particles_x, 1), (plate_position, 0.0), + density=structure_density, place_on_shell=true) -solid = union(plate, fixed_particles) +structure = union(plate, clamped_particles) # ========================================================================================== # ==== Fluid @@ -128,30 +128,31 @@ boundary_system_tank = BoundarySPHSystem(tank.boundary, boundary_model_tank) boundary_system_gate = BoundarySPHSystem(gate, boundary_model_gate, movement=gate_movement) # ========================================================================================== -# ==== Solid -solid_smoothing_length = sqrt(2) * solid_particle_spacing -solid_smoothing_kernel = WendlandC2Kernel{2}() - -# For the FSI we need the hydrodynamic masses and densities in the solid boundary model -hydrodynamic_densites = fluid_density * ones(size(solid.density)) -hydrodynamic_masses = hydrodynamic_densites * solid_particle_spacing^2 - -boundary_model_solid = BoundaryModelDummyParticles(hydrodynamic_densites, - hydrodynamic_masses, - state_equation=state_equation, - AdamiPressureExtrapolation(), - smoothing_kernel, smoothing_length) - -solid_system = TotalLagrangianSPHSystem(solid, - solid_smoothing_kernel, solid_smoothing_length, - E, nu, boundary_model=boundary_model_solid, - n_fixed_particles=n_particles_x, - acceleration=(0.0, -gravity)) +# ==== Structure +structure_smoothing_length = sqrt(2) * structure_particle_spacing +structure_smoothing_kernel = WendlandC2Kernel{2}() + +# For the FSI we need the hydrodynamic masses and densities in the structure boundary model +hydrodynamic_densites = fluid_density * ones(size(structure.density)) +hydrodynamic_masses = hydrodynamic_densites * structure_particle_spacing^2 + +boundary_model_structure = BoundaryModelDummyParticles(hydrodynamic_densites, + hydrodynamic_masses, + state_equation=state_equation, + AdamiPressureExtrapolation(), + smoothing_kernel, smoothing_length) + +structure_system = TotalLagrangianSPHSystem(structure, + structure_smoothing_kernel, + structure_smoothing_length, + E, nu, boundary_model=boundary_model_structure, + n_clamped_particles=n_particles_x, + acceleration=(0.0, -gravity)) # ========================================================================================== # ==== Simulation semi = Semidiscretization(fluid_system, boundary_system_tank, - boundary_system_gate, solid_system, + boundary_system_gate, structure_system, parallelization_backend=PolyesterBackend()) ode = semidiscretize(semi, tspan) diff --git a/examples/fsi/dam_break_plate_2d.jl b/examples/fsi/dam_break_plate_2d.jl index d4f8b206fb..208bf3377c 100644 --- a/examples/fsi/dam_break_plate_2d.jl +++ b/examples/fsi/dam_break_plate_2d.jl @@ -8,7 +8,7 @@ # https://doi.org/10.1016/j.jfluidstructs.2019.02.002 # # This example simulates a 2D dam break where the collapsing water column impacts -# a flexible elastic plate fixed at its base. +# a flexible elastic plate clamped at its base. # ========================================================================================== using TrixiParticles @@ -44,7 +44,7 @@ tank = RectangularTank(fluid_particle_spacing, initial_fluid_size, tank_size, fl # Elastic plate/beam length_beam = 0.08 thickness = 0.012 -solid_density = 2500 +structure_density = 2500 # Young's modulus and Poisson ratio E = 1e6 @@ -52,22 +52,22 @@ nu = 0.0 # The structure starts at the position of the first particle and ends # at the position of the last particle. -solid_particle_spacing = thickness / (n_particles_x - 1) +structure_particle_spacing = thickness / (n_particles_x - 1) -n_particles_y = round(Int, length_beam / solid_particle_spacing) + 1 +n_particles_y = round(Int, length_beam / structure_particle_spacing) + 1 # The bottom layer is sampled separately below. Note that the `RectangularShape` puts the # first particle half a particle spacing away from the shell of the shape, which is -# correct for fluids, but not for solids. We therefore need to pass `place_on_shell=true`. -plate = RectangularShape(solid_particle_spacing, +# correct for fluids, but not for structures. We therefore need to pass `place_on_shell=true`. +plate = RectangularShape(structure_particle_spacing, (n_particles_x, n_particles_y - 1), - (2initial_fluid_size[1], solid_particle_spacing), - density=solid_density, place_on_shell=true) -fixed_particles = RectangularShape(solid_particle_spacing, - (n_particles_x, 1), (2initial_fluid_size[1], 0.0), - density=solid_density, place_on_shell=true) + (2initial_fluid_size[1], structure_particle_spacing), + density=structure_density, place_on_shell=true) +clamped_particles = RectangularShape(structure_particle_spacing, + (n_particles_x, 1), (2initial_fluid_size[1], 0.0), + density=structure_density, place_on_shell=true) -solid = union(plate, fixed_particles) +structure = union(plate, clamped_particles) # ========================================================================================== # ==== Fluid @@ -93,43 +93,44 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) # ========================================================================================== -# ==== Solid -solid_smoothing_length = sqrt(2) * solid_particle_spacing -solid_smoothing_kernel = WendlandC2Kernel{2}() +# ==== Structure +structure_smoothing_length = sqrt(2) * structure_particle_spacing +structure_smoothing_kernel = WendlandC2Kernel{2}() -# For the FSI we need the hydrodynamic masses and densities in the solid boundary model -hydrodynamic_densites = fluid_density * ones(size(solid.density)) -hydrodynamic_masses = hydrodynamic_densites * solid_particle_spacing^2 +# For the FSI we need the hydrodynamic masses and densities in the structure boundary model +hydrodynamic_densites = fluid_density * ones(size(structure.density)) +hydrodynamic_masses = hydrodynamic_densites * structure_particle_spacing^2 -k_solid = gravity * initial_fluid_size[2] -spacing_ratio_solid = fluid_particle_spacing / solid_particle_spacing -boundary_model_solid = BoundaryModelMonaghanKajtar(k_solid, spacing_ratio_solid, - solid_particle_spacing, - hydrodynamic_masses) +k_structure = gravity * initial_fluid_size[2] +spacing_ratio_structure = fluid_particle_spacing / structure_particle_spacing +boundary_model_structure = BoundaryModelMonaghanKajtar(k_structure, spacing_ratio_structure, + structure_particle_spacing, + hydrodynamic_masses) # `BoundaryModelDummyParticles` usually produces better results, since Monaghan-Kajtar BCs # tend to introduce a non-physical gap between fluid and boundary. # However, `BoundaryModelDummyParticles` can only be used when the plate thickness is # at least two fluid particle spacings, so that the compact support is fully sampled, -# or fluid particles can penetrate the solid. +# or fluid particles can penetrate the structure. # With higher fluid resolutions, uncomment the code below for better results. # -# boundary_model_solid = BoundaryModelDummyParticles(hydrodynamic_densites, +# boundary_model_structure = BoundaryModelDummyParticles(hydrodynamic_densites, # hydrodynamic_masses, # state_equation=state_equation, # boundary_density_calculator, # smoothing_kernel, smoothing_length) -solid_system = TotalLagrangianSPHSystem(solid, - solid_smoothing_kernel, solid_smoothing_length, - E, nu, boundary_model=boundary_model_solid, - n_fixed_particles=n_particles_x, - acceleration=(0.0, -gravity), - penalty_force=PenaltyForceGanzenmueller(alpha=0.01)) +structure_system = TotalLagrangianSPHSystem(structure, + structure_smoothing_kernel, + structure_smoothing_length, + E, nu, boundary_model=boundary_model_structure, + n_clamped_particles=n_particles_x, + acceleration=(0.0, -gravity), + penalty_force=PenaltyForceGanzenmueller(alpha=0.01)) # ========================================================================================== # ==== Simulation -semi = Semidiscretization(fluid_system, boundary_system, solid_system) +semi = Semidiscretization(fluid_system, boundary_system, structure_system) ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=100) diff --git a/examples/fsi/falling_sphere_2d.jl b/examples/fsi/falling_sphere_2d.jl index 8e6db81507..4af4b4bad2 100644 --- a/examples/fsi/falling_sphere_2d.jl +++ b/examples/fsi/falling_sphere_2d.jl @@ -8,6 +8,6 @@ using TrixiParticles trixi_include(@__MODULE__, joinpath(examples_dir(), "fsi", "falling_spheres_2d.jl"), - solid_system_2=nothing, fluid_particle_spacing=0.02, + structure_system_2=nothing, fluid_particle_spacing=0.02, initial_fluid_size=(1.0, 0.9), tank_size=(1.0, 1.0), tspan=(0.0, 1.0), abstol=1e-6, reltol=1e-3) diff --git a/examples/fsi/falling_sphere_3d.jl b/examples/fsi/falling_sphere_3d.jl index 0f49aa7b29..966d203578 100644 --- a/examples/fsi/falling_sphere_3d.jl +++ b/examples/fsi/falling_sphere_3d.jl @@ -8,12 +8,12 @@ using TrixiParticles trixi_include(@__MODULE__, joinpath(examples_dir(), "fsi", "falling_spheres_2d.jl"), - solid_system_2=nothing, fluid_particle_spacing=0.05, + structure_system_2=nothing, fluid_particle_spacing=0.05, initial_fluid_size=(1.0, 0.9, 1.0), tank_size=(1.0, 1.0, 1.0), faces=(true, true, true, false, true, true), acceleration=(0.0, -9.81, 0.0), sphere1_center=(0.5, 2.0, 0.5), fluid_smoothing_kernel=WendlandC2Kernel{3}(), - solid_smoothing_kernel=WendlandC2Kernel{3}(), + structure_smoothing_kernel=WendlandC2Kernel{3}(), sphere_type=RoundSphere(), output_directory="out", prefix="", tspan=(0.0, 1.0), abstol=1e-6, reltol=1e-3) diff --git a/examples/fsi/falling_spheres_2d.jl b/examples/fsi/falling_spheres_2d.jl index d59766befe..a475da1628 100644 --- a/examples/fsi/falling_spheres_2d.jl +++ b/examples/fsi/falling_spheres_2d.jl @@ -11,7 +11,7 @@ using OrdinaryDiffEq # ========================================================================================== # ==== Resolution fluid_particle_spacing = 0.02 -solid_particle_spacing = fluid_particle_spacing +structure_particle_spacing = fluid_particle_spacing # Change spacing ratio to 3 and boundary layers to 1 when using Monaghan-Kajtar boundary model boundary_layers = 3 @@ -48,9 +48,9 @@ nu = 0.0 sphere1_center = (0.5, 1.6) sphere2_center = (1.5, 1.6) -sphere1 = SphereShape(solid_particle_spacing, sphere1_radius, sphere1_center, +sphere1 = SphereShape(structure_particle_spacing, sphere1_radius, sphere1_center, sphere1_density, sphere_type=VoxelSphere()) -sphere2 = SphereShape(solid_particle_spacing, sphere2_radius, sphere2_center, +sphere2 = SphereShape(structure_particle_spacing, sphere2_radius, sphere2_center, sphere2_density, sphere_type=VoxelSphere()) # ========================================================================================== @@ -79,48 +79,53 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) # ========================================================================================== -# ==== Solid -solid_smoothing_length = sqrt(2) * solid_particle_spacing -solid_smoothing_kernel = WendlandC2Kernel{2}() +# ==== Structure +structure_smoothing_length = sqrt(2) * structure_particle_spacing +structure_smoothing_kernel = WendlandC2Kernel{2}() -# For the FSI we need the hydrodynamic masses and densities in the solid boundary model +# For the FSI we need the hydrodynamic masses and densities in the structure boundary model hydrodynamic_densites_1 = fluid_density * ones(size(sphere1.density)) -hydrodynamic_masses_1 = hydrodynamic_densites_1 * solid_particle_spacing^ndims(fluid_system) +hydrodynamic_masses_1 = hydrodynamic_densites_1 * + structure_particle_spacing^ndims(fluid_system) -solid_boundary_model_1 = BoundaryModelDummyParticles(hydrodynamic_densites_1, - hydrodynamic_masses_1, - state_equation=state_equation, - boundary_density_calculator, - fluid_smoothing_kernel, - fluid_smoothing_length) +structure_boundary_model_1 = BoundaryModelDummyParticles(hydrodynamic_densites_1, + hydrodynamic_masses_1, + state_equation=state_equation, + boundary_density_calculator, + fluid_smoothing_kernel, + fluid_smoothing_length) hydrodynamic_densites_2 = fluid_density * ones(size(sphere2.density)) -hydrodynamic_masses_2 = hydrodynamic_densites_2 * solid_particle_spacing^ndims(fluid_system) - -solid_boundary_model_2 = BoundaryModelDummyParticles(hydrodynamic_densites_2, - hydrodynamic_masses_2, - state_equation=state_equation, - boundary_density_calculator, - fluid_smoothing_kernel, - fluid_smoothing_length) - -solid_system_1 = TotalLagrangianSPHSystem(sphere1, - solid_smoothing_kernel, solid_smoothing_length, - sphere1_E, nu, - acceleration=(0.0, -gravity), - boundary_model=solid_boundary_model_1, - penalty_force=PenaltyForceGanzenmueller(alpha=0.3)) - -solid_system_2 = TotalLagrangianSPHSystem(sphere2, - solid_smoothing_kernel, solid_smoothing_length, - sphere2_E, nu, - acceleration=(0.0, -gravity), - boundary_model=solid_boundary_model_2, - penalty_force=PenaltyForceGanzenmueller(alpha=0.3)) +hydrodynamic_masses_2 = hydrodynamic_densites_2 * + structure_particle_spacing^ndims(fluid_system) + +structure_boundary_model_2 = BoundaryModelDummyParticles(hydrodynamic_densites_2, + hydrodynamic_masses_2, + state_equation=state_equation, + boundary_density_calculator, + fluid_smoothing_kernel, + fluid_smoothing_length) + +structure_system_1 = TotalLagrangianSPHSystem(sphere1, + structure_smoothing_kernel, + structure_smoothing_length, + sphere1_E, nu, + acceleration=(0.0, -gravity), + boundary_model=structure_boundary_model_1, + penalty_force=PenaltyForceGanzenmueller(alpha=0.3)) + +structure_system_2 = TotalLagrangianSPHSystem(sphere2, + structure_smoothing_kernel, + structure_smoothing_length, + sphere2_E, nu, + acceleration=(0.0, -gravity), + boundary_model=structure_boundary_model_2, + penalty_force=PenaltyForceGanzenmueller(alpha=0.3)) # ========================================================================================== # ==== Simulation -semi = Semidiscretization(fluid_system, boundary_system, solid_system_1, solid_system_2) +semi = Semidiscretization(fluid_system, boundary_system, structure_system_1, + structure_system_2) ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=50) diff --git a/examples/fsi/falling_water_column_2d.jl b/examples/fsi/falling_water_column_2d.jl index cd6357386c..6fec694bfe 100644 --- a/examples/fsi/falling_water_column_2d.jl +++ b/examples/fsi/falling_water_column_2d.jl @@ -2,8 +2,8 @@ # 2D Falling Water Column on an Elastic Beam (FSI) # # This example simulates a column of water falling under gravity and impacting -# an elastic beam, which is fixed at one end (cantilever). -# It demonstrates Fluid-Structure Interaction where the fluid deforms the solid structure. +# an elastic beam, which is clamped at one end (cantilever). +# It demonstrates Fluid-Structure Interaction where the fluid deforms the structure. # ========================================================================================== using TrixiParticles @@ -14,7 +14,7 @@ using OrdinaryDiffEq n_particles_y = 5 # Load setup from oscillating beam example -trixi_include(@__MODULE__, joinpath(examples_dir(), "solid", "oscillating_beam_2d.jl"), +trixi_include(@__MODULE__, joinpath(examples_dir(), "structure", "oscillating_beam_2d.jl"), thickness=0.05, n_particles_y=n_particles_y, sol=nothing) # Don't run simulation, only include the setup part @@ -53,27 +53,27 @@ fluid_system = WeaklyCompressibleSPHSystem(fluid, fluid_density_calculator, acceleration=(0.0, -gravity)) # ========================================================================================== -# ==== Solid +# ==== Structure k = gravity * initial_fluid_size[2] spacing_ratio = fluid_particle_spacing / particle_spacing -# For the FSI we need the hydrodynamic masses and densities in the solid boundary model -hydrodynamic_densites = fluid_density * ones(size(solid.density)) +# For the FSI we need the hydrodynamic masses and densities in the structure boundary model +hydrodynamic_densites = fluid_density * ones(size(structure.density)) hydrodynamic_masses = hydrodynamic_densites * particle_spacing^2 boundary_model = BoundaryModelMonaghanKajtar(k, spacing_ratio, particle_spacing, hydrodynamic_masses) -solid_system = TotalLagrangianSPHSystem(solid, - smoothing_kernel, smoothing_length, - material.E, material.nu, - boundary_model=boundary_model, - n_fixed_particles=nparticles(fixed_particles), - acceleration=(0.0, -gravity)) +structure_system = TotalLagrangianSPHSystem(structure, + smoothing_kernel, smoothing_length, + material.E, material.nu, + boundary_model=boundary_model, + n_clamped_particles=nparticles(clamped_particles), + acceleration=(0.0, -gravity)) # ========================================================================================== # ==== Simulation -semi = Semidiscretization(fluid_system, solid_system) +semi = Semidiscretization(fluid_system, structure_system) ode = semidiscretize(semi, tspan) info_callback = InfoCallback(interval=100) diff --git a/examples/n_body/n_body_system.jl b/examples/n_body/n_body_system.jl index b877059a41..f1913c7120 100644 --- a/examples/n_body/n_body_system.jl +++ b/examples/n_body/n_body_system.jl @@ -1,7 +1,7 @@ using TrixiParticles using LinearAlgebra -struct NBodySystem{NDIMS, ELTYPE <: Real, IC} <: TrixiParticles.System{NDIMS} +struct NBodySystem{NDIMS, ELTYPE <: Real, IC} <: TrixiParticles.AbstractSystem{NDIMS} initial_condition :: IC mass :: Array{ELTYPE, 1} # [particle] G :: ELTYPE diff --git a/examples/solid/oscillating_beam_2d.jl b/examples/structure/oscillating_beam_2d.jl similarity index 76% rename from examples/solid/oscillating_beam_2d.jl rename to examples/structure/oscillating_beam_2d.jl index 8deadc23db..f43126e935 100644 --- a/examples/solid/oscillating_beam_2d.jl +++ b/examples/structure/oscillating_beam_2d.jl @@ -2,8 +2,8 @@ # 2D Oscillating Elastic Beam (Cantilever) Simulation # # This example simulates the oscillation of a 2D elastic beam (cantilever) -# fixed at one end and subjected to gravity. It uses the Total Lagrangian SPH (TLSPH) -# method for solid mechanics. +# clamped at one end and subjected to gravity. It uses the Total Lagrangian SPH (TLSPH) +# method for structure mechanics. # # Based on: # J. O'Connor and B.D. Rogers @@ -34,11 +34,11 @@ clamp_radius = 0.05 particle_spacing = elastic_beam.thickness / (n_particles_y - 1) # Add particle_spacing/2 to the clamp_radius to ensure that particles are also placed on the radius -fixed_particles = SphereShape(particle_spacing, clamp_radius + particle_spacing / 2, - (0.0, elastic_beam.thickness / 2), material.density, - cutout_min=(0.0, 0.0), - cutout_max=(clamp_radius, elastic_beam.thickness), - place_on_shell=true) +clamped_particles = SphereShape(particle_spacing, clamp_radius + particle_spacing / 2, + (0.0, elastic_beam.thickness / 2), material.density, + cutout_min=(0.0, 0.0), + cutout_max=(clamp_radius, elastic_beam.thickness), + place_on_shell=true) n_particles_clamp_x = round(Int, clamp_radius / particle_spacing) @@ -47,27 +47,27 @@ n_particles_per_dimension = (round(Int, elastic_beam.length / particle_spacing) n_particles_clamp_x + 1, n_particles_y) # Note that the `RectangularShape` puts the first particle half a particle spacing away -# from the boundary, which is correct for fluids, but not for solids. +# from the boundary, which is correct for fluids, but not for structures. # We therefore need to pass `place_on_shell=true`. beam = RectangularShape(particle_spacing, n_particles_per_dimension, (0.0, 0.0), density=material.density, place_on_shell=true) -solid = union(beam, fixed_particles) +structure = union(beam, clamped_particles) # ========================================================================================== -# ==== Solid +# ==== Structure smoothing_length = sqrt(2) * particle_spacing smoothing_kernel = WendlandC2Kernel{2}() -solid_system = TotalLagrangianSPHSystem(solid, smoothing_kernel, smoothing_length, - material.E, material.nu, - n_fixed_particles=nparticles(fixed_particles), - acceleration=(0.0, -gravity), - penalty_force=nothing, viscosity=nothing) +structure_system = TotalLagrangianSPHSystem(structure, smoothing_kernel, smoothing_length, + material.E, material.nu, + n_clamped_particles=nparticles(clamped_particles), + acceleration=(0.0, -gravity), + penalty_force=nothing, viscosity=nothing) # ========================================================================================== # ==== Simulation -semi = Semidiscretization(solid_system, +semi = Semidiscretization(structure_system, neighborhood_search=PrecomputedNeighborhoodSearch{2}(), parallelization_backend=PolyesterBackend()) ode = semidiscretize(semi, tspan) diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 2dffe6a847..f643ef5442 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -45,7 +45,7 @@ using WriteVTK: vtk_grid, MeshCell, VTKCellTypes, paraview_collection, vtk_save # `util.jl` needs to be first because of the macros `@trixi_timeit` and `@threaded` include("util.jl") -include("general/system.jl") +include("general/abstract_system.jl") include("general/general.jl") include("setups/setups.jl") include("schemes/schemes.jl") @@ -77,8 +77,7 @@ export SchoenbergCubicSplineKernel, SchoenbergQuarticSplineKernel, export StateEquationCole, StateEquationIdealGas export ArtificialViscosityMonaghan, ViscosityAdami, ViscosityMorris, ViscosityAdamiSGS, ViscosityMorrisSGS -export DensityDiffusion, DensityDiffusionMolteniColagrossi, DensityDiffusionFerrari, - DensityDiffusionAntuono +export DensityDiffusionMolteniColagrossi, DensityDiffusionFerrari, DensityDiffusionAntuono export tensile_instability_control export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureExtrapolation, PressureMirroring, PressureZeroing, BoundaryModelCharacteristicsLastiwka, @@ -86,7 +85,7 @@ export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureEx BernoulliPressureExtrapolation export FirstOrderMirroring, ZerothOrderMirroring, SimpleMirroring export HertzContactModel, LinearContactModel -export BoundaryMovement +export PrescribedMotion export examples_dir, validation_dir export trixi2vtk, vtk2trixi export RectangularTank, RectangularShape, SphereShape, ComplexShape diff --git a/src/callbacks/post_process.jl b/src/callbacks/post_process.jl index 40b31998fa..638ca9aa61 100644 --- a/src/callbacks/post_process.jl +++ b/src/callbacks/post_process.jl @@ -245,7 +245,7 @@ function (pp::PostprocessCallback)(integrator) end foreach_system(semi) do system - if system isa BoundarySystem && pp.exclude_boundary + if system isa AbstractBoundarySystem && pp.exclude_boundary return end diff --git a/src/general/system.jl b/src/general/abstract_system.jl similarity index 86% rename from src/general/system.jl rename to src/general/abstract_system.jl index 6c4aed9b0f..70971a2535 100644 --- a/src/general/system.jl +++ b/src/general/abstract_system.jl @@ -1,28 +1,20 @@ # Abstract supertype for all system types. -abstract type System{NDIMS} end +abstract type AbstractSystem{NDIMS} end -abstract type FluidSystem{NDIMS} <: System{NDIMS} end -timer_name(::FluidSystem) = "fluid" -vtkname(system::FluidSystem) = "fluid" +@inline Base.ndims(::AbstractSystem{NDIMS}) where {NDIMS} = NDIMS +@inline Base.eltype(system::AbstractSystem) = error("eltype not implemented for system $system") -abstract type SolidSystem{NDIMS} <: System{NDIMS} end -timer_name(::SolidSystem) = "solid" -vtkname(system::SolidSystem) = "solid" +abstract type AbstractFluidSystem{NDIMS} <: AbstractSystem{NDIMS} end +timer_name(::AbstractFluidSystem) = "fluid" +vtkname(system::AbstractFluidSystem) = "fluid" -abstract type BoundarySystem{NDIMS} <: System{NDIMS} end -timer_name(::BoundarySystem) = "boundary" -vtkname(system::BoundarySystem) = "boundary" +abstract type AbstractStructureSystem{NDIMS} <: AbstractSystem{NDIMS} end +timer_name(::AbstractStructureSystem) = "structure" +vtkname(system::AbstractStructureSystem) = "structure" -@inline function set_zero!(du) - du .= zero(eltype(du)) - - return du -end - -initialize!(system, semi) = system - -@inline Base.ndims(::System{NDIMS}) where {NDIMS} = NDIMS -@inline Base.eltype(system::System) = error("eltype not implemented for system $system") +abstract type AbstractBoundarySystem{NDIMS} <: AbstractSystem{NDIMS} end +timer_name(::AbstractBoundarySystem) = "boundary" +vtkname(system::AbstractBoundarySystem) = "boundary" # Number of integrated variables in the first component of the ODE system (coordinates) @inline u_nvariables(system) = ndims(system) @@ -37,7 +29,7 @@ initialize!(system, semi) = system # Number of particles in the system whose positions are to be integrated (corresponds to the size of u and du) @inline n_moving_particles(system) = nparticles(system) -@inline eachparticle(system::System) = active_particles(system) +@inline eachparticle(system::AbstractSystem) = active_particles(system) @inline eachparticle(initial_condition) = Base.OneTo(nparticles(initial_condition)) # Wrapper for systems with `SystemBuffer` @@ -50,6 +42,14 @@ initialize!(system, semi) = system @inline active_particles(system) = active_particles(system, buffer(system)) @inline active_particles(system, ::Nothing) = Base.OneTo(nparticles(system)) +@inline function set_zero!(du) + du .= zero(eltype(du)) + + return du +end + +initialize!(system, semi) = system + # This should not be dispatched by system type. We always expect to get a column of `A`. @propagate_inbounds function extract_svector(A, system, i) extract_svector(A, Val(ndims(system)), i) @@ -100,11 +100,11 @@ end # By default, try to extract it from `v`. @inline current_velocity(v, system) = v -@inline function current_density(v, system::System, particle) +@inline function current_density(v, system::AbstractSystem, particle) return current_density(v, system)[particle] end -@propagate_inbounds function current_pressure(v, system::System, particle) +@propagate_inbounds function current_pressure(v, system::AbstractSystem, particle) return current_pressure(v, system)[particle] end diff --git a/src/general/corrections.jl b/src/general/corrections.jl index 57c586d7ed..425616a21a 100644 --- a/src/general/corrections.jl +++ b/src/general/corrections.jl @@ -107,11 +107,11 @@ which results in a 1st-order-accurate SPH method (see [Bonet, 1999](@cite Bonet1 """ struct MixedKernelGradientCorrection end -function kernel_correction_coefficient(system::FluidSystem, particle) +function kernel_correction_coefficient(system::AbstractFluidSystem, particle) return system.cache.kernel_correction_coefficient[particle] end -function kernel_correction_coefficient(system::BoundarySystem, particle) +function kernel_correction_coefficient(system::AbstractBoundarySystem, particle) return system.boundary_model.cache.kernel_correction_coefficient[particle] end @@ -126,7 +126,8 @@ function compute_correction_values!(system, ::ShepardKernelCorrection, u, v_ode, system.cache.kernel_correction_coefficient) end -function compute_correction_values!(system::BoundarySystem, ::ShepardKernelCorrection, u, +function compute_correction_values!(system::AbstractBoundarySystem, + ::ShepardKernelCorrection, u, v_ode, u_ode, semi) return compute_shepard_coeff!(system, current_coordinates(u, system), v_ode, u_ode, semi, @@ -160,15 +161,15 @@ function compute_shepard_coeff!(system, system_coords, v_ode, u_ode, semi, return kernel_correction_coefficient end -function dw_gamma(system::FluidSystem, particle) +function dw_gamma(system::AbstractFluidSystem, particle) return extract_svector(system.cache.dw_gamma, system, particle) end -function dw_gamma(system::BoundarySystem, particle) +function dw_gamma(system::AbstractBoundarySystem, particle) return extract_svector(system.boundary_model.cache.dw_gamma, system, particle) end -function compute_correction_values!(system::FluidSystem, +function compute_correction_values!(system::AbstractFluidSystem, correction::Union{KernelCorrection, MixedKernelGradientCorrection}, u, v_ode, u_ode, semi) @@ -178,7 +179,7 @@ function compute_correction_values!(system::FluidSystem, system.cache.dw_gamma) end -function compute_correction_values!(system::BoundarySystem, +function compute_correction_values!(system::AbstractBoundarySystem, correction::Union{KernelCorrection, MixedKernelGradientCorrection}, u, v_ode, u_ode, semi) diff --git a/src/general/custom_quantities.jl b/src/general/custom_quantities.jl index 975e7a8394..f9c63ec1a1 100644 --- a/src/general/custom_quantities.jl +++ b/src/general/custom_quantities.jl @@ -17,7 +17,8 @@ function kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, t) end end -function kinetic_energy(system::BoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function kinetic_energy(system::AbstractBoundarySystem, + dv_ode, du_ode, v_ode, u_ode, semi, t) return zero(eltype(system)) end @@ -30,7 +31,7 @@ function total_mass(system, dv_ode, du_ode, v_ode, u_ode, semi, t) return sum(system.mass) end -function total_mass(system::BoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function total_mass(system::AbstractBoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t) # It does not make sense to return a mass for boundary systems. # The material density and therefore the physical mass of the boundary is not relevant # when simulating a solid, stationary wall. The boundary always behaves as if it had @@ -49,7 +50,7 @@ end Returns the maximum pressure over all particles in a system. """ -function max_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function max_pressure(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return maximum(current_pressure(v, system)) end @@ -63,7 +64,7 @@ end Returns the minimum pressure over all particles in a system. """ -function min_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function min_pressure(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return minimum(current_pressure(v, system)) end @@ -77,7 +78,7 @@ end Returns the average pressure over all particles in a system. """ -function avg_pressure(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function avg_pressure(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) sum_ = sum(current_pressure(v, system)) return sum_ / nparticles(system) @@ -92,7 +93,7 @@ end Returns the maximum density over all particles in a system. """ -function max_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function max_density(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return maximum(current_density(v, system)) end @@ -106,7 +107,7 @@ end Returns the minimum density over all particles in a system. """ -function min_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function min_density(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) return minimum(current_density(v, system)) end @@ -120,7 +121,7 @@ end Returns the average_density over all particles in a system. """ -function avg_density(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) +function avg_density(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) sum_ = sum(current_density(v, system)) return sum_ / nparticles(system) diff --git a/src/general/general.jl b/src/general/general.jl index 9307671cd8..87b5b82022 100644 --- a/src/general/general.jl +++ b/src/general/general.jl @@ -1,4 +1,4 @@ -# Note that `system.jl` has already been included. +# Note that `abstract_system.jl` has already been included. # `semidiscretization.jl` depends on the system types and has to be included later. # `density_calculators.jl` needs to be included before `corrections.jl`. include("density_calculators.jl") diff --git a/src/general/gpu.jl b/src/general/gpu.jl index 8fe1f85d87..77dec03021 100644 --- a/src/general/gpu.jl +++ b/src/general/gpu.jl @@ -15,14 +15,16 @@ Adapt.@adapt_structure EntropicallyDampedSPHSystem Adapt.@adapt_structure BoundarySPHSystem Adapt.@adapt_structure BoundaryModelDummyParticles Adapt.@adapt_structure BoundaryModelMonaghanKajtar -Adapt.@adapt_structure BoundaryMovement +Adapt.@adapt_structure PrescribedMotion Adapt.@adapt_structure TotalLagrangianSPHSystem Adapt.@adapt_structure BoundaryZone Adapt.@adapt_structure SystemBuffer Adapt.@adapt_structure OpenBoundarySPHSystem KernelAbstractions.get_backend(::PtrArray) = KernelAbstractions.CPU() -KernelAbstractions.get_backend(system::System) = KernelAbstractions.get_backend(system.mass) +function KernelAbstractions.get_backend(system::AbstractSystem) + KernelAbstractions.get_backend(system.mass) +end function KernelAbstractions.get_backend(system::BoundarySPHSystem) KernelAbstractions.get_backend(system.coordinates) diff --git a/src/general/interpolation.jl b/src/general/interpolation.jl index cc246111c9..c6cb8a1319 100644 --- a/src/general/interpolation.jl +++ b/src/general/interpolation.jl @@ -594,7 +594,7 @@ end neighbor_count=neighbor_count, cache...) end -@inline function create_cache_interpolation(ref_system::FluidSystem, n_points, semi) +@inline function create_cache_interpolation(ref_system::AbstractFluidSystem, n_points, semi) (; parallelization_backend) = semi velocity = allocate(parallelization_backend, eltype(ref_system), @@ -609,7 +609,8 @@ end return (; velocity, pressure, density) end -@inline function create_cache_interpolation(ref_system::SolidSystem, n_points, semi) +@inline function create_cache_interpolation(ref_system::AbstractStructureSystem, + n_points, semi) (; parallelization_backend) = semi velocity = allocate(parallelization_backend, eltype(ref_system), @@ -627,7 +628,7 @@ end return (; velocity, jacobian, von_mises_stress, cauchy_stress) end -@inline function interpolate_system!(cache, v, system::FluidSystem, +@inline function interpolate_system!(cache, v, system::AbstractFluidSystem, point, neighbor, volume_b, W_ab, clip_negative_pressure) velocity = current_velocity(v, system, neighbor) @@ -647,7 +648,7 @@ end return cache end -@inline function interpolate_system!(cache, v, system::SolidSystem, +@inline function interpolate_system!(cache, v, system::AbstractStructureSystem, point, neighbor, volume_b, W_ab, clip_negative_pressure) velocity = current_velocity(v, system, neighbor) diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl index f6cc8015c9..2eebea8677 100644 --- a/src/general/semidiscretization.jl +++ b/src/general/semidiscretization.jl @@ -70,7 +70,7 @@ struct Semidiscretization{BACKEND, S, RU, RV, NS, UCU} end end -function Semidiscretization(systems::Union{System, Nothing}...; +function Semidiscretization(systems::Union{AbstractSystem, Nothing}...; neighborhood_search=GridNeighborhoodSearch{ndims(first(systems))}(), parallelization_backend=PolyesterBackend()) systems = filter(system -> !isnothing(system), systems) @@ -184,7 +184,7 @@ end end @inline function compact_support(system, model::BoundaryModelMonaghanKajtar, neighbor) - # Use the compact support of the fluid for solid-fluid interaction + # Use the compact support of the fluid for structure-fluid interaction return compact_support(neighbor, system) end @@ -195,7 +195,7 @@ end end @inline function compact_support(system, model::BoundaryModelDummyParticles, neighbor) - # TODO: Monaghan-Kajtar BC are using the fluid's compact support for solid-fluid + # TODO: Monaghan-Kajtar BC are using the fluid's compact support for structure-fluid # interaction. Dummy particle BC use the model's compact support, which is also used # for density summations. (; smoothing_kernel, smoothing_length) = model @@ -484,7 +484,7 @@ end # Solid wall boundary system doesn't integrate the particle positions @inline add_velocity!(du, v, particle, system::BoundarySPHSystem) = du -@inline function add_velocity!(du, v, particle, system::FluidSystem) +@inline function add_velocity!(du, v, particle, system::AbstractFluidSystem) # This is zero unless a shifting technique is used delta_v_ = delta_v(system, particle) @@ -519,7 +519,7 @@ end # before calling `interact!` to compute forces. function update_systems_and_nhs(v_ode, u_ode, semi, t) # First update step before updating the NHS - # (for example for writing the current coordinates in the solid system) + # (for example for writing the current coordinates in the TLSPH system) foreach_system(semi) do system v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) @@ -598,11 +598,13 @@ function add_source_terms!(dv_ode, v_ode, u_ode, semi, t) end @inline source_terms(system) = nothing -@inline source_terms(system::Union{FluidSystem, SolidSystem}) = system.source_terms +@inline source_terms(system::Union{AbstractFluidSystem, AbstractStructureSystem}) = system.source_terms @inline add_acceleration!(dv, particle, system) = dv -@inline function add_acceleration!(dv, particle, system::Union{FluidSystem, SolidSystem}) +@inline function add_acceleration!(dv, particle, + system::Union{AbstractFluidSystem, + AbstractStructureSystem}) (; acceleration) = system for i in 1:ndims(system) @@ -707,10 +709,10 @@ end # NHS updates # To prevent hard-to-find bugs, there is not default version function update_nhs!(neighborhood_search, - system::FluidSystem, - neighbor::Union{FluidSystem, TotalLagrangianSPHSystem}, + system::AbstractFluidSystem, + neighbor::Union{AbstractFluidSystem, TotalLagrangianSPHSystem}, u_system, u_neighbor, semi) - # The current coordinates of fluids and solids change over time + # The current coordinates of fluids and structures change over time update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), @@ -718,7 +720,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::FluidSystem, neighbor::BoundarySPHSystem, + system::AbstractFluidSystem, neighbor::BoundarySPHSystem, u_system, u_neighbor, semi) # Boundary coordinates only change over time when `neighbor.ismoving[]` update!(neighborhood_search, @@ -728,7 +730,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::FluidSystem, neighbor::OpenBoundarySPHSystem, + system::AbstractFluidSystem, neighbor::OpenBoundarySPHSystem, u_system, u_neighbor, semi) # The current coordinates of fluids and open boundaries change over time. @@ -741,7 +743,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::OpenBoundarySPHSystem, neighbor::FluidSystem, + system::OpenBoundarySPHSystem, neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # The current coordinates of both open boundaries and fluids change over time. @@ -768,9 +770,9 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::TotalLagrangianSPHSystem, neighbor::FluidSystem, + system::TotalLagrangianSPHSystem, neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) - # The current coordinates of fluids and solids change over time + # The current coordinates of fluids and structured change over time update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), @@ -787,7 +789,7 @@ end function update_nhs!(neighborhood_search, system::TotalLagrangianSPHSystem, neighbor::BoundarySPHSystem, u_system, u_neighbor, semi) - # The current coordinates of solids change over time. + # The current coordinates of structured change over time. # Boundary coordinates only change over time when `neighbor.ismoving[]`. update!(neighborhood_search, current_coordinates(u_system, system), @@ -798,14 +800,14 @@ end # This function is the same as the one below to avoid ambiguous dispatch when using `Union` function update_nhs!(neighborhood_search, system::BoundarySPHSystem{<:BoundaryModelDummyParticles}, - neighbor::FluidSystem, u_system, u_neighbor, semi) + neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # Depending on the density calculator of the boundary model, this NHS is used for # - kernel summation (`SummationDensity`) # - continuity equation (`ContinuityDensity`) # - pressure extrapolation (`AdamiPressureExtrapolation`) # # Boundary coordinates only change over time when `neighbor.ismoving[]`. - # The current coordinates of fluids and solids change over time. + # The current coordinates of fluids and structured change over time. update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), @@ -823,7 +825,7 @@ function update_nhs!(neighborhood_search, # - pressure extrapolation (`AdamiPressureExtrapolation`) # # Boundary coordinates only change over time when `neighbor.ismoving[]`. - # The current coordinates of fluids and solids change over time. + # The current coordinates of fluids and structured change over time. update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), @@ -864,7 +866,7 @@ end function update_nhs!(neighborhood_search, system::BoundarySPHSystem, - neighbor::FluidSystem, + neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # Don't update. This NHS is never used. return neighborhood_search @@ -912,15 +914,15 @@ function check_configuration(systems, check_system_color(systems) end -check_configuration(system::System, systems, nhs) = nothing +check_configuration(system::AbstractSystem, systems, nhs) = nothing function check_system_color(systems) - if any(system isa FluidSystem && !(system isa ParticlePackingSystem) && + if any(system isa AbstractFluidSystem && !(system isa ParticlePackingSystem) && !isnothing(system.surface_tension) for system in systems) # System indices of all systems that are either a fluid or a boundary system - system_ids = findall(system isa Union{FluidSystem, BoundarySPHSystem} + system_ids = findall(system isa Union{AbstractFluidSystem, BoundarySPHSystem} for system in systems) if length(system_ids) > 1 && sum(i -> systems[i].cache.color, system_ids) == 0 @@ -929,12 +931,14 @@ function check_system_color(systems) end end -function check_configuration(fluid_system::FluidSystem, systems, nhs) +function check_configuration(fluid_system::AbstractFluidSystem, systems, nhs) if !(fluid_system isa ParticlePackingSystem) && !isnothing(fluid_system.surface_tension) foreach_system(systems) do neighbor - if neighbor isa FluidSystem && isnothing(fluid_system.surface_tension) && + if neighbor isa AbstractFluidSystem && + isnothing(fluid_system.surface_tension) && isnothing(fluid_system.surface_normal_method) - throw(ArgumentError("All `FluidSystem` need to use a surface tension model or a surface normal method.")) + throw(ArgumentError("either none or all fluid systems in a simulation need " * + "to use a surface tension model or a surface normal method.")) end end end @@ -957,7 +961,7 @@ function check_configuration(system::TotalLagrangianSPHSystem, systems, nhs) (; boundary_model) = system foreach_system(systems) do neighbor - if neighbor isa FluidSystem && boundary_model === nothing + if neighbor isa AbstractFluidSystem && boundary_model === nothing throw(ArgumentError("a boundary model for `TotalLagrangianSPHSystem` must be " * "specified when simulating a fluid-structure interaction.")) end diff --git a/src/general/smoothing_kernels.jl b/src/general/smoothing_kernels.jl index 507ba2daf0..6a5e8b33f4 100644 --- a/src/general/smoothing_kernels.jl +++ b/src/general/smoothing_kernels.jl @@ -1,6 +1,6 @@ -abstract type SmoothingKernel{NDIMS} end +abstract type AbstractSmoothingKernel{NDIMS} end -@inline Base.ndims(::SmoothingKernel{NDIMS}) where {NDIMS} = NDIMS +@inline Base.ndims(::AbstractSmoothingKernel{NDIMS}) where {NDIMS} = NDIMS @inline function kernel_grad(kernel, pos_diff, distance, h) # TODO Use `eps` relative to `h` to allow scaling of simulations @@ -73,7 +73,7 @@ Note: This truncation makes this Kernel not conservative, which is beneficial in regards to stability but makes it less accurate. """ -struct GaussianKernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct GaussianKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end @inline @fastmath function kernel(kernel::GaussianKernel, r::Real, h) q = r / h @@ -133,7 +133,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct SchoenbergCubicSplineKernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct SchoenbergCubicSplineKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end @muladd @inline function kernel(kernel::SchoenbergCubicSplineKernel, r::Real, h) q = r / h @@ -207,7 +207,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct SchoenbergQuarticSplineKernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct SchoenbergQuarticSplineKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end # Note that `floating_point_number^integer_literal` is lowered to `Base.literal_pow`. # Currently, specializations reducing this to simple multiplications exist only up @@ -297,7 +297,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct SchoenbergQuinticSplineKernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct SchoenbergQuinticSplineKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end @fastpow @muladd @inline function kernel(kernel::SchoenbergQuinticSplineKernel, r::Real, h) q = r / h @@ -343,10 +343,10 @@ end @inline normalization_factor(::SchoenbergQuinticSplineKernel{2}, h) = 7 / (pi * h^2 * 478) @inline normalization_factor(::SchoenbergQuinticSplineKernel{3}, h) = 1 / (pi * h^3 * 120) -abstract type WendlandKernel{NDIMS} <: SmoothingKernel{NDIMS} end +abstract type AbstractWendlandKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end # Compact support for all Wendland kernels -@inline compact_support(::WendlandKernel, h) = 2h +@inline compact_support(::AbstractWendlandKernel, h) = 2h @doc raw""" WendlandC2Kernel{NDIMS}() @@ -380,7 +380,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct WendlandC2Kernel{NDIMS} <: WendlandKernel{NDIMS} end +struct WendlandC2Kernel{NDIMS} <: AbstractWendlandKernel{NDIMS} end @fastpow @inline function kernel(kernel::WendlandC2Kernel, r::Real, h) q = r / h @@ -447,7 +447,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct WendlandC4Kernel{NDIMS} <: WendlandKernel{NDIMS} end +struct WendlandC4Kernel{NDIMS} <: AbstractWendlandKernel{NDIMS} end @fastpow @inline function kernel(kernel::WendlandC4Kernel, r::Real, h) q = r / h @@ -511,7 +511,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct WendlandC6Kernel{NDIMS} <: WendlandKernel{NDIMS} end +struct WendlandC6Kernel{NDIMS} <: AbstractWendlandKernel{NDIMS} end @fastpow @inline function kernel(kernel::WendlandC6Kernel, r::Real, h) q = r / h @@ -577,7 +577,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct Poly6Kernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct Poly6Kernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end @inline function kernel(kernel::Poly6Kernel, r::Real, h) q = r / h @@ -642,7 +642,7 @@ where ``\delta`` is the typical particle spacing. For general information and usage see [Smoothing Kernels](@ref smoothing_kernel). """ -struct SpikyKernel{NDIMS} <: SmoothingKernel{NDIMS} end +struct SpikyKernel{NDIMS} <: AbstractSmoothingKernel{NDIMS} end @inline function kernel(kernel::SpikyKernel, r::Real, h) q = r / h diff --git a/src/io/io.jl b/src/io/io.jl index 680bbe9ac2..59440d6cbb 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -75,7 +75,7 @@ end add_system_data!(system_data, data::Nothing) = system_data -function add_system_data!(system_data, system::FluidSystem) +function add_system_data!(system_data, system::AbstractFluidSystem) system_data["system_type"] = type2string(system) system_data["particle_spacing"] = particle_spacing(system, 1) system_data["density_calculator"] = type2string(system.density_calculator) @@ -295,12 +295,12 @@ function add_system_data!(system_data, boundary_zone::BoundaryZone, indice) system_data[zone_name]["prescribed_velocity"] = boundary_zone.prescribed_velocity end -function add_system_data!(system_data, movement::BoundaryMovement) - system_data["movement"] = Dict{String, Any}() - system_data["movement"]["model"] = type2string(movement) - system_data["movement"]["movement_function"] = type2string(movement.movement_function) - system_data["movement"]["is_moving"] = type2string(movement.is_moving) - system_data["movement"]["moving_particles"] = movement.moving_particles +function add_system_data!(system_data, motion::PrescribedMotion) + system_data["prescribed_motion"] = Dict{String, Any}() + system_data["prescribed_motion"]["model"] = type2string(motion) + system_data["prescribed_motion"]["movement_function"] = type2string(motion.movement_function) + system_data["prescribed_motion"]["is_moving"] = type2string(motion.is_moving) + system_data["prescribed_motion"]["moving_particles"] = motion.moving_particles end function add_system_data!(system_data, penalty_force::PenaltyForceGanzenmueller) diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index 6f0b772189..fb2323d648 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -127,7 +127,7 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; end @trixi_timeit timer() "write to vtk" vtk_grid(file, points, cells) do vtk - # Dispatches based on the different system types e.g. FluidSystem, TotalLagrangianSPHSystem + # Dispatches based on the different system types e.g. AbstractFluidSystem write2vtk!(vtk, v, u, t, system) # Store particle index @@ -289,7 +289,7 @@ function write2vtk!(vtk, v, u, t, system::DEMSystem) return vtk end -function write2vtk!(vtk, v, u, t, system::FluidSystem) +function write2vtk!(vtk, v, u, t, system::AbstractFluidSystem) vtk["velocity"] = [current_velocity(v, system, particle) for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) @@ -362,8 +362,6 @@ function write2vtk!(vtk, viscosity::ArtificialViscosityMonaghan) end function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem) - n_fixed_particles = nparticles(system) - n_moving_particles(system) - vtk["velocity"] = [current_velocity(v, system, particle) for particle in eachparticle(system)] vtk["jacobian"] = [det(deformation_gradient(system, particle)) diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl index 42502e9afc..9483aa2789 100644 --- a/src/preprocessing/particle_packing/system.jl +++ b/src/preprocessing/particle_packing/system.jl @@ -56,7 +56,7 @@ For more information on the methods, see [particle packing](@ref particle_packin Recommended values are `0.8` or `0.9`. """ struct ParticlePackingSystem{S, F, NDIMS, ELTYPE <: Real, PR, C, AV, - IC, M, D, K, N, SD} <: FluidSystem{NDIMS} + IC, M, D, K, N, SD} <: AbstractFluidSystem{NDIMS} initial_condition :: IC advection_velocity :: AV mass :: M diff --git a/src/schemes/boundary/dummy_particles/dummy_particles.jl b/src/schemes/boundary/dummy_particles/dummy_particles.jl index f0051bdb3a..b495dffab5 100644 --- a/src/schemes/boundary/dummy_particles/dummy_particles.jl +++ b/src/schemes/boundary/dummy_particles/dummy_particles.jl @@ -469,7 +469,8 @@ end end @inline function boundary_pressure_extrapolation!(parallel::Val{true}, boundary_model, - system, neighbor_system::FluidSystem, + system, + neighbor_system::AbstractFluidSystem, system_coords, neighbor_coords, v, v_neighbor_system, semi) (; pressure, cache, viscosity, density_calculator) = boundary_model @@ -491,7 +492,8 @@ end # Note that this needs to be serial, as we are writing into the same # pressure entry from different loop iterations. @inline function boundary_pressure_extrapolation!(parallel::Val{false}, boundary_model, - system, neighbor_system::FluidSystem, + system, + neighbor_system::AbstractFluidSystem, system_coords, neighbor_coords, v, v_neighbor_system, semi) (; pressure, cache, viscosity, density_calculator) = boundary_model @@ -512,7 +514,7 @@ end end @inline function boundary_pressure_inner!(boundary_model, boundary_density_calculator, - system, neighbor_system::FluidSystem, v, + system, neighbor_system::AbstractFluidSystem, v, v_neighbor_system, particle, neighbor, pos_diff, distance, viscosity, cache, pressure, pressure_offset) @@ -554,7 +556,7 @@ end @inline function dynamic_pressure(boundary_density_calculator::BernoulliPressureExtrapolation, density_neighbor, v, v_neighbor_system, pos_diff, distance, particle, neighbor, - system::BoundarySystem, neighbor_system) + system::AbstractBoundarySystem, neighbor_system) if system.ismoving[] relative_velocity = current_velocity(v, system, particle) .- current_velocity(v_neighbor_system, neighbor_system, neighbor) @@ -569,7 +571,7 @@ end @inline function dynamic_pressure(boundary_density_calculator::BernoulliPressureExtrapolation, density_neighbor, v, v_neighbor_system, pos_diff, distance, particle, neighbor, - system::SolidSystem, neighbor_system) + system::AbstractStructureSystem, neighbor_system) relative_velocity = current_velocity(v, system, particle) .- current_velocity(v_neighbor_system, neighbor_system, neighbor) normal_velocity = dot(relative_velocity, pos_diff) / distance @@ -627,6 +629,6 @@ end return density end -@inline function correction_matrix(system::BoundarySystem, particle) +@inline function correction_matrix(system::AbstractBoundarySystem, particle) extract_smatrix(system.boundary_model.cache.correction_matrix, system, particle) end diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index a7fe53308d..0b424f43a6 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -1,6 +1,6 @@ @doc raw""" OpenBoundarySPHSystem(boundary_zone::BoundaryZone; - fluid_system::FluidSystem, buffer_size::Integer, + fluid_system::AbstractFluidSystem, buffer_size::Integer, boundary_model) Open boundary system for in- and outflow particles. @@ -17,7 +17,7 @@ Open boundary system for in- and outflow particles. This is an experimental feature and may change in any future releases. """ struct OpenBoundarySPHSystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZI, BZ, - B, C} <: System{NDIMS} + B, C} <: AbstractSystem{NDIMS} boundary_model :: BM initial_condition :: IC fluid_system :: FS @@ -53,7 +53,7 @@ function OpenBoundarySPHSystem(boundary_model, initial_condition, fluid_system, end function OpenBoundarySPHSystem(boundary_zones::Union{BoundaryZone, Nothing}...; - fluid_system::FluidSystem, buffer_size::Integer, + fluid_system::AbstractFluidSystem, buffer_size::Integer, boundary_model) boundary_zones_ = filter(bz -> !isnothing(bz), boundary_zones) reference_values_ = map(bz -> bz.reference_values, boundary_zones_) @@ -325,7 +325,7 @@ end end # Fluid particle is in boundary zone -@inline function convert_particle!(fluid_system::FluidSystem, system, +@inline function convert_particle!(fluid_system::AbstractFluidSystem, system, particle, particle_new, v, u, v_fluid, u_fluid) # Activate particle in boundary zone transfer_particle!(system, fluid_system, particle, particle_new, v, u, v_fluid, u_fluid) @@ -379,12 +379,12 @@ end # To account for boundary effects in the viscosity term of the RHS, use the viscosity model # of the neighboring particle systems. @inline function viscosity_model(system::OpenBoundarySPHSystem, - neighbor_system::FluidSystem) + neighbor_system::AbstractFluidSystem) return neighbor_system.viscosity end @inline function viscosity_model(system::OpenBoundarySPHSystem, - neighbor_system::BoundarySystem) + neighbor_system::AbstractBoundarySystem) return neighbor_system.boundary_model.viscosity end diff --git a/src/schemes/boundary/rhs.jl b/src/schemes/boundary/rhs.jl index 2e56d637e0..047df3aa62 100644 --- a/src/schemes/boundary/rhs.jl +++ b/src/schemes/boundary/rhs.jl @@ -1,7 +1,7 @@ # Interaction of boundary with other systems function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, - particle_system::Union{BoundarySystem, OpenBoundarySPHSystem}, + particle_system::Union{AbstractBoundarySystem, OpenBoundarySPHSystem}, neighbor_system, semi) # TODO Solids and moving boundaries should be considered in the continuity equation return dv @@ -11,7 +11,7 @@ end function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::BoundarySPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, - neighbor_system::FluidSystem, semi) + neighbor_system::AbstractFluidSystem, semi) (; boundary_model) = particle_system fluid_density_calculator = neighbor_system.density_calculator diff --git a/src/schemes/boundary/system.jl b/src/schemes/boundary/system.jl index e039aa8c71..619e699e88 100644 --- a/src/schemes/boundary/system.jl +++ b/src/schemes/boundary/system.jl @@ -9,12 +9,12 @@ The interaction between fluid and boundary particles is specified by the boundar - `boundary_model`: Boundary model (see [Boundary Models](@ref boundary_models)) # Keyword Arguments -- `movement`: For moving boundaries, a [`BoundaryMovement`](@ref) can be passed. +- `movement`: For moving boundaries, a [`PrescribedMotion`](@ref) can be passed. - `adhesion_coefficient`: Coefficient specifying the adhesion of a fluid to the surface. Note: currently it is assumed that all fluids have the same adhesion coefficient. """ struct BoundarySPHSystem{BM, NDIMS, ELTYPE <: Real, IC, CO, M, IM, - CA} <: BoundarySystem{NDIMS} + CA} <: AbstractBoundarySystem{NDIMS} initial_condition :: IC coordinates :: CO # Array{ELTYPE, 2} boundary_model :: BM @@ -48,7 +48,7 @@ function BoundarySPHSystem(initial_condition, model; movement=nothing, if movement !== nothing && isempty(movement.moving_particles) # Default is an empty vector, since the number of particles is not known when - # instantiating `BoundaryMovement`. + # instantiating `PrescribedMotion`. resize!(movement.moving_particles, nparticles(initial_condition)) movement.moving_particles .= collect(1:nparticles(initial_condition)) end @@ -98,7 +98,7 @@ The interaction between fluid and boundary particles is specified by the boundar """ struct BoundaryDEMSystem{NDIMS, ELTYPE <: Real, IC, - ARRAY1D, ARRAY2D} <: BoundarySystem{NDIMS} + ARRAY1D, ARRAY2D} <: AbstractBoundarySystem{NDIMS} initial_condition :: IC coordinates :: ARRAY2D # [dimension, particle] radius :: ARRAY1D # [particle] @@ -137,18 +137,18 @@ function Base.show(io::IO, ::MIME"text/plain", system::BoundaryDEMSystem) end """ - BoundaryMovement(movement_function, is_moving; moving_particles=nothing) + PrescribedMotion(movement_function, is_moving; moving_particles=nothing) # Arguments - `movement_function`: Time-dependent function returning an `SVector` of ``d`` dimensions for a ``d``-dimensional problem. - `is_moving`: Function to determine in each timestep if the particles are moving or not. Its - boolean return value is mandatory to determine if the neighborhood search will be updated. + boolean return value is mandatory to determine if the neighborhood search will be updated. # Keyword Arguments -- `moving_particles`: Indices of moving particles. Default is each particle in [`BoundarySPHSystem`](@ref). +- `moving_particles`: Indices of moving particles. Default is each particle in the system. -In the example below, `movement` describes particles moving in a circle as long as +In the example below, `motion` describes particles moving in a circle as long as the time is lower than `1.5`. # Examples @@ -156,13 +156,13 @@ the time is lower than `1.5`. movement_function(t) = SVector(cos(2pi*t), sin(2pi*t)) is_moving(t) = t < 1.5 -movement = BoundaryMovement(movement_function, is_moving) +motion = PrescribedMotion(movement_function, is_moving) # output -BoundaryMovement{typeof(movement_function), typeof(is_moving), Vector{Int64}}(movement_function, is_moving, Int64[]) +PrescribedMotion{typeof(movement_function), typeof(is_moving), Vector{Int64}}(movement_function, is_moving, Int64[]) ``` """ -struct BoundaryMovement{MF, IM, MP} +struct PrescribedMotion{MF, IM, MP} movement_function :: MF is_moving :: IM moving_particles :: MP # Vector{Int} @@ -170,7 +170,7 @@ 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 BoundaryMovement(movement_function, is_moving; moving_particles=nothing) +function PrescribedMotion(movement_function, is_moving; moving_particles=nothing) if !(movement_function(0.0) isa SVector) @warn "Return value of `movement_function` is not of type `SVector`. " * "Returning regular `Vector`s causes allocations and significant performance overhead." @@ -180,12 +180,12 @@ function BoundaryMovement(movement_function, is_moving; moving_particles=nothing # constructor to move all particles. moving_particles = isnothing(moving_particles) ? Int[] : vec(moving_particles) - return BoundaryMovement(movement_function, is_moving, moving_particles) + return PrescribedMotion(movement_function, is_moving, moving_particles) end create_cache_boundary(::Nothing, initial_condition) = (;) -function create_cache_boundary(::BoundaryMovement, initial_condition) +function create_cache_boundary(::PrescribedMotion, initial_condition) initial_coordinates = copy(initial_condition.coordinates) velocity = zero(initial_condition.velocity) acceleration = zero(initial_condition.velocity) @@ -211,7 +211,7 @@ end return system.cache.initial_coordinates end -function (movement::BoundaryMovement)(system, t, semi) +function (movement::PrescribedMotion)(system, t, semi) (; coordinates, cache) = system (; movement_function, is_moving, moving_particles) = movement (; acceleration, velocity) = cache @@ -235,7 +235,7 @@ function (movement::BoundaryMovement)(system, t, semi) return system end -function (movement::Nothing)(system::System, t, semi) +function (movement::Nothing)(system::AbstractSystem, t, semi) system.ismoving[] = false return system @@ -408,11 +408,11 @@ end # To incorporate the effect at boundaries in the viscosity term of the RHS the neighbor # viscosity model has to be used. @inline function viscosity_model(system::BoundarySPHSystem, - neighbor_system::FluidSystem) + neighbor_system::AbstractFluidSystem) return neighbor_system.viscosity end -function calculate_dt(v_ode, u_ode, cfl_number, system::BoundarySystem, semi) +function calculate_dt(v_ode, u_ode, cfl_number, system::AbstractBoundarySystem, semi) return Inf end diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index a20ed70ac1..33ba9a7883 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -57,7 +57,8 @@ See [Entropically Damped Artificial Compressibility for SPH](@ref edac) for more """ struct EntropicallyDampedSPHSystem{NDIMS, ELTYPE <: Real, IC, M, DC, K, V, COR, PF, TV, - AVGP, ST, SRFT, SRFN, B, PR, C} <: FluidSystem{NDIMS} + AVGP, ST, SRFT, SRFN, B, PR, + C} <: AbstractFluidSystem{NDIMS} initial_condition :: IC mass :: M # Vector{ELTYPE}: [particle] density_calculator :: DC diff --git a/src/schemes/fluid/fluid.jl b/src/schemes/fluid/fluid.jl index b70f33d29a..ed8e19780f 100644 --- a/src/schemes/fluid/fluid.jl +++ b/src/schemes/fluid/fluid.jl @@ -3,7 +3,7 @@ # of newly activated particles in a callback. # DO NOT use outside a callback. OrdinaryDiffEq does not allow changing `v` and `u` # outside of callbacks. -@inline function set_particle_density!(v, system::FluidSystem, particle, density) +@inline function set_particle_density!(v, system::AbstractFluidSystem, particle, density) current_density(v, system)[particle] = density return v @@ -14,7 +14,7 @@ end # of newly activated particles in a callback. # DO NOT use outside a callback. OrdinaryDiffEq does not allow changing `v` and `u` # outside of callbacks. -@inline function set_particle_pressure!(v, system::FluidSystem, particle, pressure) +@inline function set_particle_pressure!(v, system::AbstractFluidSystem, particle, pressure) current_pressure(v, system)[particle] = pressure return v @@ -41,17 +41,19 @@ function create_cache_refinement(initial_condition, refinement, smoothing_length # TODO: If refinement is not `Nothing` and `correction` is not `Nothing`, then throw an error end -@propagate_inbounds hydrodynamic_mass(system::FluidSystem, particle) = system.mass[particle] +@propagate_inbounds function hydrodynamic_mass(system::AbstractFluidSystem, particle) + return system.mass[particle] +end -function smoothing_length(system::FluidSystem, particle) +function smoothing_length(system::AbstractFluidSystem, particle) return smoothing_length(system, system.particle_refinement, particle) end -function smoothing_length(system::FluidSystem, ::Nothing, particle) +function smoothing_length(system::AbstractFluidSystem, ::Nothing, particle) return system.cache.smoothing_length end -function initial_smoothing_length(system::FluidSystem) +function initial_smoothing_length(system::AbstractFluidSystem) return initial_smoothing_length(system, system.particle_refinement) end @@ -63,7 +65,7 @@ function initial_smoothing_length(system, refinement) system.initial_condition.particle_spacing end -@inline function particle_spacing(system::FluidSystem, particle) +@inline function particle_spacing(system::AbstractFluidSystem, particle) return particle_spacing(system, system.particle_refinement, particle) end @@ -74,7 +76,7 @@ end return smoothing_length(system, particle) / smoothing_length_factor end -function write_u0!(u0, system::FluidSystem) +function write_u0!(u0, system::AbstractFluidSystem) (; initial_condition) = system # This is as fast as a loop with `@inbounds`, but it's GPU-compatible @@ -84,7 +86,7 @@ function write_u0!(u0, system::FluidSystem) return u0 end -function write_v0!(v0, system::FluidSystem) +function write_v0!(v0, system::AbstractFluidSystem) # This is as fast as a loop with `@inbounds`, but it's GPU-compatible indices = CartesianIndices(system.initial_condition.velocity) copyto!(v0, indices, system.initial_condition.velocity, indices) @@ -94,20 +96,22 @@ function write_v0!(v0, system::FluidSystem) return v0 end -write_v0!(v0, system::FluidSystem, _) = v0 +write_v0!(v0, system::AbstractFluidSystem, _) = v0 # To account for boundary effects in the viscosity term of the RHS, use the viscosity model # of the neighboring particle systems. -@inline function viscosity_model(system::FluidSystem, neighbor_system::FluidSystem) +@inline function viscosity_model(system::AbstractFluidSystem, + neighbor_system::AbstractFluidSystem) return neighbor_system.viscosity end -@inline function viscosity_model(system::FluidSystem, neighbor_system::BoundarySystem) +@inline function viscosity_model(system::AbstractFluidSystem, + neighbor_system::AbstractBoundarySystem) return neighbor_system.boundary_model.viscosity end -@inline system_state_equation(system::FluidSystem) = system.state_equation +@inline system_state_equation(system::AbstractFluidSystem) = system.state_equation function compute_density!(system, u, u_ode, semi, ::ContinuityDensity) # No density update with `ContinuityDensity` @@ -121,7 +125,7 @@ function compute_density!(system, u, u_ode, semi, ::SummationDensity) summation_density!(system, semi, u, u_ode, density) end -function calculate_dt(v_ode, u_ode, cfl_number, system::FluidSystem, semi) +function calculate_dt(v_ode, u_ode, cfl_number, system::AbstractFluidSystem, semi) (; viscosity, acceleration, surface_tension) = system # TODO @@ -162,7 +166,7 @@ function calculate_dt(v_ode, u_ode, cfl_number, system::FluidSystem, semi) return dt end -@inline function surface_tension_model(system::FluidSystem) +@inline function surface_tension_model(system::AbstractFluidSystem) return system.surface_tension end @@ -170,7 +174,7 @@ end return nothing end -@inline function surface_normal_method(system::FluidSystem) +@inline function surface_normal_method(system::AbstractFluidSystem) return system.surface_normal_method end @@ -178,7 +182,7 @@ end return nothing end -function system_data(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi) +function system_data(system::AbstractFluidSystem, dv_ode, du_ode, v_ode, u_ode, semi) (; mass) = system dv = wrap_v(dv_ode, system, semi) @@ -194,7 +198,7 @@ function system_data(system::FluidSystem, dv_ode, du_ode, v_ode, u_ode, semi) return (; coordinates, velocity, mass, density, pressure, acceleration) end -function available_data(::FluidSystem) +function available_data(::AbstractFluidSystem) return (:coordinates, :velocity, :mass, :density, :pressure, :acceleration) end diff --git a/src/schemes/fluid/surface_normal_sph.jl b/src/schemes/fluid/surface_normal_sph.jl index da2d50f327..13fb53f5a2 100644 --- a/src/schemes/fluid/surface_normal_sph.jl +++ b/src/schemes/fluid/surface_normal_sph.jl @@ -33,7 +33,7 @@ function create_cache_surface_normal(::ColorfieldSurfaceNormal, ELTYPE, NDIMS, n return (; surface_normal, neighbor_count, colorfield, correction_factor) end -@inline function surface_normal(particle_system::FluidSystem, particle) +@inline function surface_normal(particle_system::AbstractFluidSystem, particle) (; cache) = particle_system return extract_svector(cache.surface_normal, particle_system, particle) end @@ -47,7 +47,8 @@ end # Section 2.2 in Akinci et al. 2013 "Versatile Surface Tension and Adhesion for SPH Fluids" # and Section 5 in Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics". -function calc_normal!(system::FluidSystem, neighbor_system::FluidSystem, u_system, v, +function calc_normal!(system::AbstractFluidSystem, neighbor_system::AbstractFluidSystem, + u_system, v, v_neighbor_system, u_neighbor_system, semi, surface_normal_method, ::ColorfieldSurfaceNormal) (; cache) = system @@ -76,9 +77,9 @@ end # Section 2.2 in Akinci et al. 2013 "Versatile Surface Tension and Adhesion for SPH Fluids" # Note: This is the simplest form of normal approximation commonly used in SPH and comes # with serious deficits in accuracy especially at corners, small neighborhoods and boundaries -function calc_normal!(system::FluidSystem, neighbor_system::BoundarySystem, u_system, - v, v_neighbor_system, u_neighbor_system, semi, surface_normal_method, - neighbor_surface_normal_method) +function calc_normal!(system::AbstractFluidSystem, neighbor_system::AbstractBoundarySystem, + u_system, v, v_neighbor_system, u_neighbor_system, semi, + surface_normal_method, neighbor_surface_normal_method) (; cache) = system (; colorfield, initial_colorfield) = neighbor_system.boundary_model.cache (; boundary_contact_threshold) = surface_normal_method @@ -123,7 +124,7 @@ function calc_normal!(system::FluidSystem, neighbor_system::BoundarySystem, u_sy return system end -function remove_invalid_normals!(system::FluidSystem, surface_tension, +function remove_invalid_normals!(system::AbstractFluidSystem, surface_tension, surface_normal_method) (; cache) = system @@ -139,7 +140,7 @@ function remove_invalid_normals!(system::FluidSystem, surface_tension, end # See Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics" -function remove_invalid_normals!(system::FluidSystem, +function remove_invalid_normals!(system::AbstractFluidSystem, surface_tension::Union{SurfaceTensionMorris, SurfaceTensionMomentumMorris}, surface_normal_method::ColorfieldSurfaceNormal) @@ -185,7 +186,7 @@ function compute_surface_normal!(system, surface_normal_method, v, u, v_ode, u_o return system end -function compute_surface_normal!(system::FluidSystem, +function compute_surface_normal!(system::AbstractFluidSystem, surface_normal_method_::ColorfieldSurfaceNormal, v, u, v_ode, u_ode, semi, t) (; cache, surface_tension) = system @@ -214,8 +215,8 @@ function calc_curvature!(system, neighbor_system, u_system, v, end # Section 5 in Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics" -function calc_curvature!(system::FluidSystem, neighbor_system::FluidSystem, u_system, v, - v_neighbor_system, u_neighbor_system, semi, +function calc_curvature!(system::AbstractFluidSystem, neighbor_system::AbstractFluidSystem, + u_system, v, v_neighbor_system, u_neighbor_system, semi, surface_normal_method::ColorfieldSurfaceNormal, neighbor_surface_normal_method::ColorfieldSurfaceNormal) (; cache) = system @@ -260,8 +261,9 @@ function compute_curvature!(system, surface_tension, v, u, v_ode, u_ode, semi, t return system end -function compute_curvature!(system::FluidSystem, surface_tension::SurfaceTensionMorris, v, - u, v_ode, u_ode, semi, t) +function compute_curvature!(system::AbstractFluidSystem, + surface_tension::SurfaceTensionMorris, + v, u, v_ode, u_ode, semi, t) (; cache, surface_tension) = system # Reset surface curvature diff --git a/src/schemes/fluid/surface_tension.jl b/src/schemes/fluid/surface_tension.jl index 3d4025f3e1..f96b917e8e 100644 --- a/src/schemes/fluid/surface_tension.jl +++ b/src/schemes/fluid/surface_tension.jl @@ -1,5 +1,5 @@ -abstract type SurfaceTension end -abstract type AkinciTypeSurfaceTension <: SurfaceTension end +abstract type AbstractSurfaceTension end +abstract type AkinciTypeSurfaceTension <: AbstractSurfaceTension end @doc raw""" CohesionForceAkinci(surface_tension_coefficient=1.0) @@ -58,7 +58,7 @@ See [`surface_tension`](@ref) for more details. - `surface_tension_coefficient=1.0`: Adjusts the magnitude of the surface tension forces, enabling tuning of fluid surface behaviors in simulations. """ -struct SurfaceTensionMorris{ELTYPE} <: SurfaceTension +struct SurfaceTensionMorris{ELTYPE} <: AbstractSurfaceTension surface_tension_coefficient::ELTYPE function SurfaceTensionMorris(; surface_tension_coefficient=1.0) @@ -90,7 +90,7 @@ See [`surface_tension`](@ref) for more details. - `surface_tension_coefficient=1.0`: A parameter to adjust the strength of surface tension forces, allowing fine-tuning to replicate physical behavior. """ -struct SurfaceTensionMomentumMorris{ELTYPE} <: SurfaceTension +struct SurfaceTensionMomentumMorris{ELTYPE} <: AbstractSurfaceTension surface_tension_coefficient::ELTYPE function SurfaceTensionMomentumMorris(; surface_tension_coefficient=1.0) @@ -106,7 +106,7 @@ function create_cache_surface_tension(::SurfaceTensionMomentumMorris, ELTYPE, ND return (; stress_tensor, delta_s) end -@inline function stress_tensor(particle_system::FluidSystem, particle) +@inline function stress_tensor(particle_system::AbstractFluidSystem, particle) return extract_smatrix(particle_system.cache.stress_tensor, particle_system, particle) end @@ -170,9 +170,10 @@ end @inline function surface_tension_force(surface_tension_a::CohesionForceAkinci, surface_tension_b::CohesionForceAkinci, - particle_system::FluidSystem, - neighbor_system::FluidSystem, particle, neighbor, - pos_diff, distance, rho_a, rho_b, grad_kernel) + particle_system::AbstractFluidSystem, + neighbor_system::AbstractFluidSystem, + particle, neighbor, pos_diff, distance, + rho_a, rho_b, grad_kernel) # No cohesion with oneself distance < sqrt(eps()) && return zero(pos_diff) @@ -185,8 +186,9 @@ end @inline function surface_tension_force(surface_tension_a::SurfaceTensionAkinci, surface_tension_b::SurfaceTensionAkinci, - particle_system::FluidSystem, - neighbor_system::FluidSystem, particle, neighbor, + particle_system::AbstractFluidSystem, + neighbor_system::AbstractFluidSystem, particle, + neighbor, pos_diff, distance, rho_a, rho_b, grad_kernel) (; smoothing_kernel) = particle_system (; surface_tension_coefficient) = surface_tension_a @@ -207,9 +209,10 @@ end @inline function surface_tension_force(surface_tension_a::SurfaceTensionMorris, surface_tension_b::SurfaceTensionMorris, - particle_system::FluidSystem, - neighbor_system::FluidSystem, particle, neighbor, - pos_diff, distance, rho_a, rho_b, grad_kernel) + particle_system::AbstractFluidSystem, + neighbor_system::AbstractFluidSystem, + particle, neighbor, pos_diff, distance, + rho_a, rho_b, grad_kernel) (; surface_tension_coefficient) = surface_tension_a # No surface tension with oneself @@ -226,7 +229,8 @@ function compute_stress_tensors!(system, surface_tension, v, u, v_ode, u_ode, se end # Section 6 in Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics" -function compute_stress_tensors!(system::FluidSystem, ::SurfaceTensionMomentumMorris, +function compute_stress_tensors!(system::AbstractFluidSystem, + ::SurfaceTensionMomentumMorris, v, u, v_ode, u_ode, semi, t) (; cache) = system (; delta_s, stress_tensor) = cache @@ -275,9 +279,10 @@ end @inline function surface_tension_force(surface_tension_a::SurfaceTensionMomentumMorris, surface_tension_b::SurfaceTensionMomentumMorris, - particle_system::FluidSystem, - neighbor_system::FluidSystem, particle, neighbor, - pos_diff, distance, rho_a, rho_b, grad_kernel) + particle_system::AbstractFluidSystem, + neighbor_system::AbstractFluidSystem, + particle, neighbor, pos_diff, distance, + rho_a, rho_b, grad_kernel) (; surface_tension_coefficient) = surface_tension_a # No surface tension with oneself @@ -292,8 +297,8 @@ end end @inline function adhesion_force(surface_tension::AkinciTypeSurfaceTension, - particle_system::FluidSystem, - neighbor_system::BoundarySystem, particle, neighbor, + particle_system::AbstractFluidSystem, + neighbor_system::AbstractBoundarySystem, particle, neighbor, pos_diff, distance) (; adhesion_coefficient) = neighbor_system diff --git a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl index 60d1967442..552f9a2b45 100644 --- a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl +++ b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl @@ -1,5 +1,5 @@ @doc raw""" - DensityDiffusion + AbstractDensityDiffusion An abstract supertype of all density diffusion formulations. @@ -13,7 +13,7 @@ Currently, the following formulations are available: See [Density Diffusion](@ref density_diffusion) for a comparison and more details. """ -abstract type DensityDiffusion end +abstract type AbstractDensityDiffusion end # Most density diffusion formulations don't need updating function update!(density_diffusion, v, u, system, semi) @@ -25,18 +25,18 @@ end The commonly used density diffusion term by [Molteni (2009)](@cite Molteni2009). -The term ``\psi_{ab}`` in the continuity equation in [`DensityDiffusion`](@ref) is defined -by +The term ``\psi_{ab}`` in the continuity equation in +[`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) is defined by ```math \psi_{ab} = 2(\rho_a - \rho_b) \frac{r_{ab}}{\Vert r_{ab} \Vert^2}, ``` where ``\rho_a`` and ``\rho_b`` denote the densities of particles ``a`` and ``b`` respectively and ``r_{ab} = r_a - r_b`` is the difference of the coordinates of particles ``a`` and ``b``. -See [`DensityDiffusion`](@ref) for an overview and comparison of implemented density -diffusion terms. +See [`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) +for an overview and comparison of implemented density diffusion terms. """ -struct DensityDiffusionMolteniColagrossi{ELTYPE} <: DensityDiffusion +struct DensityDiffusionMolteniColagrossi{ELTYPE} <: AbstractDensityDiffusion delta::ELTYPE function DensityDiffusionMolteniColagrossi(; delta) @@ -54,8 +54,8 @@ end A density diffusion term by [Ferrari (2009)](@cite Ferrari2009). -The term ``\psi_{ab}`` in the continuity equation in [`DensityDiffusion`](@ref) is defined -by +The term ``\psi_{ab}`` in the continuity equation in +[`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) is defined by ```math \psi_{ab} = \frac{\rho_a - \rho_b}{h_a + h_b} \frac{r_{ab}}{\Vert r_{ab} \Vert}, ``` @@ -63,10 +63,10 @@ where ``\rho_a`` and ``\rho_b`` denote the densities of particles ``a`` and ``b` ``r_{ab} = r_a - r_b`` is the difference of the coordinates of particles ``a`` and ``b`` and ``h_a`` and ``h_b`` are the smoothing lengths of particles ``a`` and ``b`` respectively. -See [`DensityDiffusion`](@ref) for an overview and comparison of implemented density -diffusion terms. +See [`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) +for an overview and comparison of implemented density diffusion terms. """ -struct DensityDiffusionFerrari <: DensityDiffusion +struct DensityDiffusionFerrari <: AbstractDensityDiffusion delta::Int # δ is always 1 in this formulation @@ -87,8 +87,8 @@ The commonly used density diffusion terms by [Antuono (2010)](@cite Antuono2010) δ-SPH. The density diffusion term by [Molteni (2009)](@cite Molteni2009) is extended by a second term, which is nicely written down by [Antuono (2012)](@cite Antuono2012). -The term ``\psi_{ab}`` in the continuity equation in [`DensityDiffusion`](@ref) is defined -by +The term ``\psi_{ab}`` in the continuity equation in +[`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) is defined by ```math \psi_{ab} = 2\left(\rho_a - \rho_b - \frac{1}{2}\big(\nabla\rho^L_a + \nabla\rho^L_b\big) \cdot r_{ab}\right) \frac{r_{ab}}{\Vert r_{ab} \Vert^2}, @@ -105,10 +105,10 @@ L_a := \left( -\sum_{b} V_b r_{ab} \otimes \nabla W_{ab} \right)^{-1} \in \R^{d ``` where ``d`` is the number of dimensions. -See [`DensityDiffusion`](@ref) for an overview and comparison of implemented density -diffusion terms. +See [`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion) +for an overview and comparison of implemented density diffusion terms. """ -struct DensityDiffusionAntuono{NDIMS, ELTYPE, ARRAY2D, ARRAY3D} <: DensityDiffusion +struct DensityDiffusionAntuono{NDIMS, ELTYPE, ARRAY2D, ARRAY3D} <: AbstractDensityDiffusion delta :: ELTYPE correction_matrix :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] normalized_density_gradient :: ARRAY2D # Array{ELTYPE, 2}: [i, particle] @@ -207,10 +207,12 @@ function update!(density_diffusion::DensityDiffusionAntuono, v, u, system, semi) return density_diffusion end -@propagate_inbounds function density_diffusion!(dv, density_diffusion::DensityDiffusion, +@propagate_inbounds function density_diffusion!(dv, + density_diffusion::AbstractDensityDiffusion, v_particle_system, particle, neighbor, pos_diff, distance, m_b, rho_a, rho_b, - particle_system::FluidSystem, grad_kernel) + particle_system::AbstractFluidSystem, + grad_kernel) # Density diffusion terms are all zero for distance zero distance < sqrt(eps(typeof(distance))) && return diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index 46a51cdb8c..21cb39a9f2 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -30,7 +30,8 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method. - `acceleration`: Acceleration vector for the system. (default: zero vector) - `viscosity`: Viscosity model for this system (default: no viscosity). See [`ArtificialViscosityMonaghan`](@ref) or [`ViscosityAdami`](@ref). -- `density_diffusion`: Density diffusion terms for this system. See [`DensityDiffusion`](@ref). +- `density_diffusion`: Density diffusion terms for this system. + See [`AbstractDensityDiffusion`](@ref TrixiParticles.AbstractDensityDiffusion). - `pressure_acceleration`: Pressure acceleration formulation for this system. By default, the correct formulation is chosen based on the density calculator and the correction method. @@ -60,7 +61,8 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method. - `color_value`: The value used to initialize the color of particles in the system. """ struct WeaklyCompressibleSPHSystem{NDIMS, ELTYPE <: Real, IC, MA, P, DC, SE, K, V, DD, COR, - PF, SC, ST, B, SRFT, SRFN, PR, C} <: FluidSystem{NDIMS} + PF, SC, ST, B, SRFT, SRFN, PR, + C} <: AbstractFluidSystem{NDIMS} initial_condition :: IC mass :: MA # Array{ELTYPE, 1} pressure :: P # Array{ELTYPE, 1} @@ -432,7 +434,7 @@ end extract_smatrix(system.cache.correction_matrix, system, particle) end -@inline function curvature(particle_system::FluidSystem, particle) +@inline function curvature(particle_system::AbstractFluidSystem, particle) (; cache) = particle_system return cache.curvature[particle] end diff --git a/src/schemes/schemes.jl b/src/schemes/schemes.jl index b81a7768b4..a34a403cd0 100644 --- a/src/schemes/schemes.jl +++ b/src/schemes/schemes.jl @@ -2,8 +2,8 @@ # interactions between the different system types. include("fluid/fluid.jl") include("boundary/boundary.jl") -include("solid/total_lagrangian_sph/total_lagrangian_sph.jl") -include("solid/discrete_element_method/discrete_element_method.jl") +include("structure/total_lagrangian_sph/total_lagrangian_sph.jl") +include("structure/discrete_element_method/discrete_element_method.jl") # Monaghan-Kajtar repulsive boundary particles require the `BoundarySPHSystem` # and the `TotalLagrangianSPHSystem`. include("boundary/monaghan_kajtar/monaghan_kajtar.jl") @@ -12,5 +12,5 @@ include("boundary/monaghan_kajtar/monaghan_kajtar.jl") include("fluid/weakly_compressible_sph/rhs.jl") include("fluid/entropically_damped_sph/rhs.jl") include("boundary/rhs.jl") -include("solid/total_lagrangian_sph/rhs.jl") -include("solid/discrete_element_method/rhs.jl") +include("structure/total_lagrangian_sph/rhs.jl") +include("structure/discrete_element_method/rhs.jl") diff --git a/src/schemes/solid/discrete_element_method/contact_models.jl b/src/schemes/structure/discrete_element_method/contact_models.jl similarity index 97% rename from src/schemes/solid/discrete_element_method/contact_models.jl rename to src/schemes/structure/discrete_element_method/contact_models.jl index 398664df0b..5a0220f42c 100644 --- a/src/schemes/solid/discrete_element_method/contact_models.jl +++ b/src/schemes/structure/discrete_element_method/contact_models.jl @@ -1,5 +1,5 @@ # Define an abstract type for contact models. -abstract type ContactModel end +abstract type AbstractContactModel end @doc raw""" HertzContactModel(; elastic_modulus, poissons_ratio) @@ -49,7 +49,7 @@ The total normal force is ``F_n = F_{\text{elastic}} + F_{\text{damping}}``. - `elastic_modulus::Float64`: Material Young's modulus ``E``. - `poissons_ratio::Float64`: Material Poisson's ratio ``\nu``. """ -struct HertzContactModel{ELTYPE <: Real} <: ContactModel +struct HertzContactModel{ELTYPE <: Real} <: AbstractContactModel elastic_modulus::ELTYPE # Material elastic modulus poissons_ratio::ELTYPE # Material Poisson's ratio end @@ -141,7 +141,7 @@ contacting objects. # Fields - `normal_stiffness::Real`: Constant spring stiffness ``k_n`` for the normal direction. """ -struct LinearContactModel{ELTYPE <: Real} <: ContactModel +struct LinearContactModel{ELTYPE <: Real} <: AbstractContactModel normal_stiffness::ELTYPE end diff --git a/src/schemes/solid/discrete_element_method/discrete_element_method.jl b/src/schemes/structure/discrete_element_method/discrete_element_method.jl similarity index 100% rename from src/schemes/solid/discrete_element_method/discrete_element_method.jl rename to src/schemes/structure/discrete_element_method/discrete_element_method.jl diff --git a/src/schemes/solid/discrete_element_method/rhs.jl b/src/schemes/structure/discrete_element_method/rhs.jl similarity index 100% rename from src/schemes/solid/discrete_element_method/rhs.jl rename to src/schemes/structure/discrete_element_method/rhs.jl diff --git a/src/schemes/solid/discrete_element_method/system.jl b/src/schemes/structure/discrete_element_method/system.jl similarity index 98% rename from src/schemes/solid/discrete_element_method/system.jl rename to src/schemes/structure/discrete_element_method/system.jl index c2b27ca75b..5f6220d9e4 100644 --- a/src/schemes/solid/discrete_element_method/system.jl +++ b/src/schemes/structure/discrete_element_method/system.jl @@ -29,7 +29,8 @@ specified material properties and contact mechanics. ## References [Bicanic2004](@cite), [Cundall1979](@cite), [DiRenzo2004](@cite) """ -struct DEMSystem{NDIMS, ELTYPE <: Real, IC, ARRAY1D, ST, CM} <: SolidSystem{NDIMS} +struct DEMSystem{NDIMS, ELTYPE <: Real, IC, ARRAY1D, ST, + CM} <: AbstractStructureSystem{NDIMS} initial_condition :: IC mass :: ARRAY1D # [particle] radius :: ARRAY1D # [particle] diff --git a/src/schemes/solid/total_lagrangian_sph/penalty_force.jl b/src/schemes/structure/total_lagrangian_sph/penalty_force.jl similarity index 100% rename from src/schemes/solid/total_lagrangian_sph/penalty_force.jl rename to src/schemes/structure/total_lagrangian_sph/penalty_force.jl diff --git a/src/schemes/solid/total_lagrangian_sph/rhs.jl b/src/schemes/structure/total_lagrangian_sph/rhs.jl similarity index 86% rename from src/schemes/solid/total_lagrangian_sph/rhs.jl rename to src/schemes/structure/total_lagrangian_sph/rhs.jl index e804263427..cb02c7ea01 100644 --- a/src/schemes/solid/total_lagrangian_sph/rhs.jl +++ b/src/schemes/structure/total_lagrangian_sph/rhs.jl @@ -1,23 +1,23 @@ -# Solid-solid interaction +# Structure-structure interaction function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::TotalLagrangianSPHSystem, neighbor_system::TotalLagrangianSPHSystem, semi) - # Different solids do not interact with each other (yet) + # Different structures do not interact with each other (yet) particle_system !== neighbor_system && return dv - interact_solid_solid!(dv, v_particle_system, particle_system, semi) + interact_structure_structure!(dv, v_particle_system, particle_system, semi) end # Function barrier without dispatch for unit testing -@inline function interact_solid_solid!(dv, v_system, system, semi) +@inline function interact_structure_structure!(dv, v_system, system, semi) (; penalty_force) = system # Everything here is done in the initial coordinates system_coords = initial_coordinates(system) # Loop over all pairs of particles and neighbors within the kernel cutoff. - # For solid-solid interaction, this has to happen in the initial coordinates. + # For structure-structure interaction, this has to happen in the initial coordinates. foreach_point_neighbor(system, system, system_coords, system_coords, semi; points=each_moving_particle(system)) do particle, neighbor, initial_pos_diff, @@ -63,11 +63,11 @@ end return dv end -# Solid-fluid interaction +# Structure-fluid interaction function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::TotalLagrangianSPHSystem, - neighbor_system::FluidSystem, semi) + neighbor_system::AbstractFluidSystem, semi) sound_speed = system_sound_speed(neighbor_system) system_coords = current_coordinates(u_particle_system, particle_system) @@ -83,12 +83,12 @@ function interact!(dv, v_particle_system, u_particle_system, # Only consider particles with a distance > 0. distance < sqrt(eps()) && return - # Apply the same force to the solid particle - # that the fluid particle experiences due to the solid particle. - # Note that the same arguments are passed here as in fluid-solid interact!, + # Apply the same force to the structure particle + # that the fluid particle experiences due to the structure particle. + # Note that the same arguments are passed here as in fluid-structure interact!, # except that pos_diff has a flipped sign. # - # In fluid-solid interaction, use the "hydrodynamic mass" of the solid particles + # In fluid-structure interaction, use the "hydrodynamic mass" of the structure particles # corresponding to the rest density of the fluid and not the material density. m_a = hydrodynamic_mass(particle_system, particle) m_b = hydrodynamic_mass(neighbor_system, neighbor) @@ -97,19 +97,19 @@ function interact!(dv, v_particle_system, u_particle_system, rho_b = current_density(v_neighbor_system, neighbor_system, neighbor) # Use kernel from the fluid system in order to get the same force here in - # solid-fluid interaction as for fluid-solid interaction. + # structure-fluid interaction as for fluid-structure interaction. # TODO this will not use corrections if the fluid uses corrections. grad_kernel = smoothing_kernel_grad(neighbor_system, pos_diff, distance, particle) - # In fluid-solid interaction, use the "hydrodynamic pressure" of the solid particles + # In fluid-structure interaction, use the "hydrodynamic pressure" of the structure particles # corresponding to the chosen boundary model. p_a = current_pressure(v_particle_system, particle_system, particle) p_b = current_pressure(v_neighbor_system, neighbor_system, neighbor) # Particle and neighbor (and corresponding systems and all corresponding quantities) # are switched in the following two calls. - # This way, we obtain the exact same force as for the fluid-solid interaction, - # but with a flipped sign (because `pos_diff` is flipped compared to fluid-solid). + # This way, we obtain the exact same force as for the fluid-structure interaction, + # but with a flipped sign (because `pos_diff` is flipped compared to fluid-structure). dv_boundary = pressure_acceleration(neighbor_system, particle_system, neighbor, particle, m_b, m_a, p_b, p_a, rho_b, rho_a, pos_diff, @@ -125,9 +125,9 @@ function interact!(dv, v_particle_system, u_particle_system, for i in 1:ndims(particle_system) # Multiply `dv` (acceleration on fluid particle b) by the mass of - # particle b to obtain the same force as for the fluid-solid interaction. + # particle b to obtain the same force as for the fluid-structure interaction. # Divide by the material mass of particle a to obtain the acceleration - # of solid particle a. + # of structure particle a. dv[i, particle] += dv_particle[i] * m_b / particle_system.mass[particle] end @@ -144,7 +144,7 @@ end particle, neighbor, pos_diff, distance, m_b, rho_a, rho_b, particle_system::TotalLagrangianSPHSystem, - neighbor_system::FluidSystem, + neighbor_system::AbstractFluidSystem, grad_kernel) return dv end @@ -153,7 +153,7 @@ end particle, neighbor, pos_diff, distance, m_b, rho_a, rho_b, particle_system::TotalLagrangianSPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, - neighbor_system::FluidSystem, + neighbor_system::AbstractFluidSystem, grad_kernel) fluid_density_calculator = neighbor_system.density_calculator @@ -165,7 +165,7 @@ end grad_kernel, particle) end -# Solid-boundary interaction +# Structure-boundary interaction function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::TotalLagrangianSPHSystem, diff --git a/src/schemes/solid/total_lagrangian_sph/system.jl b/src/schemes/structure/total_lagrangian_sph/system.jl similarity index 93% rename from src/schemes/solid/total_lagrangian_sph/system.jl rename to src/schemes/structure/total_lagrangian_sph/system.jl index 60fc7a3990..95d42aecf0 100644 --- a/src/schemes/solid/total_lagrangian_sph/system.jl +++ b/src/schemes/structure/total_lagrangian_sph/system.jl @@ -2,7 +2,7 @@ TotalLagrangianSPHSystem(initial_condition, smoothing_kernel, smoothing_length, young_modulus, poisson_ratio; - n_fixed_particles=0, boundary_model=nothing, + n_clamped_particles=0, boundary_model=nothing, acceleration=ntuple(_ -> 0.0, NDIMS), penalty_force=nothing, source_terms=nothing) @@ -23,9 +23,9 @@ See [Total Lagrangian SPH](@ref tlsph) for more details on the method. See [Smoothing Kernels](@ref smoothing_kernel). # Keyword Arguments -- `n_fixed_particles`: Number of fixed particles which are used to clamp the structure - particles. Note that the fixed particles must be the **last** - particles in the `InitialCondition`. See the info box below. +- `n_clamped_particles`: Number of clamped particles which are fixed and not integrated + to clamp the structure. Note that the clamped particles must be the **last** + particles in the `InitialCondition`. See the info box below. - `boundary_model`: Boundary model to compute the hydrodynamic density and pressure for fluid-structure interaction (see [Boundary Models](@ref boundary_models)). - `penalty_force`: Penalty force to ensure regular particle position under large deformations @@ -40,10 +40,10 @@ See [Total Lagrangian SPH](@ref tlsph) for more details on the method. See, for example, [`SourceTermDamping`](@ref). !!! note - The fixed particles must be the **last** particles in the `InitialCondition`. + The clamped particles must be the **last** particles in the `InitialCondition`. To do so, e.g. use the `union` function: - ```jldoctest; output = false, setup = :(fixed_particles = RectangularShape(0.1, (1, 4), (0.0, 0.0), density=1.0); beam = RectangularShape(0.1, (3, 4), (0.1, 0.0), density=1.0)) - solid = union(beam, fixed_particles) + ```jldoctest; output = false, setup = :(clamped_particles = RectangularShape(0.1, (1, 4), (0.0, 0.0), density=1.0); beam = RectangularShape(0.1, (3, 4), (0.1, 0.0), density=1.0)) + structure = union(beam, clamped_particles) # output ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -54,10 +54,11 @@ See [Total Lagrangian SPH](@ref tlsph) for more details on the method. │ particle spacing: ………………………………… 0.1 │ └──────────────────────────────────────────────────────────────────────────────────────────────────┘ ``` - where `beam` and `fixed_particles` are of type `InitialCondition`. + where `beam` and `clamped_particles` are of type `InitialCondition`. """ struct TotalLagrangianSPHSystem{BM, NDIMS, ELTYPE <: Real, IC, ARRAY1D, ARRAY2D, ARRAY3D, - YM, PR, LL, LM, K, PF, V, ST} <: SolidSystem{NDIMS} + YM, PR, LL, LM, K, PF, V, + ST} <: AbstractStructureSystem{NDIMS} initial_condition :: IC initial_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] current_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] @@ -83,7 +84,7 @@ end function TotalLagrangianSPHSystem(initial_condition, smoothing_kernel, smoothing_length, young_modulus, poisson_ratio; - n_fixed_particles=0, boundary_model=nothing, + n_clamped_particles=0, boundary_model=nothing, acceleration=ntuple(_ -> 0.0, ndims(smoothing_kernel)), penalty_force=nothing, viscosity=nothing, @@ -110,7 +111,7 @@ function TotalLagrangianSPHSystem(initial_condition, pk1_corrected = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, n_particles) deformation_grad = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, n_particles) - n_moving_particles = n_particles - n_fixed_particles + n_moving_particles = n_particles - n_clamped_particles lame_lambda = @. young_modulus * poisson_ratio / ((1 + poisson_ratio) * (1 - 2 * poisson_ratio)) @@ -153,11 +154,11 @@ function Base.show(io::IO, ::MIME"text/plain", system::TotalLagrangianSPHSystem) if get(io, :compact, false) show(io, system) else - n_fixed_particles = nparticles(system) - n_moving_particles(system) + n_clamped_particles = nparticles(system) - n_moving_particles(system) summary_header(io, "TotalLagrangianSPHSystem{$(ndims(system))}") summary_line(io, "total #particles", nparticles(system)) - summary_line(io, "#fixed particles", n_fixed_particles) + summary_line(io, "#clamped particles", n_clamped_particles) summary_line(io, "Young's modulus", display_param(system.young_modulus)) summary_line(io, "Poisson ratio", display_param(system.poisson_ratio)) summary_line(io, "smoothing kernel", system.smoothing_kernel |> typeof |> nameof) @@ -214,7 +215,7 @@ end return current_density(v, system.boundary_model, system) end -# In fluid-solid interaction, use the "hydrodynamic pressure" of the solid particles +# In fluid-structure interaction, use the "hydrodynamic pressure" of the structure particles # corresponding to the chosen boundary model. @inline function current_pressure(v, system::TotalLagrangianSPHSystem) return current_pressure(v, system.boundary_model, system) @@ -276,7 +277,7 @@ end function update_positions!(system::TotalLagrangianSPHSystem, v, u, v_ode, u_ode, semi, t) (; current_coordinates) = system - # `current_coordinates` stores the coordinates of both integrated and fixed particles. + # `current_coordinates` stores the coordinates of both integrated and clamped particles. # Copy the coordinates of the integrated particles from `u`. @threaded semi for particle in each_moving_particle(system) for i in 1:ndims(system) @@ -485,7 +486,7 @@ end return neighbor_system.viscosity end -@inline function viscosity_model(system::FluidSystem, +@inline function viscosity_model(system::AbstractFluidSystem, neighbor_system::TotalLagrangianSPHSystem) return neighbor_system.boundary_model.viscosity end diff --git a/src/schemes/solid/total_lagrangian_sph/total_lagrangian_sph.jl b/src/schemes/structure/total_lagrangian_sph/total_lagrangian_sph.jl similarity index 100% rename from src/schemes/solid/total_lagrangian_sph/total_lagrangian_sph.jl rename to src/schemes/structure/total_lagrangian_sph/total_lagrangian_sph.jl diff --git a/src/schemes/solid/total_lagrangian_sph/viscosity.jl b/src/schemes/structure/total_lagrangian_sph/viscosity.jl similarity index 100% rename from src/schemes/solid/total_lagrangian_sph/viscosity.jl rename to src/schemes/structure/total_lagrangian_sph/viscosity.jl diff --git a/test/examples/examples.jl b/test/examples/examples.jl index 5d73485c68..decb9ea711 100644 --- a/test/examples/examples.jl +++ b/test/examples/examples.jl @@ -3,19 +3,19 @@ @testset verbose=true "Examples" begin include("examples_fluid.jl") - @testset verbose=true "Solid" begin - @trixi_testset "solid/oscillating_beam_2d.jl" begin + @testset verbose=true "Structure" begin + @trixi_testset "structure/oscillating_beam_2d.jl" begin @trixi_test_nowarn trixi_include(@__MODULE__, - joinpath(examples_dir(), "solid", + joinpath(examples_dir(), "structure", "oscillating_beam_2d.jl"), tspan=(0.0, 0.1)) @test sol.retcode == ReturnCode.Success @test count_rhs_allocations(sol, semi) == 0 end - @trixi_testset "solid/oscillating_beam_2d.jl with penalty force and viscosity" begin + @trixi_testset "structure/oscillating_beam_2d.jl with penalty force and viscosity" begin @trixi_test_nowarn trixi_include(@__MODULE__, - joinpath(examples_dir(), "solid", + joinpath(examples_dir(), "structure", "oscillating_beam_2d.jl"), tspan=(0.0, 0.1), penalty_force=PenaltyForceGanzenmueller(alpha=0.1), diff --git a/test/examples/gpu.jl b/test/examples/gpu.jl index 3c4db90621..7ac1f663f6 100644 --- a/test/examples/gpu.jl +++ b/test/examples/gpu.jl @@ -411,28 +411,28 @@ end end end - @testset verbose=true "Solid" begin + @testset verbose=true "Structure" begin # TODO after https://github.com/trixi-framework/PointNeighbors.jl/pull/10 # is merged, there should be no need to use the `FullGridCellList`. - @trixi_testset "solid/oscillating_beam_2d.jl" begin + @trixi_testset "structure/oscillating_beam_2d.jl" begin # Import variables into scope trixi_include_changeprecision(Float32, @__MODULE__, - joinpath(examples_dir(), "solid", + joinpath(examples_dir(), "structure", "oscillating_beam_2d.jl"), sol=nothing, ode=nothing) # Neighborhood search with `FullGridCellList` for GPU compatibility - min_corner = minimum(solid.coordinates, dims=2) - max_corner = maximum(solid.coordinates, dims=2) + min_corner = minimum(structure.coordinates, dims=2) + max_corner = maximum(structure.coordinates, dims=2) cell_list = FullGridCellList(; min_corner, max_corner) - semi_fullgrid = Semidiscretization(solid_system, + semi_fullgrid = Semidiscretization(structure_system, neighborhood_search=GridNeighborhoodSearch{2}(; cell_list), parallelization_backend=Main.parallelization_backend) @trixi_test_nowarn trixi_include_changeprecision(Float32, @__MODULE__, joinpath(examples_dir(), - "solid", + "structure", "oscillating_beam_2d.jl"), tspan=(0.0f0, 0.1f0), semi=semi_fullgrid) @@ -458,7 +458,7 @@ end # is much finer than the fluid resolution. cell_list = FullGridCellList(; min_corner, max_corner) semi_fullgrid = Semidiscretization(fluid_system, boundary_system_tank, - boundary_system_gate, solid_system, + boundary_system_gate, structure_system, neighborhood_search=GridNeighborhoodSearch{2}(; cell_list), parallelization_backend=Main.parallelization_backend) diff --git a/test/general/buffer.jl b/test/general/buffer.jl index b953d7cfa8..9f32ac98a2 100644 --- a/test/general/buffer.jl +++ b/test/general/buffer.jl @@ -1,6 +1,6 @@ @testset verbose=true "`SystemBuffer`" begin # Mock fluid system - struct FluidSystemMock3 <: TrixiParticles.FluidSystem{2} end + struct FluidSystemMock3 <: TrixiParticles.AbstractFluidSystem{2} end TrixiParticles.initial_smoothing_length(system::FluidSystemMock3) = 1.0 TrixiParticles.nparticles(system::FluidSystemMock3) = 1 diff --git a/test/general/semidiscretization.jl b/test/general/semidiscretization.jl index 862f2ba37a..5621847a94 100644 --- a/test/general/semidiscretization.jl +++ b/test/general/semidiscretization.jl @@ -1,8 +1,8 @@ # Use `@trixi_testset` to isolate the mock functions in a separate namespace @trixi_testset "Semidiscretization" begin # Mock systems - struct System1 <: TrixiParticles.System{3} end - struct System2 <: TrixiParticles.System{3} end + struct System1 <: TrixiParticles.AbstractSystem{3} end + struct System2 <: TrixiParticles.AbstractSystem{3} end system1 = System1() system2 = System2() @@ -38,12 +38,12 @@ end @testset verbose=true "Check Configuration" begin - @testset verbose=true "Solid-Fluid Interaction" begin + @testset verbose=true "Structure-Fluid Interaction" begin # Mock boundary model struct BoundaryModelMock end # Mock fluid system - struct FluidSystemMock <: TrixiParticles.FluidSystem{2} + struct FluidSystemMock <: TrixiParticles.AbstractFluidSystem{2} surface_tension::Nothing FluidSystemMock() = new(nothing) end @@ -60,28 +60,29 @@ 1.0) # FSI without boundary model. - solid_system1 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0) + structure_system1 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0) error_str = "a boundary model for `TotalLagrangianSPHSystem` must be " * "specified when simulating a fluid-structure interaction." @test_throws ArgumentError(error_str) Semidiscretization(fluid_system, - solid_system1, + structure_system1, neighborhood_search=nothing) # FSI with boundary model - solid_system2 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0, - boundary_model=model_a) + structure_system2 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0, + boundary_model=model_a) - @test_nowarn TrixiParticles.check_configuration((solid_system2, fluid_system), + @test_nowarn TrixiParticles.check_configuration((structure_system2, + fluid_system), nothing) # FSI with wrong boundary model - solid_system3 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0, - boundary_model=model_b) + structure_system3 = TotalLagrangianSPHSystem(ic, kernel, 1.0, 1.0, 1.0, + boundary_model=model_b) error_str = "`BoundaryModelDummyParticles` with density calculator " * "`ContinuityDensity` is not yet supported for a `TotalLagrangianSPHSystem`" - @test_throws ArgumentError(error_str) Semidiscretization(solid_system3, + @test_throws ArgumentError(error_str) Semidiscretization(structure_system3, fluid_system, neighborhood_search=nothing) end diff --git a/test/io/read_vtk.jl b/test/io/read_vtk.jl index 8dfb626737..2c02b299c9 100644 --- a/test/io/read_vtk.jl +++ b/test/io/read_vtk.jl @@ -18,7 +18,7 @@ @test isapprox(expected_ic.pressure, test_ic.pressure, rtol=1e-5) end - @testset verbose=true "`FluidSystem`" begin + @testset verbose=true "`AbstractFluidSystem`" begin fluid_system = EntropicallyDampedSPHSystem(expected_ic, SchoenbergCubicSplineKernel{2}(), 1.5, 1.5) @@ -38,12 +38,12 @@ x = (; v_ode, u_ode) vu_ode = (; x) - # Write out `FluidSystem` Simulation-File + # Write out `AbstractFluidSystem` Simulation-File trixi2vtk(fluid_system, dvdu_ode, vu_ode, semi, 0.0, nothing; system_name="tmp_file_fluid", output_directory=tmp_dir, iter=1) - # Load `FluidSystem` Simulation-File + # Load `AbstractFluidSystem` Simulation-File test = vtk2trixi(joinpath(tmp_dir, "tmp_file_fluid_1.vtu")) @test isapprox(u, test.coordinates, rtol=1e-5) @@ -52,7 +52,7 @@ @test isapprox(fluid_system.cache.density, test.density, rtol=1e-5) end - @testset verbose=true "`BoundarySystem`" begin + @testset verbose=true "`BoundarySPHSystem`" begin boundary_model = BoundaryModelDummyParticles(expected_ic.density, expected_ic.mass, SummationDensity(), @@ -73,16 +73,16 @@ x = (; v_ode, u_ode) vu_ode = (; x) - # Write out `BoundarySystem` Simulation-File + # Write out `BoundarySPHSystem` Simulation-File trixi2vtk(boundary_system, dvdu_ode, vu_ode, semi, 0.0, nothing; system_name="tmp_file_boundary", output_directory=tmp_dir, iter=1) - # Load `BoundarySystem` Simulation-File + # Load `BoundarySPHSystem` Simulation-File test = vtk2trixi(joinpath(tmp_dir, "tmp_file_boundary_1.vtu")) @test isapprox(boundary_system.coordinates, test.coordinates, rtol=1e-5) - # The velocity is always zero for `BoundarySystem` + # The velocity is always zero for `BoundarySPHSystem` @test isapprox(zeros(size(test.velocity)), test.velocity, rtol=1e-5) @test isapprox(boundary_model.pressure, test.pressure, rtol=1e-5) @test isapprox(boundary_model.cache.density, test.density, rtol=1e-5) diff --git a/test/schemes/boundary/dummy_particles/rhs.jl b/test/schemes/boundary/dummy_particles/rhs.jl index ebc8216031..eb92fdf1b1 100644 --- a/test/schemes/boundary/dummy_particles/rhs.jl +++ b/test/schemes/boundary/dummy_particles/rhs.jl @@ -77,20 +77,20 @@ v_boundary_continuity = copy(initial_condition.density') # TLSPH system - solid_system = TotalLagrangianSPHSystem(initial_condition, smoothing_kernel, - smoothing_length, 0.0, 0.0, - boundary_model=boundary_model_continuity) + structure_system = TotalLagrangianSPHSystem(initial_condition, smoothing_kernel, + smoothing_length, 0.0, 0.0, + boundary_model=boundary_model_continuity) - # Positions of the solid particles are not used here - u_solid = zeros(0, TrixiParticles.nparticles(solid_system)) - v_solid = vcat(initial_condition.velocity, - initial_condition.density') + # Positions of the structure particles are not used here + u_structure = zeros(0, TrixiParticles.nparticles(structure_system)) + v_structure = vcat(initial_condition.velocity, + initial_condition.density') systems = Dict( "Fluid-Fluid" => second_fluid_system, "Fluid-BoundaryDummyPressureZeroing" => boundary_system_zeroing, "Fluid-BoundaryDummyContinuityDensity" => boundary_system_continuity, - "Fluid-TLSPH" => solid_system + "Fluid-TLSPH" => structure_system ) if density_calculator isa SummationDensity @@ -105,7 +105,7 @@ "Fluid-BoundaryDummyContinuityDensity" => (v_boundary_continuity, u_boundary), "Fluid-BoundaryDummySummationDensity" => (v_boundary, u_boundary), - "Fluid-TLSPH" => (v_solid, u_solid) + "Fluid-TLSPH" => (v_structure, u_structure) ) return systems, vu diff --git a/test/schemes/schemes.jl b/test/schemes/schemes.jl index 5cf7cd5bd2..7a2d5d2800 100644 --- a/test/schemes/schemes.jl +++ b/test/schemes/schemes.jl @@ -1,4 +1,4 @@ -include("solid/total_lagrangian_sph/total_lagrangian_sph.jl") +include("structure/total_lagrangian_sph/total_lagrangian_sph.jl") include("boundary/dummy_particles/dummy_particles.jl") include("boundary/monaghan_kajtar/monaghan_kajtar.jl") include("boundary/open_boundary/open_boundary.jl") diff --git a/test/schemes/solid/total_lagrangian_sph/rhs.jl b/test/schemes/structure/total_lagrangian_sph/rhs.jl similarity index 97% rename from test/schemes/solid/total_lagrangian_sph/rhs.jl rename to test/schemes/structure/total_lagrangian_sph/rhs.jl index fc35808fae..412d295fad 100644 --- a/test/schemes/solid/total_lagrangian_sph/rhs.jl +++ b/test/schemes/structure/total_lagrangian_sph/rhs.jl @@ -1,4 +1,4 @@ -@testset verbose=true "Solid RHS" begin +@testset verbose=true "Structure RHS" begin # Use `@trixi_testset` to isolate the mock functions in a separate namespace @trixi_testset "interact! Mocked" begin # Pass specific PK1 and `pos_diff` to `interact!` and verify with @@ -53,7 +53,7 @@ kernel_deriv = 1.0 #### Mocking - struct MockSystem <: TrixiParticles.System{2} end + struct MockSystem <: TrixiParticles.AbstractSystem{2} end system = MockSystem() function TrixiParticles.initial_coordinates(::MockSystem) @@ -108,7 +108,7 @@ dv_expected[:, particle[i]] = dv_particle_expected[i] semi = DummySemidiscretization(parallelization_backend=backends[j]) - TrixiParticles.interact_solid_solid!(dv, v_system, system, semi) + TrixiParticles.interact_structure_structure!(dv, v_system, system, semi) @test dv ≈ dv_expected end diff --git a/test/schemes/solid/total_lagrangian_sph/total_lagrangian_sph.jl b/test/schemes/structure/total_lagrangian_sph/total_lagrangian_sph.jl similarity index 100% rename from test/schemes/solid/total_lagrangian_sph/total_lagrangian_sph.jl rename to test/schemes/structure/total_lagrangian_sph/total_lagrangian_sph.jl diff --git a/test/systems/boundary_system.jl b/test/systems/boundary_system.jl index 7d97628c76..4237508f80 100644 --- a/test/systems/boundary_system.jl +++ b/test/systems/boundary_system.jl @@ -1,4 +1,4 @@ -@testset verbose=true "BoundarySystem" begin +@testset verbose=true "BoundarySPHSystem" begin coordinates_ = [ [1.0 2.0 1.0 2.0], @@ -45,7 +45,7 @@ end is_moving(t) = t < 1.0 - bm = BoundaryMovement(movement_function, is_moving) + bm = PrescribedMotion(movement_function, is_moving) system = BoundarySPHSystem(initial_condition, model, movement=bm) # Moving @@ -79,7 +79,7 @@ initial_condition = InitialCondition(; coordinates, mass, density) - bm = BoundaryMovement(movement_function, is_moving, moving_particles=[2]) + bm = PrescribedMotion(movement_function, is_moving, moving_particles=[2]) system = BoundarySPHSystem(initial_condition, model, movement=bm) t = 0.1 diff --git a/test/systems/open_boundary_system.jl b/test/systems/open_boundary_system.jl index 3ce14e7634..cedc1b87f0 100644 --- a/test/systems/open_boundary_system.jl +++ b/test/systems/open_boundary_system.jl @@ -2,7 +2,7 @@ @testset "`show`" begin # Mock fluid system - struct FluidSystemMock2 <: TrixiParticles.FluidSystem{2} end + struct FluidSystemMock2 <: TrixiParticles.AbstractFluidSystem{2} end TrixiParticles.initial_smoothing_length(system::FluidSystemMock2) = 1.0 TrixiParticles.nparticles(system::FluidSystemMock2) = 1 diff --git a/test/systems/systems.jl b/test/systems/systems.jl index 21246721dd..0dedbed7a0 100644 --- a/test/systems/systems.jl +++ b/test/systems/systems.jl @@ -1,6 +1,6 @@ include("wcsph_system.jl") include("edac_system.jl") -include("solid_system.jl") +include("tlsph_system.jl") include("boundary_system.jl") include("open_boundary_system.jl") include("dem_system.jl") diff --git a/test/systems/solid_system.jl b/test/systems/tlsph_system.jl similarity index 98% rename from test/systems/solid_system.jl rename to test/systems/tlsph_system.jl index ae85ee1b2e..d201286d4d 100644 --- a/test/systems/solid_system.jl +++ b/test/systems/tlsph_system.jl @@ -75,7 +75,7 @@ │ TotalLagrangianSPHSystem{2} │ │ ═══════════════════════════ │ │ total #particles: ………………………………… 2 │ - │ #fixed particles: ………………………………… 0 │ + │ #clamped particles: …………………………… 0 │ │ Young's modulus: …………………………………… 2.5 │ │ Poisson ratio: ………………………………………… 0.25 │ │ smoothing kernel: ………………………………… Val │ @@ -97,7 +97,7 @@ │ TotalLagrangianSPHSystem{2} │ │ ═══════════════════════════ │ │ total #particles: ………………………………… 2 │ - │ #fixed particles: ………………………………… 0 │ + │ #clamped particles: …………………………… 0 │ │ Young's modulus: …………………………………… min = 1.2, max = 3.4 │ │ Poisson ratio: ………………………………………… min = 0.2, max = 0.4 │ │ smoothing kernel: ………………………………… Val │ diff --git a/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl b/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl index 6fc7278fe4..b805667d4e 100644 --- a/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl +++ b/validation/lid_driven_cavity_2d/validation_lid_driven_cavity_2d.jl @@ -13,10 +13,10 @@ reynolds_numbers = [100.0, 1000.0, 10_000.0] const SENSOR_CAPTURE_TIME = 24.8 const CAPTURE_STARTED = Ref(false) -interpolated_velocity(system, dv_ode, du_ode, v, u, semi, t) = nothing +interpolated_velocity(system, dv_ode, du_ode, v_ode, u_ode, semi, t) = nothing -function interpolated_velocity(system::TrixiParticles.FluidSystem, dv_ode, du_ode, v, u, - semi, t) +function interpolated_velocity(system::TrixiParticles.AbstractFluidSystem, + dv_ode, du_ode, v_ode, u_ode, semi, t) if t < SENSOR_CAPTURE_TIME return nothing end @@ -24,10 +24,10 @@ function interpolated_velocity(system::TrixiParticles.FluidSystem, dv_ode, du_od n_particles_xy = round(Int, 1.0 / system.initial_condition.particle_spacing) values_y = interpolate_line([0.5, 0.0], [0.5, 1.0], n_particles_xy, semi, system, - v, u; endpoint=true, cut_off_bnd=true) + v_ode, u_ode; endpoint=true, cut_off_bnd=true) values_x = interpolate_line([0.0, 0.5], [1.0, 0.5], n_particles_xy, semi, system, - v, u; endpoint=true, cut_off_bnd=true) + v_ode, u_ode; endpoint=true, cut_off_bnd=true) vy_y = stack(values_y.velocity)[2, :] vy_x = stack(values_y.velocity)[1, :] diff --git a/validation/oscillating_beam_2d/plot_oscillating_beam_results.jl b/validation/oscillating_beam_2d/plot_oscillating_beam_results.jl index 32c15f7321..8c89f7098d 100644 --- a/validation/oscillating_beam_2d/plot_oscillating_beam_results.jl +++ b/validation/oscillating_beam_2d/plot_oscillating_beam_results.jl @@ -24,8 +24,8 @@ merged_files = vcat(reference_files, simulation_files) input_files = sort(merged_files, by=extract_number_from_filename) # Regular expressions for matching keys -key_pattern_x = r"deflection_x_solid_\d+" -key_pattern_y = r"deflection_y_solid_\d+" +key_pattern_x = r"deflection_x_structure_\d+" +key_pattern_y = r"deflection_y_structure_\d+" # Setup for Makie plotting fig = Figure(size=(1200, 800)) diff --git a/validation/oscillating_beam_2d/validation_oscillating_beam_2d.jl b/validation/oscillating_beam_2d/validation_oscillating_beam_2d.jl index 8e6cc83401..d610157ebc 100644 --- a/validation/oscillating_beam_2d/validation_oscillating_beam_2d.jl +++ b/validation/oscillating_beam_2d/validation_oscillating_beam_2d.jl @@ -26,7 +26,7 @@ n_particles_beam_y = 5 # Overwrite `sol` assignment to skip time integration trixi_include(@__MODULE__, - joinpath(examples_dir(), "solid", "oscillating_beam_2d.jl"), + joinpath(examples_dir(), "structure", "oscillating_beam_2d.jl"), n_particles_y=n_particles_beam_y, sol=nothing, tspan=tspan, penalty_force=PenaltyForceGanzenmueller(alpha=0.01)) @@ -49,12 +49,12 @@ run_file_name = joinpath("out", reference_data = JSON.parsefile(reference_file_name) run_data = JSON.parsefile(run_file_name) -error_deflection_x = interpolated_mse(reference_data["deflection_x_solid_1"]["time"], - reference_data["deflection_x_solid_1"]["values"], - run_data["deflection_x_solid_1"]["time"], - run_data["deflection_x_solid_1"]["values"]) +error_deflection_x = interpolated_mse(reference_data["deflection_x_structure_1"]["time"], + reference_data["deflection_x_structure_1"]["values"], + run_data["deflection_x_structure_1"]["time"], + run_data["deflection_x_structure_1"]["values"]) -error_deflection_y = interpolated_mse(reference_data["deflection_y_solid_1"]["time"], - reference_data["deflection_y_solid_1"]["values"], - run_data["deflection_y_solid_1"]["time"], - run_data["deflection_y_solid_1"]["values"]) +error_deflection_y = interpolated_mse(reference_data["deflection_y_structure_1"]["time"], + reference_data["deflection_y_structure_1"]["values"], + run_data["deflection_y_structure_1"]["time"], + run_data["deflection_y_structure_1"]["values"]) diff --git a/validation/oscillating_beam_2d/validation_reference_17.json b/validation/oscillating_beam_2d/validation_reference_17.json index 6a1a210a51..7594808f31 100644 --- a/validation/oscillating_beam_2d/validation_reference_17.json +++ b/validation/oscillating_beam_2d/validation_reference_17.json @@ -4,7 +4,7 @@ "solver_version": "v0.1.0-6-g3164e99d-dirty", "solver_name": "TrixiParticles.jl" }, - "deflection_x_solid_1": { + "deflection_x_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -1009,7 +1009,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -2.399319076751283e-10, @@ -2016,7 +2016,7 @@ "datatype": "Float64", "type": "series" }, - "deflection_y_solid_1": { + "deflection_y_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -3021,7 +3021,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -9.999997556011525e-5, diff --git a/validation/oscillating_beam_2d/validation_reference_33.json b/validation/oscillating_beam_2d/validation_reference_33.json index 45b764827e..bc40349492 100644 --- a/validation/oscillating_beam_2d/validation_reference_33.json +++ b/validation/oscillating_beam_2d/validation_reference_33.json @@ -4,7 +4,7 @@ "solver_version": "v0.1.0-6-g3164e99d-dirty", "solver_name": "TrixiParticles.jl" }, - "deflection_x_solid_1": { + "deflection_x_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -1009,7 +1009,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -2.5558510863277206e-10, @@ -2016,7 +2016,7 @@ "datatype": "Float64", "type": "series" }, - "deflection_y_solid_1": { + "deflection_y_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -3021,7 +3021,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -9.999997643593724e-5, diff --git a/validation/oscillating_beam_2d/validation_reference_5.json b/validation/oscillating_beam_2d/validation_reference_5.json index da111b580b..5ecfb68e2f 100644 --- a/validation/oscillating_beam_2d/validation_reference_5.json +++ b/validation/oscillating_beam_2d/validation_reference_5.json @@ -4,7 +4,7 @@ "solver_version": "f836223-dirty", "solver_name": "TrixiParticles.jl" }, - "deflection_x_solid_1": { + "deflection_x_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -1009,7 +1009,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -2.1757223800378256e-10, @@ -2016,7 +2016,7 @@ "datatype": "Float64", "type": "series" }, - "deflection_y_solid_1": { + "deflection_y_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -3021,7 +3021,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -9.999999694490329e-5, diff --git a/validation/oscillating_beam_2d/validation_reference_65.json b/validation/oscillating_beam_2d/validation_reference_65.json index 7e69d42b10..567a9af232 100644 --- a/validation/oscillating_beam_2d/validation_reference_65.json +++ b/validation/oscillating_beam_2d/validation_reference_65.json @@ -4,7 +4,7 @@ "solver_version": "v0.1.0-6-g3164e99d-dirty", "solver_name": "TrixiParticles.jl" }, - "deflection_x_solid_1": { + "deflection_x_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -1009,7 +1009,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -2.739832249964991e-10, @@ -2016,7 +2016,7 @@ "datatype": "Float64", "type": "series" }, - "deflection_y_solid_1": { + "deflection_y_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -3021,7 +3021,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -9.999995508498498e-5, diff --git a/validation/oscillating_beam_2d/validation_reference_9.json b/validation/oscillating_beam_2d/validation_reference_9.json index 929b94bebc..0c2f91f4ad 100644 --- a/validation/oscillating_beam_2d/validation_reference_9.json +++ b/validation/oscillating_beam_2d/validation_reference_9.json @@ -4,7 +4,7 @@ "solver_version": "c6184f4-dirty", "solver_name": "TrixiParticles.jl" }, - "deflection_y_solid_1": { + "deflection_y_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -1009,7 +1009,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -9.999999490424051e-5, @@ -2016,7 +2016,7 @@ "datatype": "Float64", "type": "series" }, - "deflection_x_solid_1": { + "deflection_x_structure_1": { "n_values": 1001, "time": [ 0.0, @@ -3021,7 +3021,7 @@ 9.99, 10.0 ], - "system_name": "solid", + "system_name": "structure", "values": [ 0.0, -2.0271367917601424e-10, From b67407550fe5fb05728f060e931c7e98db477b21 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:59:59 +0200 Subject: [PATCH 08/15] Renamings 2 (#904) * Rename `BoundarySPHSystem` and `OpenBoundarySPHSystem` * Reformat * Update NEWS.md * Fix tests * Rename kwarg * Update docs * Reformat * Fix * Fix example files * Reformat * Implement suggestions --- NEWS.md | 17 +- docs/src/gpu.md | 2 +- docs/src/systems/boundary.md | 2 +- docs/src/tutorials_template/tut_setup.md | 2 +- examples/fluid/accelerated_tank_2d.jl | 3 +- examples/fluid/dam_break_2d.jl | 3 +- examples/fluid/dam_break_3d.jl | 2 +- examples/fluid/falling_water_column_2d.jl | 2 +- examples/fluid/falling_water_spheres_2d.jl | 4 +- examples/fluid/hydrostatic_water_column_2d.jl | 3 +- examples/fluid/lid_driven_cavity_2d.jl | 5 +- examples/fluid/moving_wall_2d.jl | 4 +- .../fluid/periodic_array_of_cylinders_2d.jl | 2 +- examples/fluid/periodic_channel_2d.jl | 2 +- examples/fluid/pipe_flow_2d.jl | 8 +- examples/fsi/dam_break_gate_2d.jl | 5 +- examples/fsi/dam_break_plate_2d.jl | 2 +- examples/fsi/falling_spheres_2d.jl | 2 +- src/TrixiParticles.jl | 2 +- src/general/gpu.jl | 6 +- src/general/semidiscretization.jl | 79 +++++----- src/io/io.jl | 6 +- src/io/write_vtk.jl | 4 +- src/schemes/boundary/boundary.jl | 2 +- .../dummy_particles/dummy_particles.jl | 2 +- .../monaghan_kajtar/monaghan_kajtar.jl | 8 +- .../boundary/open_boundary/boundary_zones.jl | 2 +- .../method_of_characteristics.jl | 2 +- .../boundary/open_boundary/mirroring.jl | 2 +- src/schemes/boundary/open_boundary/system.jl | 108 +++++++------ src/schemes/boundary/rhs.jl | 4 +- src/schemes/boundary/system.jl | 145 +++++++++--------- .../fluid/entropically_damped_sph/system.jl | 2 +- .../fluid/weakly_compressible_sph/rhs.jl | 4 +- .../fluid/weakly_compressible_sph/system.jl | 2 +- src/schemes/schemes.jl | 2 +- .../structure/total_lagrangian_sph/rhs.jl | 2 +- test/general/buffer.jl | 12 +- test/general/custom_quantities.jl | 2 +- test/general/interpolation.jl | 4 +- test/general/semidiscretization.jl | 2 +- test/io/read_vtk.jl | 10 +- .../dummy_particles/dummy_particles.jl | 6 +- test/schemes/boundary/dummy_particles/rhs.jl | 12 +- .../monaghan_kajtar/monaghan_kajtar.jl | 2 +- .../open_boundary/characteristic_variables.jl | 6 +- .../boundary/open_boundary/mirroring.jl | 30 ++-- test/schemes/fluid/surface_normal_sph.jl | 2 +- test/systems/boundary_system.jl | 26 ++-- test/systems/open_boundary_system.jl | 38 ++--- 50 files changed, 312 insertions(+), 294 deletions(-) diff --git a/NEWS.md b/NEWS.md index da6ebb462a..7830c69b59 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,8 +8,21 @@ used in the Julia ecosystem. Notable changes will be documented in this file for ### API Changes -- API for `OpenBoundarySPHSystem` and `BoundaryZone` changed. - It is now possible to pass multiple `BoundaryZone`s to a single `OpenBoundarySPHSystem`. +- Renamed `BoundarySPHSystem` to `WallBoundarySystem` and the keyword argument + `movement` to `prescribed_motion`. + +- Renamed `OpenBoundarySPHSystem` to `OpenBoundarySystem`. + +- Renamed `BoundaryMovement` to `PrescribedMotion`. + +- Renamed directory `solid` to `structure` in the examples file tree. + VTK files for the `TotalLagrangianSPHSystem` are now also called `structure_*`. + +- Renamed keyword argument `n_fixed_particles` of the `TotalLagrangianSPHSystem` + to `n_clamped_particles`. + +- API for `OpenBoundarySystem` and `BoundaryZone` changed. + It is now possible to pass multiple `BoundaryZone`s to a single `OpenBoundarySystem`. Reference values are now assigned individually to each `BoundaryZone`. (#866) - The argument of `TransportVelocityAdami` is now a keyword argument. diff --git a/docs/src/gpu.md b/docs/src/gpu.md index 61d92b90a0..d22f936ad1 100644 --- a/docs/src/gpu.md +++ b/docs/src/gpu.md @@ -2,7 +2,7 @@ GPU support is still an experimental feature that is actively being worked on. Currently, the [`WeaklyCompressibleSPHSystem`](@ref), [`TotalLagrangianSPHSystem`](@ref) -and [`BoundarySPHSystem`](@ref) support GPU execution. +and [`WallBoundarySystem`](@ref) support GPU execution. We have tested GPU support on Nvidia, AMD and Apple GPUs. Note that most Apple GPUs do not support `Float64`. See [below on how to run single precision simulations](@ref single_precision). diff --git a/docs/src/systems/boundary.md b/docs/src/systems/boundary.md index 3d7e014ba5..7bd334d017 100644 --- a/docs/src/systems/boundary.md +++ b/docs/src/systems/boundary.md @@ -1,7 +1,7 @@ # Boundary System ```@docs - BoundarySPHSystem + WallBoundarySystem ``` ```@docs diff --git a/docs/src/tutorials_template/tut_setup.md b/docs/src/tutorials_template/tut_setup.md index 98e1153052..de8c37e3ba 100644 --- a/docs/src/tutorials_template/tut_setup.md +++ b/docs/src/tutorials_template/tut_setup.md @@ -161,7 +161,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar state_equation=state_equation, AdamiPressureExtrapolation(), smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) nothing # hide ``` diff --git a/examples/fluid/accelerated_tank_2d.jl b/examples/fluid/accelerated_tank_2d.jl index fe9e1cbae4..8d975656fa 100644 --- a/examples/fluid/accelerated_tank_2d.jl +++ b/examples/fluid/accelerated_tank_2d.jl @@ -20,5 +20,6 @@ boundary_movement = PrescribedMotion(movement_function, is_moving) trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "hydrostatic_water_column_2d.jl"), - fluid_particle_spacing=fluid_particle_spacing, movement=boundary_movement, + fluid_particle_spacing=fluid_particle_spacing, + prescribed_motion=boundary_movement, tspan=(0.0, 1.0), system_acceleration=(0.0, 0.0)); diff --git a/examples/fluid/dam_break_2d.jl b/examples/fluid/dam_break_2d.jl index ae8c96434b..a25084eca5 100644 --- a/examples/fluid/dam_break_2d.jl +++ b/examples/fluid/dam_break_2d.jl @@ -97,7 +97,8 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar reference_particle_spacing=0, viscosity=viscosity_wall) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model, adhesion_coefficient=0.0) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model, + adhesion_coefficient=0.0) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/dam_break_3d.jl b/examples/fluid/dam_break_3d.jl index fcfce8dbb4..32045f979e 100644 --- a/examples/fluid/dam_break_3d.jl +++ b/examples/fluid/dam_break_3d.jl @@ -59,7 +59,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/falling_water_column_2d.jl b/examples/fluid/falling_water_column_2d.jl index c4eaab6777..f712f88d93 100644 --- a/examples/fluid/falling_water_column_2d.jl +++ b/examples/fluid/falling_water_column_2d.jl @@ -58,7 +58,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/falling_water_spheres_2d.jl b/examples/fluid/falling_water_spheres_2d.jl index 667264d668..778d404d69 100644 --- a/examples/fluid/falling_water_spheres_2d.jl +++ b/examples/fluid/falling_water_spheres_2d.jl @@ -84,8 +84,8 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar viscosity=ViscosityAdami(nu=wall_viscosity), reference_particle_spacing=fluid_particle_spacing) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model, - adhesion_coefficient=1.0) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model, + adhesion_coefficient=1.0) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/hydrostatic_water_column_2d.jl b/examples/fluid/hydrostatic_water_column_2d.jl index 110d5bfdc0..a62dbab249 100644 --- a/examples/fluid/hydrostatic_water_column_2d.jl +++ b/examples/fluid/hydrostatic_water_column_2d.jl @@ -64,7 +64,8 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, smoothing_kernel, smoothing_length, viscosity=viscosity_wall) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model, movement=nothing) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model, + prescribed_motion=nothing) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl index 07c0efd645..5429b2a87a 100644 --- a/examples/fluid/lid_driven_cavity_2d.jl +++ b/examples/fluid/lid_driven_cavity_2d.jl @@ -98,9 +98,10 @@ boundary_model_lid = BoundaryModelDummyParticles(lid.density, lid.mass, state_equation=state_equation, smoothing_kernel, smoothing_length) -boundary_system_cavity = BoundarySPHSystem(cavity.boundary, boundary_model_cavity) +boundary_system_cavity = WallBoundarySystem(cavity.boundary, boundary_model_cavity) -boundary_system_lid = BoundarySPHSystem(lid, boundary_model_lid, movement=lid_movement) +boundary_system_lid = WallBoundarySystem(lid, boundary_model_lid, + prescribed_motion=lid_movement) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/moving_wall_2d.jl b/examples/fluid/moving_wall_2d.jl index a3b319372b..d67ce3efc3 100644 --- a/examples/fluid/moving_wall_2d.jl +++ b/examples/fluid/moving_wall_2d.jl @@ -64,8 +64,8 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model, - movement=boundary_movement) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model, + prescribed_motion=boundary_movement) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/periodic_array_of_cylinders_2d.jl b/examples/fluid/periodic_array_of_cylinders_2d.jl index 214f456baa..fd71e9a4be 100644 --- a/examples/fluid/periodic_array_of_cylinders_2d.jl +++ b/examples/fluid/periodic_array_of_cylinders_2d.jl @@ -70,7 +70,7 @@ boundary_model = BoundaryModelDummyParticles(boundary.density, boundary.mass, viscosity=ViscosityAdami(; nu), smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(boundary, boundary_model) +boundary_system = WallBoundarySystem(boundary, boundary_model) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/periodic_channel_2d.jl b/examples/fluid/periodic_channel_2d.jl index 9cd4bd95a5..62aed7466d 100644 --- a/examples/fluid/periodic_channel_2d.jl +++ b/examples/fluid/periodic_channel_2d.jl @@ -64,7 +64,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar smoothing_kernel, smoothing_length, viscosity=viscosity_wall) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) # ========================================================================================== # ==== Simulation diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl index fab2f467d3..6c116efe26 100644 --- a/examples/fluid/pipe_flow_2d.jl +++ b/examples/fluid/pipe_flow_2d.jl @@ -133,9 +133,9 @@ outflow = BoundaryZone(; plane=plane_out, plane_normal=(-flow_direction), reference_velocity=reference_velocity_out, initial_condition=outlet.fluid, boundary_type=boundary_type_out) -open_boundary = OpenBoundarySPHSystem(inflow, outflow; fluid_system, - boundary_model=open_boundary_model, - buffer_size=n_buffer_particles) +open_boundary = OpenBoundarySystem(inflow, outflow; fluid_system, + boundary_model=open_boundary_model, + buffer_size=n_buffer_particles) # ========================================================================================== # ==== Boundary @@ -147,7 +147,7 @@ boundary_model = BoundaryModelDummyParticles(wall.density, wall.mass, viscosity=viscosity_boundary, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(wall, boundary_model) +boundary_system = WallBoundarySystem(wall, boundary_model) # ========================================================================================== # ==== Simulation diff --git a/examples/fsi/dam_break_gate_2d.jl b/examples/fsi/dam_break_gate_2d.jl index a18147abee..b78b134810 100644 --- a/examples/fsi/dam_break_gate_2d.jl +++ b/examples/fsi/dam_break_gate_2d.jl @@ -124,8 +124,9 @@ boundary_model_gate = BoundaryModelDummyParticles(gate.density, gate.mass, boundary_density_calculator, smoothing_kernel, smoothing_length) -boundary_system_tank = BoundarySPHSystem(tank.boundary, boundary_model_tank) -boundary_system_gate = BoundarySPHSystem(gate, boundary_model_gate, movement=gate_movement) +boundary_system_tank = WallBoundarySystem(tank.boundary, boundary_model_tank) +boundary_system_gate = WallBoundarySystem(gate, boundary_model_gate, + prescribed_motion=gate_movement) # ========================================================================================== # ==== Structure diff --git a/examples/fsi/dam_break_plate_2d.jl b/examples/fsi/dam_break_plate_2d.jl index 208bf3377c..62a9a919f2 100644 --- a/examples/fsi/dam_break_plate_2d.jl +++ b/examples/fsi/dam_break_plate_2d.jl @@ -90,7 +90,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, smoothing_kernel, smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) # ========================================================================================== # ==== Structure diff --git a/examples/fsi/falling_spheres_2d.jl b/examples/fsi/falling_spheres_2d.jl index a475da1628..e938cbf6f8 100644 --- a/examples/fsi/falling_spheres_2d.jl +++ b/examples/fsi/falling_spheres_2d.jl @@ -76,7 +76,7 @@ boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundar boundary_density_calculator, fluid_smoothing_kernel, fluid_smoothing_length) -boundary_system = BoundarySPHSystem(tank.boundary, boundary_model) +boundary_system = WallBoundarySystem(tank.boundary, boundary_model) # ========================================================================================== # ==== Structure diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index f643ef5442..853ba9a3d2 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -63,7 +63,7 @@ include("visualization/recipes_plots.jl") export Semidiscretization, semidiscretize, restart_with! export InitialCondition export WeaklyCompressibleSPHSystem, EntropicallyDampedSPHSystem, TotalLagrangianSPHSystem, - BoundarySPHSystem, DEMSystem, BoundaryDEMSystem, OpenBoundarySPHSystem + WallBoundarySystem, DEMSystem, BoundaryDEMSystem, OpenBoundarySystem export BoundaryZone, InFlow, OutFlow, BidirectionalFlow export InfoCallback, SolutionSavingCallback, DensityReinitializationCallback, PostprocessCallback, StepsizeCallback, UpdateCallback, SteadyStateReachedCallback diff --git a/src/general/gpu.jl b/src/general/gpu.jl index 77dec03021..196b24861b 100644 --- a/src/general/gpu.jl +++ b/src/general/gpu.jl @@ -12,21 +12,21 @@ Adapt.@adapt_structure InitialCondition Adapt.@adapt_structure WeaklyCompressibleSPHSystem Adapt.@adapt_structure DensityDiffusionAntuono Adapt.@adapt_structure EntropicallyDampedSPHSystem -Adapt.@adapt_structure BoundarySPHSystem +Adapt.@adapt_structure WallBoundarySystem Adapt.@adapt_structure BoundaryModelDummyParticles Adapt.@adapt_structure BoundaryModelMonaghanKajtar Adapt.@adapt_structure PrescribedMotion Adapt.@adapt_structure TotalLagrangianSPHSystem Adapt.@adapt_structure BoundaryZone Adapt.@adapt_structure SystemBuffer -Adapt.@adapt_structure OpenBoundarySPHSystem +Adapt.@adapt_structure OpenBoundarySystem KernelAbstractions.get_backend(::PtrArray) = KernelAbstractions.CPU() function KernelAbstractions.get_backend(system::AbstractSystem) KernelAbstractions.get_backend(system.mass) end -function KernelAbstractions.get_backend(system::BoundarySPHSystem) +function KernelAbstractions.get_backend(system::WallBoundarySystem) KernelAbstractions.get_backend(system.coordinates) end diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl index 2eebea8677..59536a9a4c 100644 --- a/src/general/semidiscretization.jl +++ b/src/general/semidiscretization.jl @@ -151,13 +151,13 @@ end return compact_support(smoothing_kernel, initial_smoothing_length(system)) end -@inline function compact_support(system::OpenBoundarySPHSystem, neighbor) +@inline function compact_support(system::OpenBoundarySystem, neighbor) # Use the compact support of the fluid return compact_support(neighbor, system) end -@inline function compact_support(system::OpenBoundarySPHSystem, - neighbor::OpenBoundarySPHSystem) +@inline function compact_support(system::OpenBoundarySystem, + neighbor::OpenBoundarySystem) # This NHS is never used return 0.0 end @@ -178,7 +178,8 @@ end return compact_support(smoothing_kernel, smoothing_length) end -@inline function compact_support(system::Union{TotalLagrangianSPHSystem, BoundarySPHSystem}, +@inline function compact_support(system::Union{TotalLagrangianSPHSystem, + WallBoundarySystem}, neighbor) return compact_support(system, system.boundary_model, neighbor) end @@ -189,7 +190,7 @@ end end @inline function compact_support(system, model::BoundaryModelMonaghanKajtar, - neighbor::BoundarySPHSystem) + neighbor::WallBoundarySystem) # This NHS is never used return 0.0 end @@ -482,7 +483,7 @@ end end # Solid wall boundary system doesn't integrate the particle positions -@inline add_velocity!(du, v, particle, system::BoundarySPHSystem) = du +@inline add_velocity!(du, v, particle, system::WallBoundarySystem) = du @inline function add_velocity!(du, v, particle, system::AbstractFluidSystem) # This is zero unless a shifting technique is used @@ -720,7 +721,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::AbstractFluidSystem, neighbor::BoundarySPHSystem, + system::AbstractFluidSystem, neighbor::WallBoundarySystem, u_system, u_neighbor, semi) # Boundary coordinates only change over time when `neighbor.ismoving[]` update!(neighborhood_search, @@ -730,7 +731,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::AbstractFluidSystem, neighbor::OpenBoundarySPHSystem, + system::AbstractFluidSystem, neighbor::OpenBoundarySystem, u_system, u_neighbor, semi) # The current coordinates of fluids and open boundaries change over time. @@ -743,7 +744,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::OpenBoundarySPHSystem, neighbor::AbstractFluidSystem, + system::OpenBoundarySystem, neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # The current coordinates of both open boundaries and fluids change over time. @@ -756,14 +757,14 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::OpenBoundarySPHSystem, neighbor::TotalLagrangianSPHSystem, + system::OpenBoundarySystem, neighbor::TotalLagrangianSPHSystem, u_system, u_neighbor, semi) # Don't update. This NHS is never used. return neighborhood_search end function update_nhs!(neighborhood_search, - system::TotalLagrangianSPHSystem, neighbor::OpenBoundarySPHSystem, + system::TotalLagrangianSPHSystem, neighbor::OpenBoundarySystem, u_system, u_neighbor, semi) # Don't update. This NHS is never used. return neighborhood_search @@ -787,7 +788,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::TotalLagrangianSPHSystem, neighbor::BoundarySPHSystem, + system::TotalLagrangianSPHSystem, neighbor::WallBoundarySystem, u_system, u_neighbor, semi) # The current coordinates of structured change over time. # Boundary coordinates only change over time when `neighbor.ismoving[]`. @@ -799,7 +800,7 @@ end # This function is the same as the one below to avoid ambiguous dispatch when using `Union` function update_nhs!(neighborhood_search, - system::BoundarySPHSystem{<:BoundaryModelDummyParticles}, + system::WallBoundarySystem{<:BoundaryModelDummyParticles}, neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # Depending on the density calculator of the boundary model, this NHS is used for # - kernel summation (`SummationDensity`) @@ -817,7 +818,7 @@ end # This function is the same as the one above to avoid ambiguous dispatch when using `Union` function update_nhs!(neighborhood_search, - system::BoundarySPHSystem{<:BoundaryModelDummyParticles}, + system::WallBoundarySystem{<:BoundaryModelDummyParticles}, neighbor::TotalLagrangianSPHSystem, u_system, u_neighbor, semi) # Depending on the density calculator of the boundary model, this NHS is used for # - kernel summation (`SummationDensity`) @@ -833,8 +834,8 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::BoundarySPHSystem{<:BoundaryModelDummyParticles}, - neighbor::BoundarySPHSystem, + system::WallBoundarySystem{<:BoundaryModelDummyParticles}, + neighbor::WallBoundarySystem, u_system, u_neighbor, semi) # `system` coordinates only change over time when `system.ismoving[]`. # `neighbor` coordinates only change over time when `neighbor.ismoving[]`. @@ -865,7 +866,7 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::BoundarySPHSystem, + system::WallBoundarySystem, neighbor::AbstractFluidSystem, u_system, u_neighbor, semi) # Don't update. This NHS is never used. @@ -881,8 +882,8 @@ function update_nhs!(neighborhood_search, end function update_nhs!(neighborhood_search, - system::Union{BoundarySPHSystem, OpenBoundarySPHSystem}, - neighbor::Union{BoundarySPHSystem, OpenBoundarySPHSystem}, + system::Union{WallBoundarySystem, OpenBoundarySystem}, + neighbor::Union{WallBoundarySystem, OpenBoundarySystem}, u_system, u_neighbor, semi) # Don't update. This NHS is never used. return neighborhood_search @@ -922,7 +923,7 @@ function check_system_color(systems) for system in systems) # System indices of all systems that are either a fluid or a boundary system - system_ids = findall(system isa Union{AbstractFluidSystem, BoundarySPHSystem} + system_ids = findall(system isa Union{AbstractFluidSystem, WallBoundarySystem} for system in systems) if length(system_ids) > 1 && sum(i -> systems[i].cache.color, system_ids) == 0 @@ -944,7 +945,7 @@ function check_configuration(fluid_system::AbstractFluidSystem, systems, nhs) end end -function check_configuration(system::BoundarySPHSystem, systems, nhs) +function check_configuration(system::WallBoundarySystem, systems, nhs) (; boundary_model) = system foreach_system(systems) do neighbor @@ -974,7 +975,7 @@ function check_configuration(system::TotalLagrangianSPHSystem, systems, nhs) end end -function check_configuration(system::OpenBoundarySPHSystem, systems, +function check_configuration(system::OpenBoundarySystem, systems, neighborhood_search::PointNeighbors.AbstractNeighborhoodSearch) (; boundary_model, boundary_zones) = system @@ -990,7 +991,7 @@ function check_configuration(system::OpenBoundarySPHSystem, systems, end if first(PointNeighbors.requires_update(neighborhood_search)) - throw(ArgumentError("`OpenBoundarySPHSystem` requires a neighborhood search " * + throw(ArgumentError("`OpenBoundarySystem` requires a neighborhood search " * "that does not require an update for the first set of coordinates (e.g. `GridNeighborhoodSearch`). " * "See the PointNeighbors.jl documentation for more details.")) end @@ -1001,22 +1002,22 @@ end # Therefore, we have to re-link them based on the stored system index. set_system_links(system, semi) = system -function set_system_links(system::OpenBoundarySPHSystem, semi) +function set_system_links(system::OpenBoundarySystem, semi) fluid_system = semi.systems[system.fluid_system_index[]] - return OpenBoundarySPHSystem(system.boundary_model, - system.initial_condition, - fluid_system, # link to fluid system - system.fluid_system_index, - system.smoothing_length, - system.mass, - system.density, - system.volume, - system.pressure, - system.boundary_candidates, - system.fluid_candidates, - system.boundary_zone_indices, - system.boundary_zones, - system.buffer, - system.cache) + return OpenBoundarySystem(system.boundary_model, + system.initial_condition, + fluid_system, # link to fluid system + system.fluid_system_index, + system.smoothing_length, + system.mass, + system.density, + system.volume, + system.pressure, + system.boundary_candidates, + system.fluid_candidates, + system.boundary_zone_indices, + system.boundary_zones, + system.buffer, + system.cache) end diff --git a/src/io/io.jl b/src/io/io.jl index 59440d6cbb..19b4a22cc2 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -109,12 +109,12 @@ function add_system_data!(system_data, system::TotalLagrangianSPHSystem) add_system_data!(system_data, system.penalty_force) end -function add_system_data!(system_data, system::BoundarySPHSystem) +function add_system_data!(system_data, system::WallBoundarySystem) system_data["system_type"] = type2string(system) system_data["particle_spacing"] = particle_spacing(system, 1) system_data["adhesion_coefficient"] = system.adhesion_coefficient add_system_data!(system_data, system.boundary_model) - add_system_data!(system_data, system.movement) + add_system_data!(system_data, system.prescribed_motion) end function add_system_data!(system_data, system::BoundaryDEMSystem) @@ -131,7 +131,7 @@ function add_system_data!(system_data, system::DEMSystem) add_system_data!(system_data, system.contact_model) end -function add_system_data!(system_data, system::OpenBoundarySPHSystem) +function add_system_data!(system_data, system::OpenBoundarySystem) system_data["system_type"] = type2string(system) system_data["fluid_system_index"] = system.fluid_system_index[] system_data["smoothing_length"] = system.smoothing_length diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index fb2323d648..9db388dcd4 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -390,7 +390,7 @@ function write2vtk!(vtk, v, u, t, system::TotalLagrangianSPHSystem) write2vtk!(vtk, v, u, t, system.boundary_model, system) end -function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem) +function write2vtk!(vtk, v, u, t, system::OpenBoundarySystem) vtk["velocity"] = [current_velocity(v, system, particle) for particle in eachparticle(system)] vtk["density"] = [current_density(v, system, particle) @@ -401,7 +401,7 @@ function write2vtk!(vtk, v, u, t, system::OpenBoundarySPHSystem) return vtk end -function write2vtk!(vtk, v, u, t, system::BoundarySPHSystem) +function write2vtk!(vtk, v, u, t, system::WallBoundarySystem) write2vtk!(vtk, v, u, t, system.boundary_model, system) end diff --git a/src/schemes/boundary/boundary.jl b/src/schemes/boundary/boundary.jl index a09545a9b7..2442dbec49 100644 --- a/src/schemes/boundary/boundary.jl +++ b/src/schemes/boundary/boundary.jl @@ -4,7 +4,7 @@ include("open_boundary/boundary_zones.jl") include("open_boundary/mirroring.jl") include("open_boundary/method_of_characteristics.jl") include("open_boundary/system.jl") -# Monaghan-Kajtar repulsive boundary particles require the `BoundarySPHSystem` +# Monaghan-Kajtar repulsive boundary particles require the `WallBoundarySystem` # and the `TotalLagrangianSPHSystem` and are therefore included later. @inline Base.ndims(boundary_model::BoundaryModelDummyParticles) = ndims(boundary_model.smoothing_kernel) diff --git a/src/schemes/boundary/dummy_particles/dummy_particles.jl b/src/schemes/boundary/dummy_particles/dummy_particles.jl index b495dffab5..0d3755e5ef 100644 --- a/src/schemes/boundary/dummy_particles/dummy_particles.jl +++ b/src/schemes/boundary/dummy_particles/dummy_particles.jl @@ -5,7 +5,7 @@ state_equation=nothing, correction=nothing, reference_particle_spacing=0.0) -Boundary model for `BoundarySPHSystem`. +Boundary model for [`WallBoundarySystem`](@ref). # Arguments - `initial_density`: Vector holding the initial density of each boundary particle. diff --git a/src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl b/src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl index 6fcdf7ec37..1df9a38cf9 100644 --- a/src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl +++ b/src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl @@ -2,7 +2,7 @@ BoundaryModelMonaghanKajtar(K, beta, boundary_particle_spacing, mass; viscosity=nothing) -Boundary model for `BoundarySPHSystem`. +Boundary model for [`WallBoundarySystem`](@ref). # Arguments - `K`: Scaling factor for repulsive force. @@ -44,7 +44,7 @@ function Base.show(io::IO, model::BoundaryModelMonaghanKajtar) end @inline function pressure_acceleration(particle_system, - neighbor_system::Union{BoundarySPHSystem{<:BoundaryModelMonaghanKajtar}, + neighbor_system::Union{WallBoundarySystem{<:BoundaryModelMonaghanKajtar}, TotalLagrangianSPHSystem{<:BoundaryModelMonaghanKajtar}}, particle, neighbor, m_a, m_b, p_a, p_b, rho_a, rho_b, pos_diff, distance, grad_kernel, correction) @@ -80,7 +80,7 @@ end end @inline function current_density(v, - system::Union{BoundarySPHSystem{<:BoundaryModelMonaghanKajtar}, + system::Union{WallBoundarySystem{<:BoundaryModelMonaghanKajtar}, TotalLagrangianSPHSystem{<:BoundaryModelMonaghanKajtar}}, particle) (; hydrodynamic_mass, boundary_particle_spacing) = system.boundary_model @@ -97,7 +97,7 @@ end # This model does not not use any particle pressure @inline function current_pressure(v, - system::Union{BoundarySPHSystem{<:BoundaryModelMonaghanKajtar}, + system::Union{WallBoundarySystem{<:BoundaryModelMonaghanKajtar}, TotalLagrangianSPHSystem{<:BoundaryModelMonaghanKajtar}}, particle) return zero(eltype(v)) diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl index 00ae2cfe39..33fd3614fc 100644 --- a/src/schemes/boundary/open_boundary/boundary_zones.jl +++ b/src/schemes/boundary/open_boundary/boundary_zones.jl @@ -12,7 +12,7 @@ struct OutFlow end reference_density=nothing, reference_pressure=nothing, reference_velocity=nothing) -Boundary zone for [`OpenBoundarySPHSystem`](@ref). +Boundary zone for [`OpenBoundarySystem`](@ref). The specified plane (line in 2D or rectangle in 3D) will be extruded in the direction opposite to `plane_normal` to create a box for the boundary zone. diff --git a/src/schemes/boundary/open_boundary/method_of_characteristics.jl b/src/schemes/boundary/open_boundary/method_of_characteristics.jl index 4e25500462..48842cde5a 100644 --- a/src/schemes/boundary/open_boundary/method_of_characteristics.jl +++ b/src/schemes/boundary/open_boundary/method_of_characteristics.jl @@ -1,7 +1,7 @@ @doc raw""" BoundaryModelCharacteristicsLastiwka(; extrapolate_reference_values=nothing) -Boundary model for [`OpenBoundarySPHSystem`](@ref). +Boundary model for [`OpenBoundarySystem`](@ref). This model uses the characteristic variables to propagate the appropriate values to the outlet or inlet and was proposed by Lastiwka et al. (2009). It requires a specific flow direction to be passed to the [`BoundaryZone`](@ref). diff --git a/src/schemes/boundary/open_boundary/mirroring.jl b/src/schemes/boundary/open_boundary/mirroring.jl index e1f33fed7e..b90f55bf62 100644 --- a/src/schemes/boundary/open_boundary/mirroring.jl +++ b/src/schemes/boundary/open_boundary/mirroring.jl @@ -49,7 +49,7 @@ struct ZerothOrderMirroring end @doc raw""" BoundaryModelMirroringTafuni(; mirror_method=FirstOrderMirroring()) -Boundary model for the `OpenBoundarySPHSystem`. +Boundary model for the [`OpenBoundarySystem`](@ref). This model implements the method of [Tafuni et al. (2018)](@cite Tafuni2018) to extrapolate the properties from the fluid domain to the buffer zones (inflow and outflow) using ghost nodes. The position of the ghost nodes is obtained by mirroring the boundary particles diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index 0b424f43a6..c03bd763ad 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -1,7 +1,7 @@ @doc raw""" - OpenBoundarySPHSystem(boundary_zone::BoundaryZone; - fluid_system::AbstractFluidSystem, buffer_size::Integer, - boundary_model) + OpenBoundarySystem(boundary_zone::BoundaryZone; + fluid_system::AbstractFluidSystem, buffer_size::Integer, + boundary_model) Open boundary system for in- and outflow particles. @@ -16,8 +16,8 @@ Open boundary system for in- and outflow particles. !!! warning "Experimental Implementation" This is an experimental feature and may change in any future releases. """ -struct OpenBoundarySPHSystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZI, BZ, - B, C} <: AbstractSystem{NDIMS} +struct OpenBoundarySystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZI, BZ, + B, C} <: AbstractSystem{NDIMS} boundary_model :: BM initial_condition :: IC fluid_system :: FS @@ -35,26 +35,26 @@ struct OpenBoundarySPHSystem{BM, ELTYPE, NDIMS, IC, FS, FSI, ARRAY1D, BC, FC, BZ cache :: C end -function OpenBoundarySPHSystem(boundary_model, initial_condition, fluid_system, - fluid_system_index, smoothing_length, mass, density, volume, - pressure, boundary_candidates, fluid_candidates, - boundary_zone_indices, boundary_zone, buffer, cache) - OpenBoundarySPHSystem{typeof(boundary_model), eltype(mass), ndims(initial_condition), - typeof(initial_condition), typeof(fluid_system), - typeof(fluid_system_index), typeof(mass), - typeof(boundary_candidates), typeof(fluid_candidates), - typeof(boundary_zone_indices), typeof(boundary_zone), - typeof(buffer), - typeof(cache)}(boundary_model, initial_condition, fluid_system, - fluid_system_index, smoothing_length, mass, - density, volume, pressure, boundary_candidates, - fluid_candidates, boundary_zone_indices, - boundary_zone, buffer, cache) +function OpenBoundarySystem(boundary_model, initial_condition, fluid_system, + fluid_system_index, smoothing_length, mass, density, volume, + pressure, boundary_candidates, fluid_candidates, + boundary_zone_indices, boundary_zone, buffer, cache) + OpenBoundarySystem{typeof(boundary_model), eltype(mass), ndims(initial_condition), + typeof(initial_condition), typeof(fluid_system), + typeof(fluid_system_index), typeof(mass), + typeof(boundary_candidates), typeof(fluid_candidates), + typeof(boundary_zone_indices), typeof(boundary_zone), + typeof(buffer), + typeof(cache)}(boundary_model, initial_condition, fluid_system, + fluid_system_index, smoothing_length, mass, + density, volume, pressure, boundary_candidates, + fluid_candidates, boundary_zone_indices, + boundary_zone, buffer, cache) end -function OpenBoundarySPHSystem(boundary_zones::Union{BoundaryZone, Nothing}...; - fluid_system::AbstractFluidSystem, buffer_size::Integer, - boundary_model) +function OpenBoundarySystem(boundary_zones::Union{BoundaryZone, Nothing}...; + fluid_system::AbstractFluidSystem, buffer_size::Integer, + boundary_model) boundary_zones_ = filter(bz -> !isnothing(bz), boundary_zones) reference_values_ = map(bz -> bz.reference_values, boundary_zones_) @@ -97,13 +97,13 @@ function OpenBoundarySPHSystem(boundary_zones::Union{BoundaryZone, Nothing}...; zone.prescribed_velocity), boundary_zones) - return OpenBoundarySPHSystem(boundary_model, initial_conditions, fluid_system, - fluid_system_index, smoothing_length, mass, density, - volume, pressure, boundary_candidates, fluid_candidates, - boundary_zone_indices, boundary_zones_new, buffer, cache) + return OpenBoundarySystem(boundary_model, initial_conditions, fluid_system, + fluid_system_index, smoothing_length, mass, density, + volume, pressure, boundary_candidates, fluid_candidates, + boundary_zone_indices, boundary_zones_new, buffer, cache) end -function initialize!(system::OpenBoundarySPHSystem, semi) +function initialize!(system::OpenBoundarySystem, semi) (; boundary_zones) = system update_boundary_zone_indices!(system, initial_coordinates(system), boundary_zones, semi) @@ -135,23 +135,23 @@ function create_cache_open_boundary(boundary_model, initial_condition, reference end end -timer_name(::OpenBoundarySPHSystem) = "open_boundary" -vtkname(system::OpenBoundarySPHSystem) = "open_boundary" +timer_name(::OpenBoundarySystem) = "open_boundary" +vtkname(system::OpenBoundarySystem) = "open_boundary" -function Base.show(io::IO, system::OpenBoundarySPHSystem) +function Base.show(io::IO, system::OpenBoundarySystem) @nospecialize system # reduce precompilation time - print(io, "OpenBoundarySPHSystem{", ndims(system), "}(") + print(io, "OpenBoundarySystem{", ndims(system), "}(") print(io, ") with ", nparticles(system), " particles") end -function Base.show(io::IO, ::MIME"text/plain", system::OpenBoundarySPHSystem) +function Base.show(io::IO, ::MIME"text/plain", system::OpenBoundarySystem) @nospecialize system # reduce precompilation time if get(io, :compact, false) show(io, system) else - summary_header(io, "OpenBoundarySPHSystem{$(ndims(system))}") + summary_header(io, "OpenBoundarySystem{$(ndims(system))}") summary_line(io, "#particles", nparticles(system)) summary_line(io, "#buffer_particles", system.buffer.buffer_size) summary_line(io, "#boundary_zones", length(system.boundary_zones)) @@ -161,50 +161,48 @@ function Base.show(io::IO, ::MIME"text/plain", system::OpenBoundarySPHSystem) end end -@inline function Base.eltype(::OpenBoundarySPHSystem{<:Any, ELTYPE}) where {ELTYPE} +@inline function Base.eltype(::OpenBoundarySystem{<:Any, ELTYPE}) where {ELTYPE} return ELTYPE end -@inline buffer(system::OpenBoundarySPHSystem) = system.buffer +@inline buffer(system::OpenBoundarySystem) = system.buffer # The `UpdateCallback` is required to update particle positions between time steps -@inline requires_update_callback(system::OpenBoundarySPHSystem) = true +@inline requires_update_callback(system::OpenBoundarySystem) = true -function smoothing_length(system::OpenBoundarySPHSystem, particle) +function smoothing_length(system::OpenBoundarySystem, particle) return system.smoothing_length end -@inline hydrodynamic_mass(system::OpenBoundarySPHSystem, particle) = system.mass[particle] +@inline hydrodynamic_mass(system::OpenBoundarySystem, particle) = system.mass[particle] -@inline function current_density(v, system::OpenBoundarySPHSystem) +@inline function current_density(v, system::OpenBoundarySystem) return system.density end -@inline function current_pressure(v, system::OpenBoundarySPHSystem) +@inline function current_pressure(v, system::OpenBoundarySystem) return system.pressure end -@inline function set_particle_pressure!(v, system::OpenBoundarySPHSystem, particle, - pressure) +@inline function set_particle_pressure!(v, system::OpenBoundarySystem, particle, pressure) system.pressure[particle] = pressure return v end -@inline function set_particle_density!(v, system::OpenBoundarySPHSystem, particle, - density) +@inline function set_particle_density!(v, system::OpenBoundarySystem, particle, density) system.density[particle] = density return v end -function update_boundary_interpolation!(system::OpenBoundarySPHSystem, v, u, v_ode, u_ode, +function update_boundary_interpolation!(system::OpenBoundarySystem, v, u, v_ode, u_ode, semi, t) update_boundary_model!(system, system.boundary_model, v, u, v_ode, u_ode, semi, t) end # This function is called by the `UpdateCallback`, as the integrator array might be modified -function update_open_boundary_eachstep!(system::OpenBoundarySPHSystem, v_ode, u_ode, +function update_open_boundary_eachstep!(system::OpenBoundarySystem, v_ode, u_ode, semi, t) (; boundary_model) = system @@ -300,7 +298,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) end # Buffer particle is outside the boundary zone -@inline function convert_particle!(system::OpenBoundarySPHSystem, fluid_system, +@inline function convert_particle!(system::OpenBoundarySystem, fluid_system, boundary_zone, particle, particle_new, v, u, v_fluid, u_fluid) relative_position = current_coords(u, system, particle) - boundary_zone.zone_origin @@ -358,7 +356,7 @@ end return system_new end -function write_v0!(v0, system::OpenBoundarySPHSystem) +function write_v0!(v0, system::OpenBoundarySystem) # This is as fast as a loop with `@inbounds`, but it's GPU-compatible indices = CartesianIndices(system.initial_condition.velocity) copyto!(v0, indices, system.initial_condition.velocity, indices) @@ -366,7 +364,7 @@ function write_v0!(v0, system::OpenBoundarySPHSystem) return v0 end -function write_u0!(u0, system::OpenBoundarySPHSystem) +function write_u0!(u0, system::OpenBoundarySystem) (; initial_condition) = system # This is as fast as a loop with `@inbounds`, but it's GPU-compatible @@ -378,20 +376,20 @@ end # To account for boundary effects in the viscosity term of the RHS, use the viscosity model # of the neighboring particle systems. -@inline function viscosity_model(system::OpenBoundarySPHSystem, +@inline function viscosity_model(system::OpenBoundarySystem, neighbor_system::AbstractFluidSystem) return neighbor_system.viscosity end -@inline function viscosity_model(system::OpenBoundarySPHSystem, +@inline function viscosity_model(system::OpenBoundarySystem, neighbor_system::AbstractBoundarySystem) return neighbor_system.boundary_model.viscosity end # When the neighbor is an open boundary system, just use the viscosity of the fluid `system` instead -@inline viscosity_model(system, neighbor_system::OpenBoundarySPHSystem) = system.viscosity +@inline viscosity_model(system, neighbor_system::OpenBoundarySystem) = system.viscosity -function system_data(system::OpenBoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode, semi) +function system_data(system::OpenBoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi) v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) @@ -403,6 +401,6 @@ function system_data(system::OpenBoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode return (; coordinates, velocity, density, pressure) end -function available_data(::OpenBoundarySPHSystem) +function available_data(::OpenBoundarySystem) return (:coordinates, :velocity, :density, :pressure) end diff --git a/src/schemes/boundary/rhs.jl b/src/schemes/boundary/rhs.jl index 047df3aa62..a5998f00a6 100644 --- a/src/schemes/boundary/rhs.jl +++ b/src/schemes/boundary/rhs.jl @@ -1,7 +1,7 @@ # Interaction of boundary with other systems function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, - particle_system::Union{AbstractBoundarySystem, OpenBoundarySPHSystem}, + particle_system::Union{AbstractBoundarySystem, OpenBoundarySystem}, neighbor_system, semi) # TODO Solids and moving boundaries should be considered in the continuity equation return dv @@ -10,7 +10,7 @@ end # For dummy particles with `ContinuityDensity`, solve the continuity equation function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, - particle_system::BoundarySPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, + particle_system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, neighbor_system::AbstractFluidSystem, semi) (; boundary_model) = particle_system fluid_density_calculator = neighbor_system.density_calculator diff --git a/src/schemes/boundary/system.jl b/src/schemes/boundary/system.jl index 619e699e88..46d405ebca 100644 --- a/src/schemes/boundary/system.jl +++ b/src/schemes/boundary/system.jl @@ -1,5 +1,6 @@ """ - BoundarySPHSystem(initial_condition, boundary_model; movement=nothing, adhesion_coefficient=0.0) + WallBoundarySystem(initial_condition, boundary_model; + prescribed_motion=nothing, adhesion_coefficient=0.0) System for boundaries modeled by boundary particles. The interaction between fluid and boundary particles is specified by the boundary model. @@ -9,78 +10,77 @@ The interaction between fluid and boundary particles is specified by the boundar - `boundary_model`: Boundary model (see [Boundary Models](@ref boundary_models)) # Keyword Arguments -- `movement`: For moving boundaries, a [`PrescribedMotion`](@ref) can be passed. +- `prescribed_motion`: For moving boundaries, a [`PrescribedMotion`](@ref) can be passed. - `adhesion_coefficient`: Coefficient specifying the adhesion of a fluid to the surface. Note: currently it is assumed that all fluids have the same adhesion coefficient. """ -struct BoundarySPHSystem{BM, NDIMS, ELTYPE <: Real, IC, CO, M, IM, - CA} <: AbstractBoundarySystem{NDIMS} +struct WallBoundarySystem{BM, NDIMS, ELTYPE <: Real, IC, CO, M, IM, + CA} <: AbstractBoundarySystem{NDIMS} initial_condition :: IC coordinates :: CO # Array{ELTYPE, 2} boundary_model :: BM - movement :: M + prescribed_motion :: M ismoving :: IM # Ref{Bool} (to make a mutable field compatible with GPUs) adhesion_coefficient :: ELTYPE cache :: CA - buffer :: Nothing # This constructor is necessary for Adapt.jl to work with this struct. # See the comments in general/gpu.jl for more details. - function BoundarySPHSystem(initial_condition, coordinates, boundary_model, movement, - ismoving, adhesion_coefficient, cache, buffer) + function WallBoundarySystem(initial_condition, coordinates, boundary_model, + prescribed_motion, ismoving, adhesion_coefficient, cache) ELTYPE = eltype(coordinates) new{typeof(boundary_model), size(coordinates, 1), ELTYPE, typeof(initial_condition), - typeof(coordinates), typeof(movement), typeof(ismoving), - typeof(cache)}(initial_condition, coordinates, boundary_model, movement, - ismoving, adhesion_coefficient, cache, buffer) + typeof(coordinates), typeof(prescribed_motion), typeof(ismoving), + typeof(cache)}(initial_condition, coordinates, boundary_model, + prescribed_motion, ismoving, adhesion_coefficient, cache) end end -function BoundarySPHSystem(initial_condition, model; movement=nothing, - adhesion_coefficient=0.0, color_value=0) +function WallBoundarySystem(initial_condition, model; prescribed_motion=nothing, + adhesion_coefficient=0.0, color_value=0) coordinates = copy(initial_condition.coordinates) - ismoving = Ref(!isnothing(movement)) + ismoving = Ref(!isnothing(prescribed_motion)) - cache = create_cache_boundary(movement, initial_condition) + cache = create_cache_boundary(prescribed_motion, initial_condition) cache = (cache..., color=Int(color_value)) - if movement !== nothing && isempty(movement.moving_particles) + if prescribed_motion !== nothing && isempty(prescribed_motion.moving_particles) # Default is an empty vector, since the number of particles is not known when # instantiating `PrescribedMotion`. - resize!(movement.moving_particles, nparticles(initial_condition)) - movement.moving_particles .= collect(1:nparticles(initial_condition)) + resize!(prescribed_motion.moving_particles, nparticles(initial_condition)) + prescribed_motion.moving_particles .= collect(1:nparticles(initial_condition)) end # Because of dispatches boundary model needs to be first! - return BoundarySPHSystem(initial_condition, coordinates, model, movement, - ismoving, adhesion_coefficient, cache, nothing) + return WallBoundarySystem(initial_condition, coordinates, model, prescribed_motion, + ismoving, adhesion_coefficient, cache) end -function Base.show(io::IO, system::BoundarySPHSystem) +function Base.show(io::IO, system::WallBoundarySystem) @nospecialize system # reduce precompilation time - print(io, "BoundarySPHSystem{", ndims(system), "}(") + print(io, "WallBoundarySystem{", ndims(system), "}(") print(io, system.boundary_model) - print(io, ", ", system.movement) + print(io, ", ", system.prescribed_motion) print(io, ", ", system.adhesion_coefficient) print(io, ", ", system.cache.color) print(io, ") with ", nparticles(system), " particles") end -function Base.show(io::IO, ::MIME"text/plain", system::BoundarySPHSystem) +function Base.show(io::IO, ::MIME"text/plain", system::WallBoundarySystem) @nospecialize system # reduce precompilation time if get(io, :compact, false) show(io, system) else - summary_header(io, "BoundarySPHSystem{$(ndims(system))}") + summary_header(io, "WallBoundarySystem{$(ndims(system))}") summary_line(io, "#particles", nparticles(system)) summary_line(io, "boundary model", system.boundary_model) summary_line(io, "movement function", - isnothing(system.movement) ? "nothing" : - string(system.movement.movement_function)) + isnothing(system.prescribed_motion) ? "nothing" : + string(system.prescribed_motion.movement_function)) summary_line(io, "adhesion coefficient", system.adhesion_coefficient) summary_line(io, "color", system.cache.color) summary_footer(io) @@ -176,7 +176,7 @@ function PrescribedMotion(movement_function, is_moving; moving_particles=nothing "Returning regular `Vector`s causes allocations and significant performance overhead." end - # Default value is an empty vector, which will be resized in the `BoundarySPHSystem` + # Default value is an empty vector, which will be resized in the `WallBoundarySystem` # constructor to move all particles. moving_particles = isnothing(moving_particles) ? Int[] : vec(moving_particles) @@ -192,28 +192,28 @@ function create_cache_boundary(::PrescribedMotion, initial_condition) return (; velocity, acceleration, initial_coordinates) end -timer_name(::Union{BoundarySPHSystem, BoundaryDEMSystem}) = "boundary" +timer_name(::Union{WallBoundarySystem, BoundaryDEMSystem}) = "boundary" -@inline function Base.eltype(system::Union{BoundarySPHSystem, BoundaryDEMSystem}) +@inline function Base.eltype(system::Union{WallBoundarySystem, BoundaryDEMSystem}) eltype(system.coordinates) end -@inline function initial_coordinates(system::BoundarySPHSystem) - initial_coordinates(system::BoundarySPHSystem, system.movement) +@inline function initial_coordinates(system::WallBoundarySystem) + initial_coordinates(system::WallBoundarySystem, system.prescribed_motion) end @inline initial_coordinates(system::BoundaryDEMSystem) = system.coordinates -@inline initial_coordinates(system::BoundarySPHSystem, ::Nothing) = system.coordinates +@inline initial_coordinates(system::WallBoundarySystem, ::Nothing) = system.coordinates # We need static initial coordinates as reference when system is moving -@inline function initial_coordinates(system::BoundarySPHSystem, movement) +@inline function initial_coordinates(system::WallBoundarySystem, prescribed_motion) return system.cache.initial_coordinates end -function (movement::PrescribedMotion)(system, t, semi) +function (prescribed_motion::PrescribedMotion)(system, t, semi) (; coordinates, cache) = system - (; movement_function, is_moving, moving_particles) = movement + (; movement_function, is_moving, moving_particles) = prescribed_motion (; acceleration, velocity) = cache system.ismoving[] = is_moving(t) @@ -235,34 +235,35 @@ function (movement::PrescribedMotion)(system, t, semi) return system end -function (movement::Nothing)(system::AbstractSystem, t, semi) +function (prescribed_motion::Nothing)(system::AbstractSystem, t, semi) system.ismoving[] = false return system end -@inline function nparticles(system::Union{BoundaryDEMSystem, BoundarySPHSystem}) +@inline function nparticles(system::Union{BoundaryDEMSystem, WallBoundarySystem}) size(system.coordinates, 2) end # No particle positions are advanced for boundary systems, # except when using `BoundaryModelDummyParticles` with `ContinuityDensity`. -@inline function n_moving_particles(system::Union{BoundarySPHSystem, BoundaryDEMSystem}) +@inline function n_moving_particles(system::Union{WallBoundarySystem, BoundaryDEMSystem}) return 0 end -@inline function n_moving_particles(system::BoundarySPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) +@inline function n_moving_particles(system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) return nparticles(system) end -@inline u_nvariables(system::Union{BoundarySPHSystem, BoundaryDEMSystem}) = 0 +@inline u_nvariables(system::Union{WallBoundarySystem, BoundaryDEMSystem}) = 0 # For BoundaryModelDummyParticles with ContinuityDensity, this needs to be 1. # For all other models and density calculators, it's irrelevant. -@inline v_nvariables(system::BoundarySPHSystem) = 1 +@inline v_nvariables(system::WallBoundarySystem) = 1 @inline v_nvariables(system::BoundaryDEMSystem) = 0 -@inline function current_coordinates(u, system::Union{BoundarySPHSystem, BoundaryDEMSystem}) +@inline function current_coordinates(u, + system::Union{WallBoundarySystem, BoundaryDEMSystem}) return system.coordinates end @@ -270,8 +271,8 @@ end return zero(SVector{ndims(system), eltype(system)}) end -@inline function current_velocity(v, system::BoundarySPHSystem, particle) - return current_velocity(v, system, system.movement, particle) +@inline function current_velocity(v, system::WallBoundarySystem, particle) + return current_velocity(v, system, system.prescribed_motion, particle) end @inline function current_velocity(v, system, movement, particle) @@ -288,12 +289,12 @@ end return zero(SVector{ndims(system), eltype(system)}) end -@inline function current_velocity(v, system::BoundarySPHSystem) - error("`current_velocity(v, system)` is not implemented for `BoundarySPHSystem`") +@inline function current_velocity(v, system::WallBoundarySystem) + error("`current_velocity(v, system)` is not implemented for `WallBoundarySystem`") end -@inline function current_acceleration(system::BoundarySPHSystem, particle) - return current_acceleration(system, system.movement, particle) +@inline function current_acceleration(system::WallBoundarySystem, particle) + return current_acceleration(system, system.prescribed_motion, particle) end @inline function current_acceleration(system, movement, particle) @@ -310,7 +311,7 @@ end return zero(SVector{ndims(system), eltype(system)}) end -@inline function viscous_velocity(v, system::BoundarySPHSystem, particle) +@inline function viscous_velocity(v, system::WallBoundarySystem, particle) return viscous_velocity(v, system.boundary_model.viscosity, system, particle) end @@ -322,34 +323,34 @@ end return current_velocity(v, system, particle) end -@inline function current_density(v, system::BoundarySPHSystem) +@inline function current_density(v, system::WallBoundarySystem) return current_density(v, system.boundary_model, system) end -@inline function current_pressure(v, system::BoundarySPHSystem) +@inline function current_pressure(v, system::WallBoundarySystem) return current_pressure(v, system.boundary_model, system) end -@inline function hydrodynamic_mass(system::BoundarySPHSystem, particle) +@inline function hydrodynamic_mass(system::WallBoundarySystem, particle) return system.boundary_model.hydrodynamic_mass[particle] end -@inline function smoothing_kernel(system::BoundarySPHSystem, distance, particle) +@inline function smoothing_kernel(system::WallBoundarySystem, distance, particle) (; smoothing_kernel, smoothing_length) = system.boundary_model return kernel(smoothing_kernel, distance, smoothing_length) end -@inline function smoothing_length(system::BoundarySPHSystem, particle) +@inline function smoothing_length(system::WallBoundarySystem, particle) return smoothing_length(system.boundary_model, particle) end -function update_positions!(system::BoundarySPHSystem, v, u, v_ode, u_ode, semi, t) - (; movement) = system +function update_positions!(system::WallBoundarySystem, v, u, v_ode, u_ode, semi, t) + (; prescribed_motion) = system - movement(system, t, semi) + prescribed_motion(system, t, semi) end -function update_quantities!(system::BoundarySPHSystem, v, u, v_ode, u_ode, semi, t) +function update_quantities!(system::WallBoundarySystem, v, u, v_ode, u_ode, semi, t) (; boundary_model) = system update_density!(boundary_model, system, v, u, v_ode, u_ode, semi) @@ -359,29 +360,29 @@ end # This update depends on the computed quantities of the fluid system and therefore # has to be in `update_boundary_interpolation!` after `update_quantities!`. -function update_boundary_interpolation!(system::BoundarySPHSystem, v, u, v_ode, u_ode, +function update_boundary_interpolation!(system::WallBoundarySystem, v, u, v_ode, u_ode, semi, t) (; boundary_model) = system - # Note that `update_pressure!(::BoundarySPHSystem, ...)` is empty, + # Note that `update_pressure!(::WallBoundarySystem, ...)` is empty, # so no pressure is updated in the previous update steps. update_pressure!(boundary_model, system, v, u, v_ode, u_ode, semi) return system end -function write_u0!(u0, system::Union{BoundarySPHSystem, BoundaryDEMSystem}) +function write_u0!(u0, system::Union{WallBoundarySystem, BoundaryDEMSystem}) return u0 end function write_v0!(v0, - system::Union{BoundarySPHSystem, + system::Union{WallBoundarySystem, BoundaryDEMSystem}) return v0 end function write_v0!(v0, - system::BoundarySPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) + system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) (; cache) = system.boundary_model (; initial_density) = cache @@ -390,11 +391,11 @@ function write_v0!(v0, return v0 end -function restart_with!(system::BoundarySPHSystem, v, u) +function restart_with!(system::WallBoundarySystem, v, u) return system end -function restart_with!(system::BoundarySPHSystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, +function restart_with!(system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}, v, u) (; initial_density) = model.cache @@ -407,7 +408,7 @@ end # To incorporate the effect at boundaries in the viscosity term of the RHS the neighbor # viscosity model has to be used. -@inline function viscosity_model(system::BoundarySPHSystem, +@inline function viscosity_model(system::WallBoundarySystem, neighbor_system::AbstractFluidSystem) return neighbor_system.viscosity end @@ -416,7 +417,7 @@ function calculate_dt(v_ode, u_ode, cfl_number, system::AbstractBoundarySystem, return Inf end -function initialize!(system::BoundarySPHSystem, semi) +function initialize!(system::WallBoundarySystem, semi) initialize_colorfield!(system, system.boundary_model, semi) return system end @@ -445,15 +446,15 @@ function initialize_colorfield!(system, ::BoundaryModelDummyParticles, semi) return system end -function system_smoothing_kernel(system::BoundarySPHSystem{<:BoundaryModelDummyParticles}) +function system_smoothing_kernel(system::WallBoundarySystem{<:BoundaryModelDummyParticles}) return system.boundary_model.smoothing_kernel end -function system_correction(system::BoundarySPHSystem{<:BoundaryModelDummyParticles}) +function system_correction(system::WallBoundarySystem{<:BoundaryModelDummyParticles}) return system.boundary_model.correction end -function system_data(system::BoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode, semi) +function system_data(system::WallBoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi) dv = [current_acceleration(system, particle) for particle in eachparticle(system)] v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) @@ -466,7 +467,7 @@ function system_data(system::BoundarySPHSystem, dv_ode, du_ode, v_ode, u_ode, se return (; coordinates, velocity, density, pressure, acceleration=dv) end -function available_data(::BoundarySPHSystem) +function available_data(::WallBoundarySystem) return (:coordinates, :velocity, :density, :pressure, :acceleration) end diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index 33ba9a7883..adb53cc942 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -36,7 +36,7 @@ See [Entropically Damped Artificial Compressibility for SPH](@ref edac) for more - `average_pressure_reduction`: Whether to subtract the average pressure of neighboring particles from the local pressure (default: `true` when using shifting, `false` otherwise). - `buffer_size`: Number of buffer particles. - This is needed when simulating with [`OpenBoundarySPHSystem`](@ref). + This is needed when simulating with [`OpenBoundarySystem`](@ref). - `correction`: Correction method used for this system. (default: no correction, see [Corrections](@ref corrections)) - `source_terms`: Additional source terms for this system. Has to be either `nothing` (by default), or a function of `(coords, velocity, density, pressure, t)` diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl index 1b0a577429..a688393fea 100644 --- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl +++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl @@ -47,7 +47,7 @@ function interact!(dv, v_particle_system, u_particle_system, # The following call is equivalent to # `p_a = current_pressure(v_particle_system, particle_system, particle)` # `p_b = current_pressure(v_neighbor_system, neighbor_system, neighbor)` - # Only when the neighbor system is a `BoundarySPHSystem` or a `TotalLagrangianSPHSystem` + # Only when the neighbor system is a `WallBoundarySystem` or a `TotalLagrangianSPHSystem` # with the boundary model `PressureMirroring`, this will return `p_b = p_a`, which is # the pressure of the fluid particle. p_a, @@ -160,7 +160,7 @@ end @inline function particle_neighbor_pressure(v_particle_system, v_neighbor_system, particle_system, - neighbor_system::BoundarySPHSystem{<:BoundaryModelDummyParticles{PressureMirroring}}, + neighbor_system::WallBoundarySystem{<:BoundaryModelDummyParticles{PressureMirroring}}, particle, neighbor) p_a = current_pressure(v_particle_system, particle_system, particle) diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index 21cb39a9f2..65a41a3442 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -41,7 +41,7 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method. formulation](@ref transport_velocity_formulation) to use with this system. Default is no shifting. - `buffer_size`: Number of buffer particles. - This is needed when simulating with [`OpenBoundarySPHSystem`](@ref). + This is needed when simulating with [`OpenBoundarySystem`](@ref). - `correction`: Correction method used for this system. (default: no correction, see [Corrections](@ref corrections)) - `source_terms`: Additional source terms for this system. Has to be either `nothing` (by default), or a function of `(coords, velocity, density, pressure, t)` diff --git a/src/schemes/schemes.jl b/src/schemes/schemes.jl index a34a403cd0..8c9bf320c8 100644 --- a/src/schemes/schemes.jl +++ b/src/schemes/schemes.jl @@ -4,7 +4,7 @@ include("fluid/fluid.jl") include("boundary/boundary.jl") include("structure/total_lagrangian_sph/total_lagrangian_sph.jl") include("structure/discrete_element_method/discrete_element_method.jl") -# Monaghan-Kajtar repulsive boundary particles require the `BoundarySPHSystem` +# Monaghan-Kajtar repulsive boundary particles require the `WallBoundarySystem` # and the `TotalLagrangianSPHSystem`. include("boundary/monaghan_kajtar/monaghan_kajtar.jl") diff --git a/src/schemes/structure/total_lagrangian_sph/rhs.jl b/src/schemes/structure/total_lagrangian_sph/rhs.jl index cb02c7ea01..a371abc6a1 100644 --- a/src/schemes/structure/total_lagrangian_sph/rhs.jl +++ b/src/schemes/structure/total_lagrangian_sph/rhs.jl @@ -169,7 +169,7 @@ end function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::TotalLagrangianSPHSystem, - neighbor_system::Union{BoundarySPHSystem, OpenBoundarySPHSystem}, semi) + neighbor_system::Union{WallBoundarySystem, OpenBoundarySystem}, semi) # TODO continuity equation? return dv end diff --git a/test/general/buffer.jl b/test/general/buffer.jl index 9f32ac98a2..854b219c79 100644 --- a/test/general/buffer.jl +++ b/test/general/buffer.jl @@ -8,12 +8,12 @@ open_boundary_layers=2, density=1.0, plane_normal=[1.0, 0.0], reference_density=1.0, reference_pressure=0.0, reference_velocity=[0, 0], boundary_type=InFlow()) - system = OpenBoundarySPHSystem(zone; fluid_system=FluidSystemMock3(), - boundary_model=BoundaryModelCharacteristicsLastiwka(), - buffer_size=0) - system_buffer = OpenBoundarySPHSystem(zone; buffer_size=5, - boundary_model=BoundaryModelCharacteristicsLastiwka(), - fluid_system=FluidSystemMock3()) + system = OpenBoundarySystem(zone; fluid_system=FluidSystemMock3(), + boundary_model=BoundaryModelCharacteristicsLastiwka(), + buffer_size=0) + system_buffer = OpenBoundarySystem(zone; buffer_size=5, + boundary_model=BoundaryModelCharacteristicsLastiwka(), + fluid_system=FluidSystemMock3()) n_particles = nparticles(system) diff --git a/test/general/custom_quantities.jl b/test/general/custom_quantities.jl index f75e5db25d..54e976b395 100644 --- a/test/general/custom_quantities.jl +++ b/test/general/custom_quantities.jl @@ -21,7 +21,7 @@ AdamiPressureExtrapolation(), smoothing_kernel, smoothing_length) - boundary_system = BoundarySPHSystem(initial_condition, boundary_model) + boundary_system = WallBoundarySystem(initial_condition, boundary_model) semi = Semidiscretization(fluid_system, boundary_system) diff --git a/test/general/interpolation.jl b/test/general/interpolation.jl index b7ee61a66b..60d668aef8 100644 --- a/test/general/interpolation.jl +++ b/test/general/interpolation.jl @@ -110,7 +110,7 @@ AdamiPressureExtrapolation(), smoothing_kernel, smoothing_length) - boundary_system = BoundarySPHSystem(bnd, boundary_model) + boundary_system = WallBoundarySystem(bnd, boundary_model) # Overwrite `system.pressure` because we skip the update step fluid_system.pressure .= fluid.pressure @@ -624,7 +624,7 @@ AdamiPressureExtrapolation(), smoothing_kernel, smoothing_length) - boundary_system = BoundarySPHSystem(bnd, boundary_model) + boundary_system = WallBoundarySystem(bnd, boundary_model) # Overwrite `system.pressure` because we skip the update step fluid_system.pressure .= fluid.pressure diff --git a/test/general/semidiscretization.jl b/test/general/semidiscretization.jl index 5621847a94..59646bdf30 100644 --- a/test/general/semidiscretization.jl +++ b/test/general/semidiscretization.jl @@ -94,7 +94,7 @@ boundary_model = BoundaryModelDummyParticles(ic.density, ic.mass, SummationDensity(), kernel, 1.0) - boundary_system = BoundarySPHSystem(ic, boundary_model) + boundary_system = WallBoundarySystem(ic, boundary_model) fluid_system = WeaklyCompressibleSPHSystem(ic, SummationDensity(), nothing, kernel, 1.0) diff --git a/test/io/read_vtk.jl b/test/io/read_vtk.jl index 2c02b299c9..d82e8c50c3 100644 --- a/test/io/read_vtk.jl +++ b/test/io/read_vtk.jl @@ -52,7 +52,7 @@ @test isapprox(fluid_system.cache.density, test.density, rtol=1e-5) end - @testset verbose=true "`BoundarySPHSystem`" begin + @testset verbose=true "`WallBoundarySystem`" begin boundary_model = BoundaryModelDummyParticles(expected_ic.density, expected_ic.mass, SummationDensity(), @@ -63,7 +63,7 @@ boundary_model.pressure .= expected_ic.pressure boundary_model.cache.density .= expected_ic.density - boundary_system = BoundarySPHSystem(expected_ic, boundary_model) + boundary_system = WallBoundarySystem(expected_ic, boundary_model) semi = Semidiscretization(boundary_system) # Create dummy ODE solutions @@ -73,16 +73,16 @@ x = (; v_ode, u_ode) vu_ode = (; x) - # Write out `BoundarySPHSystem` Simulation-File + # Write out `WallBoundarySystem` Simulation-File trixi2vtk(boundary_system, dvdu_ode, vu_ode, semi, 0.0, nothing; system_name="tmp_file_boundary", output_directory=tmp_dir, iter=1) - # Load `BoundarySPHSystem` Simulation-File + # Load `WallBoundarySystem` Simulation-File test = vtk2trixi(joinpath(tmp_dir, "tmp_file_boundary_1.vtu")) @test isapprox(boundary_system.coordinates, test.coordinates, rtol=1e-5) - # The velocity is always zero for `BoundarySPHSystem` + # The velocity is always zero for `WallBoundarySystem` @test isapprox(zeros(size(test.velocity)), test.velocity, rtol=1e-5) @test isapprox(boundary_model.pressure, test.pressure, rtol=1e-5) @test isapprox(boundary_model.cache.density, test.density, rtol=1e-5) diff --git a/test/schemes/boundary/dummy_particles/dummy_particles.jl b/test/schemes/boundary/dummy_particles/dummy_particles.jl index e0b433b856..4f46abe701 100644 --- a/test/schemes/boundary/dummy_particles/dummy_particles.jl +++ b/test/schemes/boundary/dummy_particles/dummy_particles.jl @@ -66,8 +66,8 @@ viscosity=viscosity) boundary_systems = [ - BoundarySPHSystem(boundary, boundary_model_adami), - BoundarySPHSystem(boundary, boundary_model_bernoulli), + WallBoundarySystem(boundary, boundary_model_adami), + WallBoundarySystem(boundary, boundary_model_bernoulli), TotalLagrangianSPHSystem(boundary, smoothing_kernel, smoothing_length, 1e6, 0.3; boundary_model=boundary_model_adami), @@ -234,7 +234,7 @@ AdamiPressureExtrapolation(), smoothing_kernel, smoothing_length) - boundary_system = BoundarySPHSystem(tank1.boundary, boundary_model) + boundary_system = WallBoundarySystem(tank1.boundary, boundary_model) viscosity = boundary_system.boundary_model.viscosity semi = DummySemidiscretization() diff --git a/test/schemes/boundary/dummy_particles/rhs.jl b/test/schemes/boundary/dummy_particles/rhs.jl index eb92fdf1b1..4d64fc61a5 100644 --- a/test/schemes/boundary/dummy_particles/rhs.jl +++ b/test/schemes/boundary/dummy_particles/rhs.jl @@ -47,8 +47,8 @@ PressureZeroing(), smoothing_kernel, smoothing_length) - boundary_system_zeroing = BoundarySPHSystem(initial_condition, - boundary_model_zeroing) + boundary_system_zeroing = WallBoundarySystem(initial_condition, + boundary_model_zeroing) boundary_model_continuity = BoundaryModelDummyParticles(initial_condition.density, initial_condition.mass, ContinuityDensity(), @@ -56,8 +56,8 @@ smoothing_length) # Overwrite `boundary_model_continuity.pressure` because we skip the update step boundary_model_continuity.pressure .= initial_condition.pressure - boundary_system_continuity = BoundarySPHSystem(initial_condition, - boundary_model_continuity) + boundary_system_continuity = WallBoundarySystem(initial_condition, + boundary_model_continuity) boundary_model_summation = BoundaryModelDummyParticles(initial_condition.density, initial_condition.mass, @@ -68,8 +68,8 @@ boundary_model_summation.pressure .= initial_condition.pressure # Density is stored in the cache boundary_model_summation.cache.density .= initial_condition.density - boundary_system_summation = BoundarySPHSystem(initial_condition, - boundary_model_summation) + boundary_system_summation = WallBoundarySystem(initial_condition, + boundary_model_summation) u_boundary = zeros(0, TrixiParticles.nparticles(initial_condition)) v_boundary = zeros(0, TrixiParticles.nparticles(initial_condition)) diff --git a/test/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl b/test/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl index 50face550b..d5c0042c28 100644 --- a/test/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl +++ b/test/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl @@ -35,7 +35,7 @@ spacing_ratio = 0.5 boundary_model = BoundaryModelMonaghanKajtar(K, spacing_ratio, 2particle_spacing, boundary.mass) - boundary_system = BoundarySPHSystem(boundary, boundary_model) + boundary_system = WallBoundarySystem(boundary, boundary_model) # Density is integrated with `ContinuityDensity` v = vcat(fluid.velocity, fluid.density') diff --git a/test/schemes/boundary/open_boundary/characteristic_variables.jl b/test/schemes/boundary/open_boundary/characteristic_variables.jl index 1920521caa..571634a5b3 100644 --- a/test/schemes/boundary/open_boundary/characteristic_variables.jl +++ b/test/schemes/boundary/open_boundary/characteristic_variables.jl @@ -63,9 +63,9 @@ density_calculator=ContinuityDensity(), smoothing_length, sound_speed) - boundary_system = OpenBoundarySPHSystem(boundary_zone; - fluid_system, buffer_size=0, - boundary_model=BoundaryModelCharacteristicsLastiwka()) + boundary_system = OpenBoundarySystem(boundary_zone; + fluid_system, buffer_size=0, + boundary_model=BoundaryModelCharacteristicsLastiwka()) semi = Semidiscretization(fluid_system, boundary_system) diff --git a/test/schemes/boundary/open_boundary/mirroring.jl b/test/schemes/boundary/open_boundary/mirroring.jl index ae7afc26bf..40832e539a 100644 --- a/test/schemes/boundary/open_boundary/mirroring.jl +++ b/test/schemes/boundary/open_boundary/mirroring.jl @@ -60,9 +60,9 @@ average_inflow_velocity=false, open_boundary_layers=10, density=1000.0, particle_spacing) - open_boundary = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelMirroringTafuni(), - buffer_size=0) + open_boundary = OpenBoundarySystem(inflow; fluid_system, + boundary_model=BoundaryModelMirroringTafuni(), + buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary) TrixiParticles.initialize_neighborhood_searches!(semi) @@ -156,9 +156,9 @@ average_inflow_velocity=false, open_boundary_layers=10, density=1000.0, particle_spacing) - open_boundary = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelMirroringTafuni(), - buffer_size=0) + open_boundary = OpenBoundarySystem(inflow; fluid_system, + boundary_model=BoundaryModelMirroringTafuni(), + buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary) TrixiParticles.initialize_neighborhood_searches!(semi) @@ -228,9 +228,9 @@ plane_normal=(i == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]), open_boundary_layers=open_boundary_layers, density=1000.0, particle_spacing, average_inflow_velocity=true) - open_boundary_in = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelMirroringTafuni(), - buffer_size=0) + open_boundary_in = OpenBoundarySystem(inflow; fluid_system, + boundary_model=BoundaryModelMirroringTafuni(), + buffer_size=0) semi = Semidiscretization(fluid_system, open_boundary_in) TrixiParticles.initialize_neighborhood_searches!(semi) @@ -280,9 +280,9 @@ plane_normal=[-1.0, 0.0], open_boundary_layers=10, density=1000.0, particle_spacing) - open_boundary_out = OpenBoundarySPHSystem(outflow; fluid_system, - boundary_model=BoundaryModelMirroringTafuni(), - buffer_size=0) + open_boundary_out = OpenBoundarySystem(outflow; fluid_system, + boundary_model=BoundaryModelMirroringTafuni(), + buffer_size=0) # Temporary semidiscretization just to extrapolate the pressure into the outflow system semi = Semidiscretization(fluid_system, open_boundary_out) @@ -304,9 +304,9 @@ inflow = BoundaryZone(; plane=plane_in, boundary_type=InFlow(), plane_normal=[1.0, 0.0], open_boundary_layers=10, density=1000.0, particle_spacing) - open_boundary_in = OpenBoundarySPHSystem(inflow; fluid_system, - boundary_model=BoundaryModelMirroringTafuni(), - buffer_size=0) + open_boundary_in = OpenBoundarySystem(inflow; fluid_system, + boundary_model=BoundaryModelMirroringTafuni(), + buffer_size=0) # Temporary semidiscretization just to extrapolate the pressure into the outflow system semi = Semidiscretization(fluid_system, open_boundary_in) diff --git a/test/schemes/fluid/surface_normal_sph.jl b/test/schemes/fluid/surface_normal_sph.jl index 35404d621d..cec4ea3a71 100644 --- a/test/schemes/fluid/surface_normal_sph.jl +++ b/test/schemes/fluid/surface_normal_sph.jl @@ -36,7 +36,7 @@ function create_boundary_system(coordinates, particle_spacing, state_equation, k correction=nothing, reference_particle_spacing=particle_spacing) - boundary_system = BoundarySPHSystem(wall, boundary_model, adhesion_coefficient=0.0) + boundary_system = WallBoundarySystem(wall, boundary_model, adhesion_coefficient=0.0) return boundary_system end diff --git a/test/systems/boundary_system.jl b/test/systems/boundary_system.jl index 4237508f80..7224e701a7 100644 --- a/test/systems/boundary_system.jl +++ b/test/systems/boundary_system.jl @@ -1,4 +1,4 @@ -@testset verbose=true "BoundarySPHSystem" begin +@testset verbose=true "WallBoundarySystem" begin coordinates_ = [ [1.0 2.0 1.0 2.0], @@ -16,14 +16,14 @@ initial_condition = InitialCondition(; coordinates, mass, density) model = Val(:boundary_model) - system = BoundarySPHSystem(initial_condition, model) + system = WallBoundarySystem(initial_condition, model) TrixiParticles.update_positions!(system, 0, 0, 0, 0, 0, 0.0) - @test system isa BoundarySPHSystem + @test system isa WallBoundarySystem @test ndims(system) == NDIMS @test system.coordinates == coordinates @test system.boundary_model == model - @test system.movement === nothing + @test system.prescribed_motion === nothing @test system.ismoving[] == false end end @@ -46,12 +46,12 @@ is_moving(t) = t < 1.0 bm = PrescribedMotion(movement_function, is_moving) - system = BoundarySPHSystem(initial_condition, model, movement=bm) + system = WallBoundarySystem(initial_condition, model, prescribed_motion=bm) # Moving t = 0.6 # semi is only passed to `@threaded` - system.movement(system, t, SerialBackend()) + system.prescribed_motion(system, t, SerialBackend()) if NDIMS == 2 new_coordinates = coordinates .+ [0.5 * t, 0.3 * t^2] new_velocity = [0.5, 0.6 * t] .* ones(size(new_coordinates)) @@ -68,7 +68,7 @@ # Stop moving t = 1.0 - system.movement(system, t, false) + system.prescribed_motion(system, t, false) @test isapprox(new_coordinates, system.coordinates) @@ -80,11 +80,11 @@ initial_condition = InitialCondition(; coordinates, mass, density) bm = PrescribedMotion(movement_function, is_moving, moving_particles=[2]) - system = BoundarySPHSystem(initial_condition, model, movement=bm) + system = WallBoundarySystem(initial_condition, model, prescribed_motion=bm) t = 0.1 # semi is only passed to `@threaded` - system.movement(system, t, SerialBackend()) + system.prescribed_motion(system, t, SerialBackend()) if NDIMS == 2 new_coordinates[:, 2] .+= [0.5 * t, 0.3 * t^2] @@ -111,15 +111,15 @@ initial_condition = InitialCondition(; coordinates, mass, density) model = (; hydrodynamic_mass=3) - system = BoundarySPHSystem(initial_condition, model) + system = WallBoundarySystem(initial_condition, model) - show_compact = "BoundarySPHSystem{2}((hydrodynamic_mass = 3,), nothing, 0.0, 0) with 2 particles" + show_compact = "WallBoundarySystem{2}((hydrodynamic_mass = 3,), nothing, 0.0, 0) with 2 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ BoundarySPHSystem{2} │ - │ ════════════════════ │ + │ WallBoundarySystem{2} │ + │ ═════════════════════ │ │ #particles: ………………………………………………… 2 │ │ boundary model: ……………………………………… (hydrodynamic_mass = 3,) │ │ movement function: ……………………………… nothing │ diff --git a/test/systems/open_boundary_system.jl b/test/systems/open_boundary_system.jl index cedc1b87f0..24086395b3 100644 --- a/test/systems/open_boundary_system.jl +++ b/test/systems/open_boundary_system.jl @@ -1,4 +1,4 @@ -@testset verbose=true "`OpenBoundarySPHSystem`" begin +@testset verbose=true "`OpenBoundarySystem`" begin @testset "`show`" begin # Mock fluid system @@ -9,16 +9,16 @@ inflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=InFlow()) - system = OpenBoundarySPHSystem(inflow; buffer_size=0, - boundary_model=BoundaryModelCharacteristicsLastiwka(), - fluid_system=FluidSystemMock2()) + system = OpenBoundarySystem(inflow; buffer_size=0, + boundary_model=BoundaryModelCharacteristicsLastiwka(), + fluid_system=FluidSystemMock2()) - show_compact = "OpenBoundarySPHSystem{2}() with 80 particles" + show_compact = "OpenBoundarySystem{2}() with 80 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ OpenBoundarySPHSystem{2} │ - │ ════════════════════════ │ + │ OpenBoundarySystem{2} │ + │ ═════════════════════ │ │ #particles: ………………………………………………… 80 │ │ #buffer_particles: ……………………………… 0 │ │ #boundary_zones: …………………………………… 1 │ @@ -31,16 +31,16 @@ outflow = BoundaryZone(; plane=([5.0, 0.0], [5.0, 1.0]), particle_spacing=0.05, plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=OutFlow()) - system = OpenBoundarySPHSystem(outflow; buffer_size=0, - boundary_model=BoundaryModelMirroringTafuni(), - fluid_system=FluidSystemMock2()) + system = OpenBoundarySystem(outflow; buffer_size=0, + boundary_model=BoundaryModelMirroringTafuni(), + fluid_system=FluidSystemMock2()) - show_compact = "OpenBoundarySPHSystem{2}() with 80 particles" + show_compact = "OpenBoundarySystem{2}() with 80 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ OpenBoundarySPHSystem{2} │ - │ ════════════════════════ │ + │ OpenBoundarySystem{2} │ + │ ═════════════════════ │ │ #particles: ………………………………………………… 80 │ │ #buffer_particles: ……………………………… 0 │ │ #boundary_zones: …………………………………… 1 │ @@ -50,16 +50,16 @@ @test repr("text/plain", system) == show_box - system = OpenBoundarySPHSystem(outflow, inflow; buffer_size=0, - boundary_model=BoundaryModelMirroringTafuni(), - fluid_system=FluidSystemMock2()) + system = OpenBoundarySystem(outflow, inflow; buffer_size=0, + boundary_model=BoundaryModelMirroringTafuni(), + fluid_system=FluidSystemMock2()) - show_compact = "OpenBoundarySPHSystem{2}() with 160 particles" + show_compact = "OpenBoundarySystem{2}() with 160 particles" @test repr(system) == show_compact show_box = """ ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ OpenBoundarySPHSystem{2} │ - │ ════════════════════════ │ + │ OpenBoundarySystem{2} │ + │ ═════════════════════ │ │ #particles: ………………………………………………… 160 │ │ #buffer_particles: ……………………………… 0 │ │ #boundary_zones: …………………………………… 2 │ From 3ec40a90b71cb40c09382cfb57d76023caa1b533 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:09:33 +0200 Subject: [PATCH 09/15] Renamings 3 (#905) * Rename `each_moving_particle`, `n_moving_particles` and `active_particles` * Reformat * Update NEWS.md --- NEWS.md | 4 ++ examples/n_body/n_body_benchmark_trixi.jl | 2 +- ext/TrixiParticlesOrdinaryDiffEqExt.jl | 6 +- src/general/abstract_system.jl | 14 ++-- src/general/buffer.jl | 8 ++- src/general/custom_quantities.jl | 5 +- src/general/density_calculators.jl | 2 +- src/general/semidiscretization.jl | 34 +++++----- src/io/write_vtk.jl | 4 +- src/preprocessing/particle_packing/system.jl | 6 +- .../dummy_particles/dummy_particles.jl | 2 +- .../boundary/open_boundary/boundary_zones.jl | 2 +- .../method_of_characteristics.jl | 12 ++-- .../boundary/open_boundary/mirroring.jl | 8 +-- src/schemes/boundary/open_boundary/system.jl | 4 +- src/schemes/boundary/system.jl | 5 +- .../fluid/entropically_damped_sph/rhs.jl | 12 ++-- .../fluid/entropically_damped_sph/system.jl | 8 ++- src/schemes/fluid/shifting_techniques.jl | 12 ++-- src/schemes/fluid/surface_normal_sph.jl | 12 ++-- src/schemes/fluid/surface_tension.jl | 4 +- .../density_diffusion.jl | 4 +- .../fluid/weakly_compressible_sph/rhs.jl | 10 +-- .../fluid/weakly_compressible_sph/system.jl | 4 +- .../structure/discrete_element_method/rhs.jl | 8 +-- .../structure/total_lagrangian_sph/rhs.jl | 20 +++--- .../structure/total_lagrangian_sph/system.jl | 66 +++++++++---------- test/general/buffer.jl | 10 +-- test/general/semidiscretization.jl | 7 +- .../structure/total_lagrangian_sph/rhs.jl | 4 +- test/systems/edac_system.jl | 6 +- test/systems/tlsph_system.jl | 6 +- test/systems/wcsph_system.jl | 6 +- 33 files changed, 166 insertions(+), 151 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7830c69b59..3c8a2941be 100644 --- a/NEWS.md +++ b/NEWS.md @@ -43,6 +43,10 @@ used in the Julia ecosystem. Notable changes will be documented in this file for `(system, v_ode, u_ode, semi, t)` now need to be functions of `(system, dv_ode, du_ode, v_ode, u_ode, semi, t)` (#879). +- Renamed `each_moving_particle` to `each_integrated_particle`, + `n_moving_particles` to `n_integrated_particles` + and `active_particles` to `each_active_particle`. + ### Features - Added consistent particle shifting by Sun et al. (2019) as `ConsistentShiftingSun2019` (#888). diff --git a/examples/n_body/n_body_benchmark_trixi.jl b/examples/n_body/n_body_benchmark_trixi.jl index 925f6283d9..828f046bf2 100644 --- a/examples/n_body/n_body_benchmark_trixi.jl +++ b/examples/n_body/n_body_benchmark_trixi.jl @@ -18,7 +18,7 @@ function TrixiParticles.interact!(dv, v_particle_system, u_particle_system, neighbor_system::NBodySystem) (; mass, G) = neighbor_system - for particle in TrixiParticles.each_moving_particle(particle_system) + for particle in TrixiParticles.each_integrated_particle(particle_system) particle_coords = TrixiParticles.current_coords(u_particle_system, particle_system, particle) diff --git a/ext/TrixiParticlesOrdinaryDiffEqExt.jl b/ext/TrixiParticlesOrdinaryDiffEqExt.jl index 496e8dd199..7178978156 100644 --- a/ext/TrixiParticlesOrdinaryDiffEqExt.jl +++ b/ext/TrixiParticlesOrdinaryDiffEqExt.jl @@ -8,7 +8,7 @@ module TrixiParticlesOrdinaryDiffEqExt # We need to load the name `PointNeighbors` because `@threaded` translates # to `PointNeighbors.parallel_foreach`, so `PointNeighbors` must be available. -using TrixiParticles: TrixiParticles, @threaded, each_moving_particle, +using TrixiParticles: TrixiParticles, @threaded, each_integrated_particle, WeaklyCompressibleSPHSystem, ContinuityDensity, PointNeighbors @@ -137,7 +137,7 @@ end # For WCSPH, only update the first NDIMS components of the velocity. # With `ContinuityDensity`, the last component is the density, # which is updated separately. - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) for i in 1:ndims(system) du_system[i, particle] = duprev_system[i, particle] + dt * kdu_system[i, particle] @@ -160,7 +160,7 @@ end @muladd function update_density!(du_system, kdu_system, duprev_system, ::ContinuityDensity, system, semi, dt) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) density_prev = duprev_system[end, particle] density_half = du_system[end, particle] epsilon = -kdu_system[end, particle] / density_half * dt diff --git a/src/general/abstract_system.jl b/src/general/abstract_system.jl index 70971a2535..1d0bc2813f 100644 --- a/src/general/abstract_system.jl +++ b/src/general/abstract_system.jl @@ -27,20 +27,22 @@ vtkname(system::AbstractBoundarySystem) = "boundary" @inline nparticles(system) = length(system.mass) # Number of particles in the system whose positions are to be integrated (corresponds to the size of u and du) -@inline n_moving_particles(system) = nparticles(system) +@inline n_integrated_particles(system) = nparticles(system) -@inline eachparticle(system::AbstractSystem) = active_particles(system) +@inline eachparticle(system::AbstractSystem) = each_active_particle(system) @inline eachparticle(initial_condition) = Base.OneTo(nparticles(initial_condition)) # Wrapper for systems with `SystemBuffer` -@inline each_moving_particle(system) = each_moving_particle(system, buffer(system)) -@inline each_moving_particle(system, ::Nothing) = Base.OneTo(n_moving_particles(system)) +@inline each_integrated_particle(system) = each_integrated_particle(system, buffer(system)) +@inline function each_integrated_particle(system, ::Nothing) + return Base.OneTo(n_integrated_particles(system)) +end @inline active_coordinates(u, system) = active_coordinates(u, system, buffer(system)) @inline active_coordinates(u, system, ::Nothing) = current_coordinates(u, system) -@inline active_particles(system) = active_particles(system, buffer(system)) -@inline active_particles(system, ::Nothing) = Base.OneTo(nparticles(system)) +@inline each_active_particle(system) = each_active_particle(system, buffer(system)) +@inline each_active_particle(system, ::Nothing) = Base.OneTo(nparticles(system)) @inline function set_zero!(du) du .= zero(eltype(du)) diff --git a/src/general/buffer.jl b/src/general/buffer.jl index 214d9765bc..1e2c196799 100644 --- a/src/general/buffer.jl +++ b/src/general/buffer.jl @@ -54,11 +54,13 @@ end return buffer end -@inline each_moving_particle(system, buffer) = active_particles(system, buffer) +@inline each_integrated_particle(system, buffer) = each_active_particle(system, buffer) -@inline active_coordinates(u, system, buffer) = view(u, :, active_particles(system, buffer)) +@inline function active_coordinates(u, system, buffer) + return view(u, :, each_active_particle(system, buffer)) +end -@inline function active_particles(system, buffer) +@inline function each_active_particle(system, buffer) return view(buffer.eachparticle, 1:buffer.active_particle_count[]) end diff --git a/src/general/custom_quantities.jl b/src/general/custom_quantities.jl index f9c63ec1a1..c35de55589 100644 --- a/src/general/custom_quantities.jl +++ b/src/general/custom_quantities.jl @@ -9,8 +9,9 @@ function kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, t) # TODO: `current_velocity` should only contain active particles # (see https://github.com/trixi-framework/TrixiParticles.jl/issues/850) velocity = reinterpret(reshape, SVector{ndims(system), eltype(v)}, - view(current_velocity(v, system), :, active_particles(system))) - mass = view(system.mass, active_particles(system)) + view(current_velocity(v, system), :, + each_active_particle(system))) + mass = view(system.mass, each_active_particle(system)) return mapreduce(+, velocity, mass) do v_i, m_i return m_i * dot(v_i, v_i) / 2 diff --git a/src/general/density_calculators.jl b/src/general/density_calculators.jl index efeff6017e..65937de01e 100644 --- a/src/general/density_calculators.jl +++ b/src/general/density_calculators.jl @@ -24,7 +24,7 @@ difference of the coordinates, ``v_{ab} = v_a - v_b`` of the velocities of parti struct ContinuityDensity end function summation_density!(system, semi, u, u_ode, density; - particles=each_moving_particle(system)) + particles=each_integrated_particle(system)) set_zero!(density) # Use all other systems for the density summation diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl index 59536a9a4c..320e3ad8ba 100644 --- a/src/general/semidiscretization.jl +++ b/src/general/semidiscretization.jl @@ -79,11 +79,11 @@ function Semidiscretization(systems::Union{AbstractSystem, Nothing}...; # Other checks might be added here later. check_configuration(systems, neighborhood_search) - sizes_u = [u_nvariables(system) * n_moving_particles(system) + sizes_u = [u_nvariables(system) * n_integrated_particles(system) for system in systems] ranges_u = Tuple((sum(sizes_u[1:(i - 1)]) + 1):sum(sizes_u[1:i]) for i in eachindex(sizes_u)) - sizes_v = [v_nvariables(system) * n_moving_particles(system) + sizes_v = [v_nvariables(system) * n_integrated_particles(system) for system in systems] ranges_v = Tuple((sum(sizes_v[1:(i - 1)]) + 1):sum(sizes_v[1:i]) for i in eachindex(sizes_v)) @@ -288,8 +288,8 @@ function semidiscretize(semi, tspan; reset_threads=true) Polyester.reset_threads!() end - sizes_u = (u_nvariables(system) * n_moving_particles(system) for system in systems) - sizes_v = (v_nvariables(system) * n_moving_particles(system) for system in systems) + sizes_u = (u_nvariables(system) * n_integrated_particles(system) for system in systems) + sizes_v = (v_nvariables(system) * n_integrated_particles(system) for system in systems) # Use either the specified backend, e.g., `CUDABackend` or `MetalBackend` or # use CPU vectors for all CPU backends. @@ -396,7 +396,7 @@ function initialize_neighborhood_searches!(semi) PointNeighbors.initialize!(get_neighborhood_search(system, neighbor, semi), initial_coordinates(system), initial_coordinates(neighbor), - eachindex_y=active_particles(neighbor)) + eachindex_y=each_active_particle(neighbor)) end end @@ -410,10 +410,11 @@ end range = ranges_v[system_indices(system, semi)] - @boundscheck @assert length(range) == v_nvariables(system) * n_moving_particles(system) + @boundscheck @assert length(range) == + v_nvariables(system) * n_integrated_particles(system) return wrap_array(v_ode, range, - (StaticInt(v_nvariables(system)), n_moving_particles(system))) + (StaticInt(v_nvariables(system)), n_integrated_particles(system))) end @inline function wrap_u(u_ode, system, semi) @@ -421,10 +422,11 @@ end range = ranges_u[system_indices(system, semi)] - @boundscheck @assert length(range) == u_nvariables(system) * n_moving_particles(system) + @boundscheck @assert length(range) == + u_nvariables(system) * n_integrated_particles(system) return wrap_array(u_ode, range, - (StaticInt(u_nvariables(system)), n_moving_particles(system))) + (StaticInt(u_nvariables(system)), n_integrated_particles(system))) end @inline function wrap_array(array::Array, range, size) @@ -462,7 +464,7 @@ function drift!(du_ode, v_ode, u_ode, semi, t) du = wrap_u(du_ode, system, semi) v = wrap_v(v_ode, system, semi) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) # This can be dispatched per system add_velocity!(du, v, particle, system) end @@ -588,7 +590,7 @@ function add_source_terms!(dv_ode, v_ode, u_ode, semi, t) v = wrap_v(v_ode, system, semi) u = wrap_u(u_ode, system, semi) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) # Dispatch by system type to exclude boundary systems add_acceleration!(dv, particle, system) add_source_terms_inner!(dv, v, u, particle, system, source_terms(system), t) @@ -717,7 +719,7 @@ function update_nhs!(neighborhood_search, update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), - semi, points_moving=(true, true), eachindex_y=active_particles(neighbor)) + semi, points_moving=(true, true), eachindex_y=each_active_particle(neighbor)) end function update_nhs!(neighborhood_search, @@ -740,7 +742,7 @@ function update_nhs!(neighborhood_search, update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), - semi, points_moving=(true, true), eachindex_y=active_particles(neighbor)) + semi, points_moving=(true, true), eachindex_y=each_active_particle(neighbor)) end function update_nhs!(neighborhood_search, @@ -753,7 +755,7 @@ function update_nhs!(neighborhood_search, update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), - semi, points_moving=(true, true), eachindex_y=active_particles(neighbor)) + semi, points_moving=(true, true), eachindex_y=each_active_particle(neighbor)) end function update_nhs!(neighborhood_search, @@ -777,7 +779,7 @@ function update_nhs!(neighborhood_search, update!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), - semi, points_moving=(true, true), eachindex_y=active_particles(neighbor)) + semi, points_moving=(true, true), eachindex_y=each_active_particle(neighbor)) end function update_nhs!(neighborhood_search, @@ -813,7 +815,7 @@ function update_nhs!(neighborhood_search, current_coordinates(u_system, system), current_coordinates(u_neighbor, neighbor), semi, points_moving=(system.ismoving[], true), - eachindex_y=active_particles(neighbor)) + eachindex_y=each_active_particle(neighbor)) end # This function is the same as the one above to avoid ambiguous dispatch when using `Union` diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index 9db388dcd4..303c8aa5d7 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -136,7 +136,7 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; vtk["ndims"] = ndims(system) vtk["particle_spacing"] = [particle_spacing(system, particle) - for particle in active_particles(system)] + for particle in each_active_particle(system)] # Extract custom quantities for this system if !isempty(custom_quantities) @@ -307,7 +307,7 @@ function write2vtk!(vtk, v, u, t, system::AbstractFluidSystem) if system.surface_tension isa SurfaceTensionMorris || system.surface_tension isa SurfaceTensionMomentumMorris - surface_tension = zeros((ndims(system), n_moving_particles(system))) + surface_tension = zeros((ndims(system), n_integrated_particles(system))) system_coords = current_coordinates(u, system) surface_tension_a = surface_tension_model(system) diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl index 9483aa2789..70aedd7dcb 100644 --- a/src/preprocessing/particle_packing/system.jl +++ b/src/preprocessing/particle_packing/system.jl @@ -251,8 +251,8 @@ function kinetic_energy(system::ParticlePackingSystem, v_ode, u_ode, semi, t) # Exclude boundary packing system is_boundary && return zero(eltype(system)) - # If `each_moving_particle` is empty (no moving particles), return zero - return sum(each_moving_particle(system), init=zero(eltype(system))) do particle + # If `each_integrated_particle` is empty (no integrated particles), return zero + return sum(each_integrated_particle(system), init=zero(eltype(system))) do particle velocity = advection_velocity(v, system, particle) return initial_condition.mass[particle] * dot(velocity, velocity) / 2 end @@ -366,7 +366,7 @@ end # Update from `UpdateCallback` (between time steps) @inline function update_transport_velocity!(system::ParticlePackingSystem, v_ode, semi) v = wrap_v(v_ode, system, semi) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) for i in 1:ndims(system) system.advection_velocity[i, particle] = v[i, particle] diff --git a/src/schemes/boundary/dummy_particles/dummy_particles.jl b/src/schemes/boundary/dummy_particles/dummy_particles.jl index 0d3755e5ef..906c88c520 100644 --- a/src/schemes/boundary/dummy_particles/dummy_particles.jl +++ b/src/schemes/boundary/dummy_particles/dummy_particles.jl @@ -501,7 +501,7 @@ end # This needs to be serial to avoid race conditions when writing into `system` foreach_point_neighbor(neighbor_system, system, neighbor_coords, system_coords, semi; - points=each_moving_particle(neighbor_system), + points=each_integrated_particle(neighbor_system), parallelization_backend=SerialBackend()) do neighbor, particle, pos_diff, distance # Since neighbor and particle are switched diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl index 33fd3614fc..d1fc5ce619 100644 --- a/src/schemes/boundary/open_boundary/boundary_zones.jl +++ b/src/schemes/boundary/open_boundary/boundary_zones.jl @@ -413,7 +413,7 @@ end function update_boundary_zone_indices!(system, u, boundary_zones, semi) set_zero!(system.boundary_zone_indices) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) particle_coords = current_coords(u, system, particle) for (zone_id, boundary_zone) in enumerate(boundary_zones) diff --git a/src/schemes/boundary/open_boundary/method_of_characteristics.jl b/src/schemes/boundary/open_boundary/method_of_characteristics.jl index 48842cde5a..d650a3e7cf 100644 --- a/src/schemes/boundary/open_boundary/method_of_characteristics.jl +++ b/src/schemes/boundary/open_boundary/method_of_characteristics.jl @@ -46,7 +46,7 @@ end end # Update quantities based on the characteristic variables - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) boundary_zone = current_boundary_zone(system, particle) (; flow_direction) = boundary_zone @@ -124,8 +124,8 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) # Loop over all fluid neighbors within the kernel cutoff foreach_point_neighbor(system, fluid_system, system_coords, fluid_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, neighbor, + pos_diff, distance boundary_zone = current_boundary_zone(system, particle) (; flow_direction) = boundary_zone @@ -164,7 +164,7 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) # Thus, we compute the characteristics for the particles that are outside the influence # of fluid particles by using the average of the values of the previous time step. # See eq. 27 in Negi (2020) https://doi.org/10.1016/j.cma.2020.113119 - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) # Particle is outside of the influence of fluid particles if isapprox(volume[particle], 0) @@ -175,7 +175,7 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) avg_J3 = zero(eltype(volume)) counter = 0 - for neighbor in each_moving_particle(system) + for neighbor in each_integrated_particle(system) # Make sure that only neighbors in the influence of # the fluid particles are used. if volume[neighbor] > sqrt(eps()) @@ -230,7 +230,7 @@ function average_velocity!(v, u, system, ::BoundaryModelCharacteristicsLastiwka, particles_in_zone = findall(particle -> boundary_zone == current_boundary_zone(system, particle), - each_moving_particle(system)) + each_integrated_particle(system)) # Division inside the `sum` closure to maintain GPU compatibility avg_velocity = sum(particles_in_zone) do particle diff --git a/src/schemes/boundary/open_boundary/mirroring.jl b/src/schemes/boundary/open_boundary/mirroring.jl index b90f55bf62..4f1269df94 100644 --- a/src/schemes/boundary/open_boundary/mirroring.jl +++ b/src/schemes/boundary/open_boundary/mirroring.jl @@ -92,7 +92,7 @@ function update_boundary_quantities!(system, boundary_model::BoundaryModelMirror end end - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) boundary_zone = current_boundary_zone(system, particle) (; prescribed_density, prescribed_pressure, prescribed_velocity) = boundary_zone @@ -141,7 +141,7 @@ function extrapolate_values!(system, # of the ghost node positions of each particle. # We can do this because we require the neighborhood search to support querying neighbors # of arbitrary positions (see `PointNeighbors.requires_update`). - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) boundary_zone = current_boundary_zone(system, particle) (; prescribed_density, prescribed_pressure, prescribed_velocity) = boundary_zone @@ -288,7 +288,7 @@ function extrapolate_values!(system, mirror_method::ZerothOrderMirroring, # of the ghost node positions of each particle. # We can do this because we require the neighborhood search to support querying neighbors # of arbitrary positions (see `PointNeighbors.requires_update`). - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) boundary_zone = current_boundary_zone(system, particle) (; prescribed_pressure, prescribed_density, prescribed_velocity) = boundary_zone @@ -501,7 +501,7 @@ function average_velocity!(v, u, system, boundary_zone, semi) particles_in_zone = findall(particle -> boundary_zone == current_boundary_zone(system, particle), - each_moving_particle(system)) + each_integrated_particle(system)) intersect!(candidates, particles_in_zone) diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index c03bd763ad..08f332e928 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -230,7 +230,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) boundary_candidates .= false # Check the boundary particles whether they're leaving the boundary zone - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) particle_coords = current_coords(u, system, particle) # Check if boundary particle is outside the boundary zone @@ -261,7 +261,7 @@ function check_domain!(system, v, u, v_ode, u_ode, semi) fluid_candidates .= false # Check the fluid particles whether they're entering the boundary zone - @threaded semi for fluid_particle in each_moving_particle(fluid_system) + @threaded semi for fluid_particle in each_integrated_particle(fluid_system) fluid_coords = current_coords(u_fluid, fluid_system, fluid_particle) # Check if fluid particle is in any boundary zone diff --git a/src/schemes/boundary/system.jl b/src/schemes/boundary/system.jl index 46d405ebca..ffde524df5 100644 --- a/src/schemes/boundary/system.jl +++ b/src/schemes/boundary/system.jl @@ -247,11 +247,12 @@ end # No particle positions are advanced for boundary systems, # except when using `BoundaryModelDummyParticles` with `ContinuityDensity`. -@inline function n_moving_particles(system::Union{WallBoundarySystem, BoundaryDEMSystem}) +@inline function n_integrated_particles(system::Union{WallBoundarySystem, + BoundaryDEMSystem}) return 0 end -@inline function n_moving_particles(system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) +@inline function n_integrated_particles(system::WallBoundarySystem{<:BoundaryModelDummyParticles{ContinuityDensity}}) return nparticles(system) end diff --git a/src/schemes/fluid/entropically_damped_sph/rhs.jl b/src/schemes/fluid/entropically_damped_sph/rhs.jl index 15d0a0991c..9989769a2d 100644 --- a/src/schemes/fluid/entropically_damped_sph/rhs.jl +++ b/src/schemes/fluid/entropically_damped_sph/rhs.jl @@ -11,14 +11,14 @@ function interact!(dv, v_particle_system, u_particle_system, surface_tension_a = surface_tension_model(particle_system) surface_tension_b = surface_tension_model(neighbor_system) - # Loop over all pairs of particles and neighbors within the kernel cutoff. + # Loop over all pairs of particles and neighbors within the kernel cutoff foreach_point_neighbor(particle_system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(particle_system)) do particle, - neighbor, - pos_diff, - distance - # Only consider particles with a distance > 0. + points=each_integrated_particle(particle_system)) do particle, + neighbor, + pos_diff, + distance + # Only consider particles with a distance > 0 distance < sqrt(eps()) && return rho_a = current_density(v_particle_system, particle_system, particle) diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index adb53cc942..a66577d87d 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -345,8 +345,10 @@ function update_average_pressure!(system, ::Val{true}, v_ode, u_ode, semi) # Loop over all pairs of particles and neighbors within the kernel cutoff. foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, + neighbor, + pos_diff, + distance pressure_average[particle] += current_pressure(v_neighbor_system, neighbor_system, neighbor) neighbor_counter[particle] += 1 @@ -376,7 +378,7 @@ function write_v0!(v0, system::EntropicallyDampedSPHSystem, ::ContinuityDensity) end function restart_with!(system::EntropicallyDampedSPHSystem, v, u) - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) system.initial_condition.coordinates[:, particle] .= u[:, particle] system.initial_condition.velocity[:, particle] .= v[1:ndims(system), particle] system.initial_condition.pressure[particle] = v[end, particle] diff --git a/src/schemes/fluid/shifting_techniques.jl b/src/schemes/fluid/shifting_techniques.jl index 6584e5f93f..44de644911 100644 --- a/src/schemes/fluid/shifting_techniques.jl +++ b/src/schemes/fluid/shifting_techniques.jl @@ -454,8 +454,10 @@ function update_shifting_inner!(system, shifting::ParticleShiftingTechnique, foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, + neighbor, + pos_diff, + distance m_b = hydrodynamic_mass(neighbor_system, neighbor) rho_a = current_density(v, system, particle) rho_b = current_density(v_neighbor, neighbor_system, neighbor) @@ -618,8 +620,10 @@ function update_shifting!(system, shifting::TransportVelocityAdami, v, u, v_ode, foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, + neighbor, + pos_diff, + distance m_a = @inbounds hydrodynamic_mass(system, particle) m_b = @inbounds hydrodynamic_mass(neighbor_system, neighbor) diff --git a/src/schemes/fluid/surface_normal_sph.jl b/src/schemes/fluid/surface_normal_sph.jl index 13fb53f5a2..30b389eed2 100644 --- a/src/schemes/fluid/surface_normal_sph.jl +++ b/src/schemes/fluid/surface_normal_sph.jl @@ -58,8 +58,8 @@ function calc_normal!(system::AbstractFluidSystem, neighbor_system::AbstractFlui foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_system_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, neighbor, + pos_diff, distance m_b = hydrodynamic_mass(neighbor_system, neighbor) density_neighbor = current_density(v_neighbor_system, neighbor_system, neighbor) @@ -129,7 +129,7 @@ function remove_invalid_normals!(system::AbstractFluidSystem, surface_tension, (; cache) = system # We remove invalid normals (too few neighbors) to reduce the impact of underdefined normals - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) # A corner has that many neighbors assuming a regular 2 * r distribution and a compact_support of 4r if cache.neighbor_count[particle] < 2^ndims(system) + 1 cache.surface_normal[1:ndims(system), particle] .= 0 @@ -154,7 +154,7 @@ function remove_invalid_normals!(system::AbstractFluidSystem, normal_condition2 = (interface_threshold / compact_support(smoothing_kernel, smoothing_length_))^2 - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) # Heuristic condition if there is no gas phase to find the free surface. # We remove normals for particles which have a lot of support e.g. they are in the interior. @@ -250,8 +250,8 @@ function calc_curvature!(system::AbstractFluidSystem, neighbor_system::AbstractF end # Eq. 23 - for i in 1:n_moving_particles(system) - curvature[i] /= (correction_factor[i] + eps()) + for particle in each_integrated_particle(system) + curvature[particle] /= (correction_factor[particle] + eps()) end return system diff --git a/src/schemes/fluid/surface_tension.jl b/src/schemes/fluid/surface_tension.jl index f96b917e8e..9514c2f4e8 100644 --- a/src/schemes/fluid/surface_tension.jl +++ b/src/schemes/fluid/surface_tension.jl @@ -242,7 +242,7 @@ function compute_stress_tensors!(system::AbstractFluidSystem, NDIMS = ndims(system) @trixi_timeit timer() "compute surface stress tensor" begin - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) normal = surface_normal(system, particle) delta_s_particle = delta_s[particle] if delta_s_particle > eps() @@ -271,7 +271,7 @@ function compute_surface_delta_function!(system, ::SurfaceTensionMomentumMorris, set_zero!(delta_s) - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) delta_s[particle] = norm(surface_normal(system, particle)) end return system diff --git a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl index 552f9a2b45..41c3a9f55f 100644 --- a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl +++ b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl @@ -183,8 +183,8 @@ function update!(density_diffusion::DensityDiffusionAntuono, v, u, system, semi) set_zero!(normalized_density_gradient) foreach_point_neighbor(system, system, system_coords, system_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - pos_diff, distance + points=each_integrated_particle(system)) do particle, neighbor, + pos_diff, distance # Only consider particles with a distance > 0 distance < sqrt(eps(typeof(distance))) && return diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl index a688393fea..2b62825a53 100644 --- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl +++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl @@ -19,13 +19,13 @@ function interact!(dv, v_particle_system, u_particle_system, # the following code and the two other lines below that are marked as "debug example". # debug_array = zeros(ndims(particle_system), nparticles(particle_system)) - # Loop over all pairs of particles and neighbors within the kernel cutoff. + # Loop over all pairs of particles and neighbors within the kernel cutoff foreach_point_neighbor(particle_system, neighbor_system, system_coords, neighbor_system_coords, semi; - points=each_moving_particle(particle_system)) do particle, - neighbor, - pos_diff, - distance + points=each_integrated_particle(particle_system)) do particle, + neighbor, + pos_diff, + distance # `foreach_point_neighbor` makes sure that `particle` and `neighbor` are # in bounds of the respective system. For performance reasons, we use `@inbounds` # in this hot loop to avoid bounds checking when extracting particle quantities. diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index 65a41a3442..1b7da00d5e 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -410,7 +410,7 @@ function write_v0!(v0, system::WeaklyCompressibleSPHSystem, ::ContinuityDensity) end function restart_with!(system::WeaklyCompressibleSPHSystem, v, u) - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) system.initial_condition.coordinates[:, particle] .= u[:, particle] system.initial_condition.velocity[:, particle] .= v[1:ndims(system), particle] end @@ -423,7 +423,7 @@ function restart_with!(system, ::SummationDensity, v, u) end function restart_with!(system, ::ContinuityDensity, v, u) - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) system.initial_condition.density[particle] = v[end, particle] end diff --git a/src/schemes/structure/discrete_element_method/rhs.jl b/src/schemes/structure/discrete_element_method/rhs.jl index 969fe720bb..77e548cf7d 100644 --- a/src/schemes/structure/discrete_element_method/rhs.jl +++ b/src/schemes/structure/discrete_element_method/rhs.jl @@ -8,10 +8,10 @@ function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, foreach_point_neighbor(particle_system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(particle_system)) do particle, - neighbor, - pos_diff, - distance + points=each_integrated_particle(particle_system)) do particle, + neighbor, + pos_diff, + distance distance < sqrt(eps()) && return # Retrieve particle properties diff --git a/src/schemes/structure/total_lagrangian_sph/rhs.jl b/src/schemes/structure/total_lagrangian_sph/rhs.jl index a371abc6a1..434d2d93df 100644 --- a/src/schemes/structure/total_lagrangian_sph/rhs.jl +++ b/src/schemes/structure/total_lagrangian_sph/rhs.jl @@ -19,10 +19,10 @@ end # Loop over all pairs of particles and neighbors within the kernel cutoff. # For structure-structure interaction, this has to happen in the initial coordinates. foreach_point_neighbor(system, system, system_coords, system_coords, semi; - points=each_moving_particle(system)) do particle, neighbor, - initial_pos_diff, - initial_distance - # Only consider particles with a distance > 0. + points=each_integrated_particle(system)) do particle, neighbor, + initial_pos_diff, + initial_distance + # Only consider particles with a distance > 0 initial_distance < sqrt(eps()) && return rho_a = @inbounds system.material_density[particle] @@ -73,14 +73,14 @@ function interact!(dv, v_particle_system, u_particle_system, system_coords = current_coordinates(u_particle_system, particle_system) neighbor_coords = current_coordinates(u_neighbor_system, neighbor_system) - # Loop over all pairs of particles and neighbors within the kernel cutoff. + # Loop over all pairs of particles and neighbors within the kernel cutoff foreach_point_neighbor(particle_system, neighbor_system, system_coords, neighbor_coords, semi; - points=each_moving_particle(particle_system)) do particle, - neighbor, - pos_diff, - distance - # Only consider particles with a distance > 0. + points=each_integrated_particle(particle_system)) do particle, + neighbor, + pos_diff, + distance + # Only consider particles with a distance > 0 distance < sqrt(eps()) && return # Apply the same force to the structure particle diff --git a/src/schemes/structure/total_lagrangian_sph/system.jl b/src/schemes/structure/total_lagrangian_sph/system.jl index 95d42aecf0..10c64b0430 100644 --- a/src/schemes/structure/total_lagrangian_sph/system.jl +++ b/src/schemes/structure/total_lagrangian_sph/system.jl @@ -59,26 +59,26 @@ See [Total Lagrangian SPH](@ref tlsph) for more details on the method. struct TotalLagrangianSPHSystem{BM, NDIMS, ELTYPE <: Real, IC, ARRAY1D, ARRAY2D, ARRAY3D, YM, PR, LL, LM, K, PF, V, ST} <: AbstractStructureSystem{NDIMS} - initial_condition :: IC - initial_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] - current_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] - mass :: ARRAY1D # Array{ELTYPE, 1}: [particle] - correction_matrix :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] - pk1_corrected :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] - deformation_grad :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] - material_density :: ARRAY1D # Array{ELTYPE, 1}: [particle] - n_moving_particles :: Int64 - young_modulus :: YM - poisson_ratio :: PR - lame_lambda :: LL - lame_mu :: LM - smoothing_kernel :: K - smoothing_length :: ELTYPE - acceleration :: SVector{NDIMS, ELTYPE} - boundary_model :: BM - penalty_force :: PF - viscosity :: V - source_terms :: ST + initial_condition :: IC + initial_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] + current_coordinates :: ARRAY2D # Array{ELTYPE, 2}: [dimension, particle] + mass :: ARRAY1D # Array{ELTYPE, 1}: [particle] + correction_matrix :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] + pk1_corrected :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] + deformation_grad :: ARRAY3D # Array{ELTYPE, 3}: [i, j, particle] + material_density :: ARRAY1D # Array{ELTYPE, 1}: [particle] + n_integrated_particles :: Int64 + young_modulus :: YM + poisson_ratio :: PR + lame_lambda :: LL + lame_mu :: LM + smoothing_kernel :: K + smoothing_length :: ELTYPE + acceleration :: SVector{NDIMS, ELTYPE} + boundary_model :: BM + penalty_force :: PF + viscosity :: V + source_terms :: ST end function TotalLagrangianSPHSystem(initial_condition, @@ -111,7 +111,7 @@ function TotalLagrangianSPHSystem(initial_condition, pk1_corrected = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, n_particles) deformation_grad = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, n_particles) - n_moving_particles = n_particles - n_clamped_particles + n_integrated_particles = n_particles - n_clamped_particles lame_lambda = @. young_modulus * poisson_ratio / ((1 + poisson_ratio) * (1 - 2 * poisson_ratio)) @@ -120,7 +120,7 @@ function TotalLagrangianSPHSystem(initial_condition, return TotalLagrangianSPHSystem(initial_condition, initial_coordinates, current_coordinates, mass, correction_matrix, pk1_corrected, deformation_grad, material_density, - n_moving_particles, young_modulus, poisson_ratio, + n_integrated_particles, young_modulus, poisson_ratio, lame_lambda, lame_mu, smoothing_kernel, smoothing_length, acceleration_, boundary_model, penalty_force, viscosity, source_terms) @@ -154,7 +154,7 @@ function Base.show(io::IO, ::MIME"text/plain", system::TotalLagrangianSPHSystem) if get(io, :compact, false) show(io, system) else - n_clamped_particles = nparticles(system) - n_moving_particles(system) + n_clamped_particles = nparticles(system) - n_integrated_particles(system) summary_header(io, "TotalLagrangianSPHSystem{$(ndims(system))}") summary_line(io, "total #particles", nparticles(system)) @@ -182,8 +182,8 @@ end return ndims(system) + 1 end -@inline function n_moving_particles(system::TotalLagrangianSPHSystem) - system.n_moving_particles +@inline function n_integrated_particles(system::TotalLagrangianSPHSystem) + system.n_integrated_particles end @inline initial_coordinates(system::TotalLagrangianSPHSystem) = system.initial_coordinates @@ -200,7 +200,7 @@ end end @inline function current_velocity(v, system::TotalLagrangianSPHSystem, particle) - if particle > n_moving_particles(system) + if particle > n_integrated_particles(system) return zero(SVector{ndims(system), eltype(system)}) end @@ -279,7 +279,7 @@ function update_positions!(system::TotalLagrangianSPHSystem, v, u, v_ode, u_ode, # `current_coordinates` stores the coordinates of both integrated and clamped particles. # Copy the coordinates of the integrated particles from `u`. - @threaded semi for particle in each_moving_particle(system) + @threaded semi for particle in each_integrated_particle(system) for i in 1:ndims(system) current_coordinates[i, particle] = u[i, particle] end @@ -381,7 +381,7 @@ function write_u0!(u0, system::TotalLagrangianSPHSystem) (; initial_condition) = system # This is as fast as a loop with `@inbounds`, but it's GPU-compatible - indices = CartesianIndices((ndims(system), each_moving_particle(system))) + indices = CartesianIndices((ndims(system), each_integrated_particle(system))) copyto!(u0, indices, initial_condition.coordinates, indices) return u0 @@ -391,7 +391,7 @@ function write_v0!(v0, system::TotalLagrangianSPHSystem) (; initial_condition, boundary_model) = system # This is as fast as a loop with `@inbounds`, but it's GPU-compatible - indices = CartesianIndices((ndims(system), each_moving_particle(system))) + indices = CartesianIndices((ndims(system), each_integrated_particle(system))) copyto!(v0, indices, initial_condition.velocity, indices) write_v0!(v0, boundary_model, system) @@ -408,7 +408,7 @@ function write_v0!(v0, ::BoundaryModelDummyParticles{ContinuityDensity}, (; cache) = system.boundary_model (; initial_density) = cache - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) # Set particle densities v0[ndims(system) + 1, particle] = initial_density[particle] end @@ -417,7 +417,7 @@ function write_v0!(v0, ::BoundaryModelDummyParticles{ContinuityDensity}, end function restart_with!(system::TotalLagrangianSPHSystem, v, u) - for particle in each_moving_particle(system) + for particle in each_integrated_particle(system) system.current_coordinates[:, particle] .= u[:, particle] system.initial_condition.velocity[:, particle] .= v[1:ndims(system), particle] end @@ -435,7 +435,7 @@ function von_mises_stress(system) von_mises_stress_vector = zeros(eltype(system.pk1_corrected), nparticles(system)) @threaded default_backend(von_mises_stress_vector) for particle in - each_moving_particle(system) + each_integrated_particle(system) von_mises_stress_vector[particle] = von_mises_stress(system, particle) end @@ -469,7 +469,7 @@ function cauchy_stress(system::TotalLagrangianSPHSystem) nparticles(system)) @threaded default_backend(cauchy_stress_tensors) for particle in - each_moving_particle(system) + each_integrated_particle(system) F = deformation_gradient(system, particle) J = det(F) P = pk1_corrected(system, particle) diff --git a/test/general/buffer.jl b/test/general/buffer.jl index 854b219c79..fbfefc8656 100644 --- a/test/general/buffer.jl +++ b/test/general/buffer.jl @@ -18,9 +18,9 @@ n_particles = nparticles(system) @testset "Iterators" begin - @test TrixiParticles.each_moving_particle(system) == 1:n_particles + @test TrixiParticles.each_integrated_particle(system) == 1:n_particles - @test TrixiParticles.each_moving_particle(system_buffer) == 1:n_particles + @test TrixiParticles.each_integrated_particle(system_buffer) == 1:n_particles # Activate a particle particle_id = findfirst(==(false), system_buffer.buffer.active_particle) @@ -29,7 +29,7 @@ TrixiParticles.update_system_buffer!(system_buffer.buffer, DummySemidiscretization()) - @test TrixiParticles.each_moving_particle(system_buffer) == 1:(n_particles + 1) + @test TrixiParticles.each_integrated_particle(system_buffer) == 1:(n_particles + 1) TrixiParticles.deactivate_particle!(system_buffer, particle_id, ones(2, particle_id)) @@ -37,7 +37,7 @@ TrixiParticles.update_system_buffer!(system_buffer.buffer, DummySemidiscretization()) - @test TrixiParticles.each_moving_particle(system_buffer) == 1:n_particles + @test TrixiParticles.each_integrated_particle(system_buffer) == 1:n_particles particle_id = 5 TrixiParticles.deactivate_particle!(system_buffer, particle_id, @@ -46,7 +46,7 @@ TrixiParticles.update_system_buffer!(system_buffer.buffer, DummySemidiscretization()) - @test TrixiParticles.each_moving_particle(system_buffer) == + @test TrixiParticles.each_integrated_particle(system_buffer) == setdiff(1:n_particles, particle_id) end diff --git a/test/general/semidiscretization.jl b/test/general/semidiscretization.jl index 59646bdf30..bb2aa75c5f 100644 --- a/test/general/semidiscretization.jl +++ b/test/general/semidiscretization.jl @@ -13,8 +13,8 @@ TrixiParticles.v_nvariables(::System2) = 2 TrixiParticles.nparticles(::System1) = 2 TrixiParticles.nparticles(::System2) = 3 - TrixiParticles.n_moving_particles(::System1) = 2 - TrixiParticles.n_moving_particles(::System2) = 3 + TrixiParticles.n_integrated_particles(::System1) = 2 + TrixiParticles.n_integrated_particles(::System2) = 3 TrixiParticles.compact_support(::System1, neighbor) = 0.2 TrixiParticles.compact_support(::System2, neighbor) = 0.2 @@ -140,9 +140,6 @@ v2 = zeros(4 * 3) v_ode = vcat(vec(v1), v2) - # Avoid `SystemBuffer` barrier - TrixiParticles.each_moving_particle(system::Union{System1, System2}) = Base.OneTo(nparticles(system)) - TrixiParticles.add_source_terms!(dv_ode, v_ode, u_ode, semi, 0.0) dv1 = TrixiParticles.wrap_v(dv_ode, system1, semi) diff --git a/test/schemes/structure/total_lagrangian_sph/rhs.jl b/test/schemes/structure/total_lagrangian_sph/rhs.jl index 412d295fad..92fc18cccb 100644 --- a/test/schemes/structure/total_lagrangian_sph/rhs.jl +++ b/test/schemes/structure/total_lagrangian_sph/rhs.jl @@ -34,7 +34,7 @@ @testset verbose=true "Test $i" for i in 1:4 #### Setup - each_moving_particle = [particle[i]] # Only calculate dv for this one particle + each_integrated_particle = [particle[i]] # Only calculate dv for this one particle eachparticle = [particle[i], neighbor[i]] initial_coordinates = 1000 * ones(2, 10) # Just something that's not zero to catch errors initial_coordinates[:, particle[i]] = initial_coordinates_particle[i] @@ -84,7 +84,7 @@ end TrixiParticles.eachparticle(::MockSystem) = eachparticle - TrixiParticles.each_moving_particle(::MockSystem) = each_moving_particle + TrixiParticles.each_integrated_particle(::MockSystem) = each_integrated_particle function TrixiParticles.add_acceleration!(_, _, ::MockSystem) return nothing diff --git a/test/systems/edac_system.jl b/test/systems/edac_system.jl index 8f184fcd20..ac8a424853 100644 --- a/test/systems/edac_system.jl +++ b/test/systems/edac_system.jl @@ -165,7 +165,7 @@ smoothing_length, sound_speed) u0 = zeros(TrixiParticles.u_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_u0!(u0, system) @test u0 == coordinates @@ -191,7 +191,7 @@ smoothing_length, sound_speed) v0 = zeros(TrixiParticles.v_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_v0!(v0, system) system.cache.density .= density @@ -208,7 +208,7 @@ smoothing_length, sound_speed) v0 = zeros(TrixiParticles.v_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_v0!(v0, system) @test v0 == vcat(velocity, [0.8, 1.0]') diff --git a/test/systems/tlsph_system.jl b/test/systems/tlsph_system.jl index d201286d4d..f8c587e3fa 100644 --- a/test/systems/tlsph_system.jl +++ b/test/systems/tlsph_system.jl @@ -34,7 +34,7 @@ @test system.current_coordinates == coordinates @test system.mass == mass @test system.material_density == material_densities - @test system.n_moving_particles == 2 + @test system.n_integrated_particles == 2 @test system.young_modulus == E @test system.poisson_ratio == nu @test system.lame_lambda == 1.0 @@ -330,7 +330,7 @@ boundary_model=boundary_model) u0 = zeros(TrixiParticles.u_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_u0!(u0, system) @test u0 == coordinates @@ -357,7 +357,7 @@ boundary_model=boundary_model) v0 = zeros(TrixiParticles.v_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_v0!(v0, system) @test v0 == velocity diff --git a/test/systems/wcsph_system.jl b/test/systems/wcsph_system.jl index 99f7152db6..7c034e7757 100644 --- a/test/systems/wcsph_system.jl +++ b/test/systems/wcsph_system.jl @@ -238,7 +238,7 @@ smoothing_length) u0 = zeros(TrixiParticles.u_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_u0!(u0, system) @test u0 == coordinates @@ -263,7 +263,7 @@ smoothing_length) v0 = zeros(TrixiParticles.v_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_v0!(v0, system) system.cache.density .= density @@ -281,7 +281,7 @@ smoothing_length) v0 = zeros(TrixiParticles.v_nvariables(system), - TrixiParticles.n_moving_particles(system)) + TrixiParticles.n_integrated_particles(system)) TrixiParticles.write_v0!(v0, system) @test v0 == vcat(velocity, density') From 5d4bfee06c50932f9b9117ba99e94e31bd3521e7 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:58:53 +0200 Subject: [PATCH 10/15] Restructure boundary files (#908) * Restructure boundary files * Fix doctests * Reformat --- docs/src/systems/boundary.md | 5 +- src/schemes/boundary/boundary.jl | 14 +- src/schemes/boundary/dem_boundary/system.jl | 92 +++++++ .../boundary/open_boundary/open_boundary.jl | 4 + src/schemes/boundary/prescribed_motion.jl | 76 ++++++ .../dummy_particles.jl | 2 + .../monaghan_kajtar.jl | 0 .../boundary/{ => wall_boundary}/rhs.jl | 0 .../boundary/{ => wall_boundary}/system.jl | 244 ++++-------------- .../boundary/wall_boundary/wall_boundary.jl | 4 + src/schemes/schemes.jl | 4 +- 11 files changed, 234 insertions(+), 211 deletions(-) create mode 100644 src/schemes/boundary/dem_boundary/system.jl create mode 100644 src/schemes/boundary/open_boundary/open_boundary.jl create mode 100644 src/schemes/boundary/prescribed_motion.jl rename src/schemes/boundary/{dummy_particles => wall_boundary}/dummy_particles.jl (99%) rename src/schemes/boundary/{monaghan_kajtar => wall_boundary}/monaghan_kajtar.jl (100%) rename src/schemes/boundary/{ => wall_boundary}/rhs.jl (100%) rename src/schemes/boundary/{ => wall_boundary}/system.jl (65%) create mode 100644 src/schemes/boundary/wall_boundary/wall_boundary.jl diff --git a/docs/src/systems/boundary.md b/docs/src/systems/boundary.md index 7bd334d017..6c41262935 100644 --- a/docs/src/systems/boundary.md +++ b/docs/src/systems/boundary.md @@ -217,9 +217,8 @@ a no-slip condition is imposed. When omitting the viscous interaction !!! warning The no-slip conditions for `BoundaryModelMonaghanKajtar` have not been verified yet. -```@autodocs -Modules = [TrixiParticles] -Pages = [joinpath("schemes", "boundary", "monaghan_kajtar", "monaghan_kajtar.jl")] +```@docs + BoundaryModelMonaghanKajtar ``` # [Open Boundaries](@id open_boundary) diff --git a/src/schemes/boundary/boundary.jl b/src/schemes/boundary/boundary.jl index 2442dbec49..343c0d022a 100644 --- a/src/schemes/boundary/boundary.jl +++ b/src/schemes/boundary/boundary.jl @@ -1,10 +1,4 @@ -include("dummy_particles/dummy_particles.jl") -include("system.jl") -include("open_boundary/boundary_zones.jl") -include("open_boundary/mirroring.jl") -include("open_boundary/method_of_characteristics.jl") -include("open_boundary/system.jl") -# Monaghan-Kajtar repulsive boundary particles require the `WallBoundarySystem` -# and the `TotalLagrangianSPHSystem` and are therefore included later. - -@inline Base.ndims(boundary_model::BoundaryModelDummyParticles) = ndims(boundary_model.smoothing_kernel) +include("prescribed_motion.jl") +include("wall_boundary/wall_boundary.jl") +include("open_boundary/open_boundary.jl") +include("dem_boundary/system.jl") diff --git a/src/schemes/boundary/dem_boundary/system.jl b/src/schemes/boundary/dem_boundary/system.jl new file mode 100644 index 0000000000..d288a081cf --- /dev/null +++ b/src/schemes/boundary/dem_boundary/system.jl @@ -0,0 +1,92 @@ +""" + BoundaryDEMSystem(initial_condition, normal_stiffness) + +System for boundaries modeled by boundary particles. +The interaction between fluid and boundary particles is specified by the boundary model. + +!!! warning "Experimental Implementation" + This is an experimental feature and may change in a future releases. + +""" +struct BoundaryDEMSystem{NDIMS, ELTYPE <: Real, IC, + ARRAY1D, ARRAY2D} <: AbstractBoundarySystem{NDIMS} + initial_condition :: IC + coordinates :: ARRAY2D # [dimension, particle] + radius :: ARRAY1D # [particle] + normal_stiffness :: ELTYPE + buffer :: Nothing + + function BoundaryDEMSystem(initial_condition, normal_stiffness) + coordinates = initial_condition.coordinates + radius = 0.5 * initial_condition.particle_spacing * + ones(length(initial_condition.mass)) + NDIMS = size(coordinates, 1) + + return new{NDIMS, eltype(coordinates), typeof(initial_condition), typeof(radius), + typeof(coordinates)}(initial_condition, coordinates, radius, + normal_stiffness, nothing) + end +end + +@inline function Base.eltype(system::BoundaryDEMSystem) + eltype(system.coordinates) +end + +@inline function nparticles(system::BoundaryDEMSystem) + size(system.coordinates, 2) +end + +# No particle positions are advanced for DEM boundary systems +@inline function n_integrated_particles(system::BoundaryDEMSystem) + return 0 +end + +@inline v_nvariables(system::BoundaryDEMSystem) = 0 +@inline u_nvariables(system::BoundaryDEMSystem) = 0 + +@inline initial_coordinates(system::BoundaryDEMSystem) = system.coordinates + +@inline function current_coordinates(u, system::BoundaryDEMSystem) + return system.coordinates +end + +@inline function current_velocity(v, system::BoundaryDEMSystem, particle) + return zero(SVector{ndims(system), eltype(system)}) +end + +function write_u0!(u0, ::BoundaryDEMSystem) + return u0 +end + +function write_v0!(v0, ::BoundaryDEMSystem) + return v0 +end + +function system_data(system::BoundaryDEMSystem, dv_ode, du_ode, v_ode, u_ode, semi) + (; coordinates, radius, normal_stiffness) = system + + return (; coordinates, radius, normal_stiffness) +end + +function available_data(::BoundaryDEMSystem) + return (:coordinates, :radius, :normal_stiffness) +end + +function Base.show(io::IO, system::BoundaryDEMSystem) + @nospecialize system # reduce precompilation time + + print(io, "BoundaryDEMSystem{", ndims(system), "}(") + print(io, ") with ", nparticles(system), " particles") +end + +function Base.show(io::IO, ::MIME"text/plain", system::BoundaryDEMSystem) + @nospecialize system # reduce precompilation time + + if get(io, :compact, false) + show(io, system) + else + summary_header(io, "BoundaryDEMSystem{$(ndims(system))}") + summary_line(io, "#particles", nparticles(system)) + summary_footer(io) + end +end diff --git a/src/schemes/boundary/open_boundary/open_boundary.jl b/src/schemes/boundary/open_boundary/open_boundary.jl new file mode 100644 index 0000000000..de691fd580 --- /dev/null +++ b/src/schemes/boundary/open_boundary/open_boundary.jl @@ -0,0 +1,4 @@ +include("boundary_zones.jl") +include("mirroring.jl") +include("method_of_characteristics.jl") +include("system.jl") diff --git a/src/schemes/boundary/prescribed_motion.jl b/src/schemes/boundary/prescribed_motion.jl new file mode 100644 index 0000000000..2e742176e0 --- /dev/null +++ b/src/schemes/boundary/prescribed_motion.jl @@ -0,0 +1,76 @@ +""" + PrescribedMotion(movement_function, is_moving; moving_particles=nothing) + +# Arguments +- `movement_function`: Time-dependent function returning an `SVector` of ``d`` dimensions + for a ``d``-dimensional problem. +- `is_moving`: Function to determine in each timestep if the particles are moving or not. Its + boolean return value is mandatory to determine if the neighborhood search will be updated. + +# Keyword Arguments +- `moving_particles`: Indices of moving particles. Default is each particle in the system. + +# Examples +In the example below, `motion` describes particles moving in a circle as long as +the time is lower than `1.5`. + +```jldoctest; output = false +movement_function(t) = SVector(cos(2pi*t), sin(2pi*t)) +is_moving(t) = t < 1.5 + +motion = PrescribedMotion(movement_function, is_moving) + +# output +PrescribedMotion{typeof(movement_function), typeof(is_moving), Vector{Int64}}(movement_function, is_moving, Int64[]) +``` +""" +struct PrescribedMotion{MF, IM, MP} + movement_function :: MF + is_moving :: IM + moving_particles :: MP # Vector{Int} +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 PrescribedMotion(movement_function, is_moving; moving_particles=nothing) + if !(movement_function(0.0) isa SVector) + @warn "Return value of `movement_function` is not of type `SVector`. " * + "Returning regular `Vector`s causes allocations and significant performance overhead." + end + + # Default value is an empty vector, which will be resized in the `WallBoundarySystem` + # constructor to move all particles. + moving_particles = isnothing(moving_particles) ? Int[] : vec(moving_particles) + + return PrescribedMotion(movement_function, is_moving, moving_particles) +end + +function (prescribed_motion::PrescribedMotion)(system, t, semi) + (; coordinates, cache) = system + (; movement_function, is_moving, moving_particles) = prescribed_motion + (; acceleration, velocity) = cache + + system.ismoving[] = is_moving(t) + + is_moving(t) || return system + + @threaded semi for particle in moving_particles + pos_new = initial_coords(system, particle) + movement_function(t) + vel = ForwardDiff.derivative(movement_function, t) + acc = ForwardDiff.derivative(t_ -> ForwardDiff.derivative(movement_function, t_), t) + + @inbounds for i in 1:ndims(system) + coordinates[i, particle] = pos_new[i] + velocity[i, particle] = vel[i] + acceleration[i, particle] = acc[i] + end + end + + return system +end + +function (prescribed_motion::Nothing)(system::AbstractSystem, t, semi) + system.ismoving[] = false + + return system +end diff --git a/src/schemes/boundary/dummy_particles/dummy_particles.jl b/src/schemes/boundary/wall_boundary/dummy_particles.jl similarity index 99% rename from src/schemes/boundary/dummy_particles/dummy_particles.jl rename to src/schemes/boundary/wall_boundary/dummy_particles.jl index 906c88c520..1ed34e8883 100644 --- a/src/schemes/boundary/dummy_particles/dummy_particles.jl +++ b/src/schemes/boundary/wall_boundary/dummy_particles.jl @@ -84,6 +84,8 @@ function BoundaryModelDummyParticles(initial_density, hydrodynamic_mass, smoothing_length, viscosity, correction, cache) end +@inline Base.ndims(boundary_model::BoundaryModelDummyParticles) = ndims(boundary_model.smoothing_kernel) + @doc raw""" AdamiPressureExtrapolation(; pressure_offset=0, allow_loop_flipping=true) diff --git a/src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl b/src/schemes/boundary/wall_boundary/monaghan_kajtar.jl similarity index 100% rename from src/schemes/boundary/monaghan_kajtar/monaghan_kajtar.jl rename to src/schemes/boundary/wall_boundary/monaghan_kajtar.jl diff --git a/src/schemes/boundary/rhs.jl b/src/schemes/boundary/wall_boundary/rhs.jl similarity index 100% rename from src/schemes/boundary/rhs.jl rename to src/schemes/boundary/wall_boundary/rhs.jl diff --git a/src/schemes/boundary/system.jl b/src/schemes/boundary/wall_boundary/system.jl similarity index 65% rename from src/schemes/boundary/system.jl rename to src/schemes/boundary/wall_boundary/system.jl index ffde524df5..7d9421642f 100644 --- a/src/schemes/boundary/system.jl +++ b/src/schemes/boundary/wall_boundary/system.jl @@ -58,197 +58,27 @@ function WallBoundarySystem(initial_condition, model; prescribed_motion=nothing, ismoving, adhesion_coefficient, cache) end -function Base.show(io::IO, system::WallBoundarySystem) - @nospecialize system # reduce precompilation time - - print(io, "WallBoundarySystem{", ndims(system), "}(") - print(io, system.boundary_model) - print(io, ", ", system.prescribed_motion) - print(io, ", ", system.adhesion_coefficient) - print(io, ", ", system.cache.color) - print(io, ") with ", nparticles(system), " particles") -end - -function Base.show(io::IO, ::MIME"text/plain", system::WallBoundarySystem) - @nospecialize system # reduce precompilation time - - if get(io, :compact, false) - show(io, system) - else - summary_header(io, "WallBoundarySystem{$(ndims(system))}") - summary_line(io, "#particles", nparticles(system)) - summary_line(io, "boundary model", system.boundary_model) - summary_line(io, "movement function", - isnothing(system.prescribed_motion) ? "nothing" : - string(system.prescribed_motion.movement_function)) - summary_line(io, "adhesion coefficient", system.adhesion_coefficient) - summary_line(io, "color", system.cache.color) - summary_footer(io) - end -end - -""" - BoundaryDEMSystem(initial_condition, normal_stiffness) - -System for boundaries modeled by boundary particles. -The interaction between fluid and boundary particles is specified by the boundary model. - -!!! warning "Experimental Implementation" - This is an experimental feature and may change in a future releases. - -""" -struct BoundaryDEMSystem{NDIMS, ELTYPE <: Real, IC, - ARRAY1D, ARRAY2D} <: AbstractBoundarySystem{NDIMS} - initial_condition :: IC - coordinates :: ARRAY2D # [dimension, particle] - radius :: ARRAY1D # [particle] - normal_stiffness :: ELTYPE - buffer :: Nothing - - function BoundaryDEMSystem(initial_condition, normal_stiffness) - coordinates = initial_condition.coordinates - radius = 0.5 * initial_condition.particle_spacing * - ones(length(initial_condition.mass)) - NDIMS = size(coordinates, 1) - - return new{NDIMS, eltype(coordinates), typeof(initial_condition), typeof(radius), - typeof(coordinates)}(initial_condition, coordinates, radius, - normal_stiffness, nothing) - end -end - -function Base.show(io::IO, system::BoundaryDEMSystem) - @nospecialize system # reduce precompilation time - - print(io, "BoundaryDEMSystem{", ndims(system), "}(") - print(io, ") with ", nparticles(system), " particles") -end - -function Base.show(io::IO, ::MIME"text/plain", system::BoundaryDEMSystem) - @nospecialize system # reduce precompilation time - - if get(io, :compact, false) - show(io, system) - else - summary_header(io, "BoundaryDEMSystem{$(ndims(system))}") - summary_line(io, "#particles", nparticles(system)) - summary_footer(io) - end -end - -""" - PrescribedMotion(movement_function, is_moving; moving_particles=nothing) - -# Arguments -- `movement_function`: Time-dependent function returning an `SVector` of ``d`` dimensions - for a ``d``-dimensional problem. -- `is_moving`: Function to determine in each timestep if the particles are moving or not. Its - boolean return value is mandatory to determine if the neighborhood search will be updated. - -# Keyword Arguments -- `moving_particles`: Indices of moving particles. Default is each particle in the system. - -In the example below, `motion` describes particles moving in a circle as long as -the time is lower than `1.5`. - -# Examples -```jldoctest; output = false -movement_function(t) = SVector(cos(2pi*t), sin(2pi*t)) -is_moving(t) = t < 1.5 - -motion = PrescribedMotion(movement_function, is_moving) - -# output -PrescribedMotion{typeof(movement_function), typeof(is_moving), Vector{Int64}}(movement_function, is_moving, Int64[]) -``` -""" -struct PrescribedMotion{MF, IM, MP} - movement_function :: MF - is_moving :: IM - moving_particles :: MP # Vector{Int} -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 PrescribedMotion(movement_function, is_moving; moving_particles=nothing) - if !(movement_function(0.0) isa SVector) - @warn "Return value of `movement_function` is not of type `SVector`. " * - "Returning regular `Vector`s causes allocations and significant performance overhead." - end - - # Default value is an empty vector, which will be resized in the `WallBoundarySystem` - # constructor to move all particles. - moving_particles = isnothing(moving_particles) ? Int[] : vec(moving_particles) - - return PrescribedMotion(movement_function, is_moving, moving_particles) -end - create_cache_boundary(::Nothing, initial_condition) = (;) function create_cache_boundary(::PrescribedMotion, initial_condition) initial_coordinates = copy(initial_condition.coordinates) velocity = zero(initial_condition.velocity) acceleration = zero(initial_condition.velocity) + return (; velocity, acceleration, initial_coordinates) end -timer_name(::Union{WallBoundarySystem, BoundaryDEMSystem}) = "boundary" - -@inline function Base.eltype(system::Union{WallBoundarySystem, BoundaryDEMSystem}) +@inline function Base.eltype(system::WallBoundarySystem) eltype(system.coordinates) end -@inline function initial_coordinates(system::WallBoundarySystem) - initial_coordinates(system::WallBoundarySystem, system.prescribed_motion) -end - -@inline initial_coordinates(system::BoundaryDEMSystem) = system.coordinates - -@inline initial_coordinates(system::WallBoundarySystem, ::Nothing) = system.coordinates - -# We need static initial coordinates as reference when system is moving -@inline function initial_coordinates(system::WallBoundarySystem, prescribed_motion) - return system.cache.initial_coordinates -end - -function (prescribed_motion::PrescribedMotion)(system, t, semi) - (; coordinates, cache) = system - (; movement_function, is_moving, moving_particles) = prescribed_motion - (; acceleration, velocity) = cache - - system.ismoving[] = is_moving(t) - - is_moving(t) || return system - - @threaded semi for particle in moving_particles - pos_new = initial_coords(system, particle) + movement_function(t) - vel = ForwardDiff.derivative(movement_function, t) - acc = ForwardDiff.derivative(t_ -> ForwardDiff.derivative(movement_function, t_), t) - - @inbounds for i in 1:ndims(system) - coordinates[i, particle] = pos_new[i] - velocity[i, particle] = vel[i] - acceleration[i, particle] = acc[i] - end - end - - return system -end - -function (prescribed_motion::Nothing)(system::AbstractSystem, t, semi) - system.ismoving[] = false - - return system -end - -@inline function nparticles(system::Union{BoundaryDEMSystem, WallBoundarySystem}) +@inline function nparticles(system::WallBoundarySystem) size(system.coordinates, 2) end -# No particle positions are advanced for boundary systems, +# No particle positions are advanced for wall boundary systems, # except when using `BoundaryModelDummyParticles` with `ContinuityDensity`. -@inline function n_integrated_particles(system::Union{WallBoundarySystem, - BoundaryDEMSystem}) +@inline function n_integrated_particles(system::WallBoundarySystem) return 0 end @@ -256,20 +86,25 @@ end return nparticles(system) end -@inline u_nvariables(system::Union{WallBoundarySystem, BoundaryDEMSystem}) = 0 +@inline u_nvariables(system::WallBoundarySystem) = 0 # For BoundaryModelDummyParticles with ContinuityDensity, this needs to be 1. # For all other models and density calculators, it's irrelevant. @inline v_nvariables(system::WallBoundarySystem) = 1 -@inline v_nvariables(system::BoundaryDEMSystem) = 0 -@inline function current_coordinates(u, - system::Union{WallBoundarySystem, BoundaryDEMSystem}) - return system.coordinates +@inline function initial_coordinates(system::WallBoundarySystem) + initial_coordinates(system::WallBoundarySystem, system.prescribed_motion) end -@inline function current_velocity(v, system::BoundaryDEMSystem, particle) - return zero(SVector{ndims(system), eltype(system)}) +@inline initial_coordinates(system::WallBoundarySystem, ::Nothing) = system.coordinates + +# We need static initial coordinates as reference when system is moving +@inline function initial_coordinates(system::WallBoundarySystem, prescribed_motion) + return system.cache.initial_coordinates +end + +@inline function current_coordinates(u, system::WallBoundarySystem) + return system.coordinates end @inline function current_velocity(v, system::WallBoundarySystem, particle) @@ -298,7 +133,7 @@ end return current_acceleration(system, system.prescribed_motion, particle) end -@inline function current_acceleration(system, movement, particle) +@inline function current_acceleration(system, ::PrescribedMotion, particle) (; cache, ismoving) = system if ismoving[] @@ -308,7 +143,7 @@ end return zero(SVector{ndims(system), eltype(system)}) end -@inline function current_acceleration(system, movement::Nothing, particle) +@inline function current_acceleration(system, ::Nothing, particle) return zero(SVector{ndims(system), eltype(system)}) end @@ -316,11 +151,11 @@ end return viscous_velocity(v, system.boundary_model.viscosity, system, particle) end -@inline function viscous_velocity(v, viscosity, system, particle) +@inline function viscous_velocity(v, viscosity, system::WallBoundarySystem, particle) return extract_svector(system.boundary_model.cache.wall_velocity, system, particle) end -@inline function viscous_velocity(v, viscosity::Nothing, system, particle) +@inline function viscous_velocity(v, ::Nothing, system::WallBoundarySystem, particle) return current_velocity(v, system, particle) end @@ -372,13 +207,11 @@ function update_boundary_interpolation!(system::WallBoundarySystem, v, u, v_ode, return system end -function write_u0!(u0, system::Union{WallBoundarySystem, BoundaryDEMSystem}) +function write_u0!(u0, ::WallBoundarySystem) return u0 end -function write_v0!(v0, - system::Union{WallBoundarySystem, - BoundaryDEMSystem}) +function write_v0!(v0, ::WallBoundarySystem) return v0 end @@ -407,7 +240,7 @@ function restart_with!(system::WallBoundarySystem{<:BoundaryModelDummyParticles{ return system end -# To incorporate the effect at boundaries in the viscosity term of the RHS the neighbor +# To incorporate the effect at boundaries in the viscosity term of the RHS, the neighbor # viscosity model has to be used. @inline function viscosity_model(system::WallBoundarySystem, neighbor_system::AbstractFluidSystem) @@ -472,12 +305,31 @@ function available_data(::WallBoundarySystem) return (:coordinates, :velocity, :density, :pressure, :acceleration) end -function system_data(system::BoundaryDEMSystem, dv_ode, du_ode, v_ode, u_ode, semi) - (; coordinates, radius, normal_stiffness) = system +function Base.show(io::IO, system::WallBoundarySystem) + @nospecialize system # reduce precompilation time - return (; coordinates, radius, normal_stiffness) + print(io, "WallBoundarySystem{", ndims(system), "}(") + print(io, system.boundary_model) + print(io, ", ", system.prescribed_motion) + print(io, ", ", system.adhesion_coefficient) + print(io, ", ", system.cache.color) + print(io, ") with ", nparticles(system), " particles") end -function available_data(::BoundaryDEMSystem) - return (:coordinates, :radius, :normal_stiffness) +function Base.show(io::IO, ::MIME"text/plain", system::WallBoundarySystem) + @nospecialize system # reduce precompilation time + + if get(io, :compact, false) + show(io, system) + else + summary_header(io, "WallBoundarySystem{$(ndims(system))}") + summary_line(io, "#particles", nparticles(system)) + summary_line(io, "boundary model", system.boundary_model) + summary_line(io, "movement function", + isnothing(system.prescribed_motion) ? "nothing" : + string(system.prescribed_motion.movement_function)) + summary_line(io, "adhesion coefficient", system.adhesion_coefficient) + summary_line(io, "color", system.cache.color) + summary_footer(io) + end end diff --git a/src/schemes/boundary/wall_boundary/wall_boundary.jl b/src/schemes/boundary/wall_boundary/wall_boundary.jl new file mode 100644 index 0000000000..f20a5dbf20 --- /dev/null +++ b/src/schemes/boundary/wall_boundary/wall_boundary.jl @@ -0,0 +1,4 @@ +include("dummy_particles.jl") +include("system.jl") +# Monaghan-Kajtar repulsive boundary particles require the `WallBoundarySystem` +# and the `TotalLagrangianSPHSystem` and are therefore included later. diff --git a/src/schemes/schemes.jl b/src/schemes/schemes.jl index 8c9bf320c8..c181d0ed24 100644 --- a/src/schemes/schemes.jl +++ b/src/schemes/schemes.jl @@ -6,11 +6,11 @@ include("structure/total_lagrangian_sph/total_lagrangian_sph.jl") include("structure/discrete_element_method/discrete_element_method.jl") # Monaghan-Kajtar repulsive boundary particles require the `WallBoundarySystem` # and the `TotalLagrangianSPHSystem`. -include("boundary/monaghan_kajtar/monaghan_kajtar.jl") +include("boundary/wall_boundary/monaghan_kajtar.jl") # Include rhs for all schemes include("fluid/weakly_compressible_sph/rhs.jl") include("fluid/entropically_damped_sph/rhs.jl") -include("boundary/rhs.jl") +include("boundary/wall_boundary/rhs.jl") include("structure/total_lagrangian_sph/rhs.jl") include("structure/discrete_element_method/rhs.jl") From 9ba634a1197e7dc8ee1a9670090d150ffbc77759 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:05:29 +0200 Subject: [PATCH 11/15] Make `PrescribedMotion` more powerful by passing the initial position (#909) * Make `PrescribedMotion` more powerful by passing the initial position * Update NEWS.md * Move `SVector` warning to `WallBoundarySystem` constructor * Extract `initialize!` function * Move constructor of `PrescribedMotion` further up * Update prescribed_motion.jl Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> --------- Co-authored-by: Niklas Neher <73897120+LasNikas@users.noreply.github.com> --- NEWS.md | 8 ++- examples/fluid/accelerated_tank_2d.jl | 2 +- examples/fluid/lid_driven_cavity_2d.jl | 2 +- examples/fluid/moving_wall_2d.jl | 2 +- examples/fsi/dam_break_gate_2d.jl | 2 +- src/schemes/boundary/prescribed_motion.jl | 58 ++++++++++++++------ src/schemes/boundary/wall_boundary/system.jl | 10 +--- test/systems/boundary_system.jl | 6 +- 8 files changed, 55 insertions(+), 35 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3c8a2941be..da99ff8101 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,7 +13,10 @@ used in the Julia ecosystem. Notable changes will be documented in this file for - Renamed `OpenBoundarySPHSystem` to `OpenBoundarySystem`. -- Renamed `BoundaryMovement` to `PrescribedMotion`. +- Renamed `BoundaryMovement` to `PrescribedMotion`. The `movement_function` must now be + a function of `(x, t)` returning the *new position* instead of an offset. + For example, `movement_function(t) = SVector(t, 0.0)` now needs to be + `movement_function(x, t) = x + SVector(t, 0.0)`. - Renamed directory `solid` to `structure` in the examples file tree. VTK files for the `TotalLagrangianSPHSystem` are now also called `structure_*`. @@ -67,7 +70,7 @@ used in the Julia ecosystem. Notable changes will be documented in this file for - Fix the coordinates used for TLSPH in Adami extrapolation (#853) - Fix PST for small smoothing length factors (#834) -- The TVF model has been improved to integrate correctly with time stepping (#864) +- The TVF model has been improved to integrate correctly with time stepping (#864) ## Version 0.3.1 @@ -277,4 +280,3 @@ Features: #### TLSPH An implementation of TLSPH (Total Lagrangian Smoothed Particle Hydrodynamics) for solid bodies enabling FSI (Fluid Structure Interactions). - diff --git a/examples/fluid/accelerated_tank_2d.jl b/examples/fluid/accelerated_tank_2d.jl index 8d975656fa..cb6fbfb9d1 100644 --- a/examples/fluid/accelerated_tank_2d.jl +++ b/examples/fluid/accelerated_tank_2d.jl @@ -12,7 +12,7 @@ using OrdinaryDiffEq fluid_particle_spacing = 0.05 # Function for moving boundaries -movement_function(t) = SVector(0.0, 0.5 * 9.81 * t^2) +movement_function(x, t) = x + SVector(0.0, 0.5 * 9.81 * t^2) is_moving(t) = true diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl index 5429b2a87a..9849ca06f7 100644 --- a/examples/fluid/lid_driven_cavity_2d.jl +++ b/examples/fluid/lid_driven_cavity_2d.jl @@ -79,7 +79,7 @@ end # ========================================================================================== # ==== Boundary -lid_movement_function(t) = SVector(VELOCITY_LID * t, 0.0) +lid_movement_function(x, t) = x + SVector(VELOCITY_LID * t, 0.0) is_moving(t) = true diff --git a/examples/fluid/moving_wall_2d.jl b/examples/fluid/moving_wall_2d.jl index d67ce3efc3..85663188ea 100644 --- a/examples/fluid/moving_wall_2d.jl +++ b/examples/fluid/moving_wall_2d.jl @@ -36,7 +36,7 @@ tank = RectangularTank(fluid_particle_spacing, initial_fluid_size, tank_size, fl reset_wall!(tank, (false, true, false, false), (0.0, tank.fluid_size[1], 0.0, 0.0)) # Movement function -movement_function(t) = SVector(0.5t^2, 0.0) +movement_function(x, t) = x + SVector(0.5 * t^2, 0.0) is_moving(t) = t < 1.5 diff --git a/examples/fsi/dam_break_gate_2d.jl b/examples/fsi/dam_break_gate_2d.jl index b78b134810..015232adf9 100644 --- a/examples/fsi/dam_break_gate_2d.jl +++ b/examples/fsi/dam_break_gate_2d.jl @@ -60,7 +60,7 @@ gate = RectangularShape(boundary_particle_spacing, (initial_fluid_size[1], 0.0), density=fluid_density) # Movement of the gate according to the paper -movement_function(t) = SVector(0.0, -285.115t^3 + 72.305t^2 + 0.1463t) +movement_function(x, t) = x + SVector(0.0, -285.115 * t^3 + 72.305 * t^2 + 0.1463 * t) is_moving(t) = t < 0.1 gate_movement = PrescribedMotion(movement_function, is_moving) diff --git a/src/schemes/boundary/prescribed_motion.jl b/src/schemes/boundary/prescribed_motion.jl index 2e742176e0..609fed8387 100644 --- a/src/schemes/boundary/prescribed_motion.jl +++ b/src/schemes/boundary/prescribed_motion.jl @@ -2,26 +2,34 @@ PrescribedMotion(movement_function, is_moving; moving_particles=nothing) # Arguments -- `movement_function`: Time-dependent function returning an `SVector` of ``d`` dimensions - for a ``d``-dimensional problem. -- `is_moving`: Function to determine in each timestep if the particles are moving or not. Its - boolean return value is mandatory to determine if the neighborhood search will be updated. +- `movement_function`: Function of `(x, t)` where `x` is an `SVector` of the *initial* + particle position and `t` is the time, returning an `SVector` + of ``d`` dimensions for a ``d``-dimensional problem containing + the new particle position at time `t`. +- `is_moving`: Function of `t` to determine in each timestep if the particles + are moving or not. Its boolean return value determines + if the neighborhood search will be updated. # Keyword Arguments - `moving_particles`: Indices of moving particles. Default is each particle in the system. # Examples -In the example below, `motion` describes particles moving in a circle as long as -the time is lower than `1.5`. - ```jldoctest; output = false -movement_function(t) = SVector(cos(2pi*t), sin(2pi*t)) +# Circular motion of particles for t < 1.5 +movement_function(x, t) = x + SVector(cos(2pi * t), sin(2pi * t)) is_moving(t) = t < 1.5 motion = PrescribedMotion(movement_function, is_moving) +# Rotation around the origin +movement_function2(x, t) = SVector(cos(2pi * t) * x[1] - sin(2pi * t) * x[2], + sin(2pi * t) * x[1] + cos(2pi * t) * x[2]) +is_moving2(t) = true + +motion2 = PrescribedMotion(movement_function2, is_moving2) + # output -PrescribedMotion{typeof(movement_function), typeof(is_moving), Vector{Int64}}(movement_function, is_moving, Int64[]) +PrescribedMotion{typeof(movement_function2), typeof(is_moving2), Vector{Int64}}(movement_function2, is_moving2, Int64[]) ``` """ struct PrescribedMotion{MF, IM, MP} @@ -33,11 +41,6 @@ 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 PrescribedMotion(movement_function, is_moving; moving_particles=nothing) - if !(movement_function(0.0) isa SVector) - @warn "Return value of `movement_function` is not of type `SVector`. " * - "Returning regular `Vector`s causes allocations and significant performance overhead." - end - # Default value is an empty vector, which will be resized in the `WallBoundarySystem` # constructor to move all particles. moving_particles = isnothing(moving_particles) ? Int[] : vec(moving_particles) @@ -45,6 +48,24 @@ function PrescribedMotion(movement_function, is_moving; moving_particles=nothing return PrescribedMotion(movement_function, is_moving, moving_particles) end +function initialize!(prescribed_motion::PrescribedMotion, initial_condition) + # Test `movement_function` return type + pos = extract_svector(initial_condition.coordinates, + Val(size(initial_condition.coordinates, 1)), 1) + if !(prescribed_motion.movement_function(pos, 0.0) isa SVector) + @warn "Return value of `movement_function` is not of type `SVector`. " * + "Returning regular `Vector`s causes allocations and significant performance overhead." + end + + # Empty `moving_particles` means all particles are moving + if isempty(prescribed_motion.moving_particles) + # Default is an empty vector, since the number of particles is not known when + # instantiating `PrescribedMotion`. + resize!(prescribed_motion.moving_particles, nparticles(initial_condition)) + prescribed_motion.moving_particles .= collect(1:nparticles(initial_condition)) + end +end + function (prescribed_motion::PrescribedMotion)(system, t, semi) (; coordinates, cache) = system (; movement_function, is_moving, moving_particles) = prescribed_motion @@ -55,9 +76,12 @@ function (prescribed_motion::PrescribedMotion)(system, t, semi) is_moving(t) || return system @threaded semi for particle in moving_particles - pos_new = initial_coords(system, particle) + movement_function(t) - vel = ForwardDiff.derivative(movement_function, t) - acc = ForwardDiff.derivative(t_ -> ForwardDiff.derivative(movement_function, t_), t) + pos_original = initial_coords(system, particle) + pos_new = movement_function(pos_original, t) + pos_deriv(t_) = ForwardDiff.derivative(t__ -> movement_function(pos_original, t__), + t_) + vel = pos_deriv(t) + acc = ForwardDiff.derivative(pos_deriv, t) @inbounds for i in 1:ndims(system) coordinates[i, particle] = pos_new[i] diff --git a/src/schemes/boundary/wall_boundary/system.jl b/src/schemes/boundary/wall_boundary/system.jl index 7d9421642f..17466c4e60 100644 --- a/src/schemes/boundary/wall_boundary/system.jl +++ b/src/schemes/boundary/wall_boundary/system.jl @@ -42,17 +42,11 @@ function WallBoundarySystem(initial_condition, model; prescribed_motion=nothing, coordinates = copy(initial_condition.coordinates) ismoving = Ref(!isnothing(prescribed_motion)) + initialize!(prescribed_motion, initial_condition) cache = create_cache_boundary(prescribed_motion, initial_condition) cache = (cache..., color=Int(color_value)) - if prescribed_motion !== nothing && isempty(prescribed_motion.moving_particles) - # Default is an empty vector, since the number of particles is not known when - # instantiating `PrescribedMotion`. - resize!(prescribed_motion.moving_particles, nparticles(initial_condition)) - prescribed_motion.moving_particles .= collect(1:nparticles(initial_condition)) - end - # Because of dispatches boundary model needs to be first! return WallBoundarySystem(initial_condition, coordinates, model, prescribed_motion, ismoving, adhesion_coefficient, cache) @@ -60,7 +54,7 @@ end create_cache_boundary(::Nothing, initial_condition) = (;) -function create_cache_boundary(::PrescribedMotion, initial_condition) +function create_cache_boundary(prescribed_motion::PrescribedMotion, initial_condition) initial_coordinates = copy(initial_condition.coordinates) velocity = zero(initial_condition.velocity) acceleration = zero(initial_condition.velocity) diff --git a/test/systems/boundary_system.jl b/test/systems/boundary_system.jl index 7224e701a7..3318e310d6 100644 --- a/test/systems/boundary_system.jl +++ b/test/systems/boundary_system.jl @@ -36,12 +36,12 @@ initial_condition = InitialCondition(; coordinates, mass, density) model = (; hydrodynamic_mass=3) - function movement_function(t) + function movement_function(x, t) if NDIMS == 2 - return SVector(0.5 * t, 0.3 * t^2) + return x + SVector(0.5 * t, 0.3 * t^2) end - return SVector(0.5 * t, 0.3 * t^2, 0.1 * t^3) + return x + SVector(0.5 * t, 0.3 * t^2, 0.1 * t^3) end is_moving(t) = t < 1.0 From 875d2b905b797f21bd0b14e202b209103516b84e Mon Sep 17 00:00:00 2001 From: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:53:23 +0200 Subject: [PATCH 12/15] Make EDAC more consistent (#902) * make edac consistent with wcsph * fix tests * formatting * fix tests * fix * fix `atol` in dam break validation * implement suggestions --------- Co-authored-by: LasNikas --- .../fluid/entropically_damped_sph/rhs.jl | 79 ++++++++----------- .../fluid/entropically_damped_sph/system.jl | 22 +++--- src/schemes/fluid/fluid.jl | 36 +++++++++ .../density_diffusion.jl | 5 +- .../fluid/weakly_compressible_sph/rhs.jl | 37 --------- .../fluid/weakly_compressible_sph/system.jl | 2 + test/schemes/fluid/rhs.jl | 8 +- test/validation/validation.jl | 4 +- 8 files changed, 88 insertions(+), 105 deletions(-) diff --git a/src/schemes/fluid/entropically_damped_sph/rhs.jl b/src/schemes/fluid/entropically_damped_sph/rhs.jl index 9989769a2d..f3e44d927a 100644 --- a/src/schemes/fluid/entropically_damped_sph/rhs.jl +++ b/src/schemes/fluid/entropically_damped_sph/rhs.jl @@ -3,7 +3,7 @@ function interact!(dv, v_particle_system, u_particle_system, v_neighbor_system, u_neighbor_system, particle_system::EntropicallyDampedSPHSystem, neighbor_system, semi) - (; sound_speed, density_calculator, correction) = particle_system + (; sound_speed, density_calculator, correction, nu_edac) = particle_system system_coords = current_coordinates(u_particle_system, particle_system) neighbor_coords = current_coordinates(u_neighbor_system, neighbor_system) @@ -18,14 +18,14 @@ function interact!(dv, v_particle_system, u_particle_system, neighbor, pos_diff, distance - # Only consider particles with a distance > 0 - distance < sqrt(eps()) && return + # `foreach_point_neighbor` makes sure that `particle` and `neighbor` are + # in bounds of the respective system. For performance reasons, we use `@inbounds` + # in this hot loop to avoid bounds checking when extracting particle quantities. + rho_a = @inbounds current_density(v_particle_system, particle_system, particle) + rho_b = @inbounds current_density(v_neighbor_system, neighbor_system, neighbor) - rho_a = current_density(v_particle_system, particle_system, particle) - rho_b = current_density(v_neighbor_system, neighbor_system, neighbor) - - p_a = current_pressure(v_particle_system, particle_system, particle) - p_b = current_pressure(v_neighbor_system, neighbor_system, neighbor) + p_a = @inbounds current_pressure(v_particle_system, particle_system, particle) + p_b = @inbounds current_pressure(v_neighbor_system, neighbor_system, neighbor) # This technique by Basa et al. 2017 (10.1002/fld.1927) aims to reduce numerical # errors due to large pressures by subtracting the average pressure of neighboring @@ -33,10 +33,10 @@ function interact!(dv, v_particle_system, u_particle_system, # It results in significant improvement for EDAC, especially with TVF, # but not for WCSPH, according to Ramachandran & Puri (2019), Section 3.2. # Note that the return value is zero when not using average pressure reduction. - p_avg = average_pressure(particle_system, particle) + p_avg = @inbounds average_pressure(particle_system, particle) - m_a = hydrodynamic_mass(particle_system, particle) - m_b = hydrodynamic_mass(neighbor_system, neighbor) + m_a = @inbounds hydrodynamic_mass(particle_system, particle) + m_b = @inbounds hydrodynamic_mass(neighbor_system, neighbor) grad_kernel = smoothing_kernel_grad(particle_system, pos_diff, distance, particle) @@ -46,17 +46,18 @@ function interact!(dv, v_particle_system, u_particle_system, rho_b, pos_diff, distance, grad_kernel, correction) - dv_viscosity_ = dv_viscosity(particle_system, neighbor_system, - v_particle_system, v_neighbor_system, - particle, neighbor, pos_diff, distance, - sound_speed, m_a, m_b, rho_a, rho_b, grad_kernel) + dv_viscosity_ = @inbounds dv_viscosity(particle_system, neighbor_system, + v_particle_system, v_neighbor_system, + particle, neighbor, pos_diff, distance, + sound_speed, m_a, m_b, rho_a, rho_b, + grad_kernel) # Extra terms in the momentum equation when using a shifting technique - dv_tvf = dv_shifting(shifting_technique(particle_system), - particle_system, neighbor_system, - v_particle_system, v_neighbor_system, - particle, neighbor, m_a, m_b, rho_a, rho_b, - pos_diff, distance, grad_kernel, correction) + dv_tvf = @inbounds dv_shifting(shifting_technique(particle_system), + particle_system, neighbor_system, + v_particle_system, v_neighbor_system, + particle, neighbor, m_a, m_b, rho_a, rho_b, + pos_diff, distance, grad_kernel, correction) dv_surface_tension = surface_tension_force(surface_tension_a, surface_tension_b, particle_system, neighbor_system, @@ -77,10 +78,14 @@ function interact!(dv, v_particle_system, u_particle_system, pressure_evolution!(dv, particle_system, neighbor_system, v_diff, grad_kernel, particle, neighbor, pos_diff, distance, - sound_speed, m_a, m_b, p_a, p_b, rho_a, rho_b) - - continuity_equation!(dv, density_calculator, v_diff, particle, m_b, rho_a, rho_b, - particle_system, grad_kernel) + sound_speed, m_a, m_b, p_a, p_b, rho_a, rho_b, nu_edac) + + # TODO If variable smoothing_length is used, this should use the neighbor smoothing length + # Propagate `@inbounds` to the continuity equation, which accesses particle data + @inbounds continuity_equation!(dv, density_calculator, particle_system, + neighbor_system, v_particle_system, + v_neighbor_system, particle, neighbor, + pos_diff, distance, m_b, rho_a, rho_b, grad_kernel) end return dv @@ -89,7 +94,7 @@ end @inline function pressure_evolution!(dv, particle_system, neighbor_system, v_diff, grad_kernel, particle, neighbor, pos_diff, distance, sound_speed, m_a, m_b, - p_a, p_b, rho_a, rho_b) + p_a, p_b, rho_a, rho_b, nu_edac) volume_a = m_a / rho_a volume_b = m_b / rho_b volume_term = (volume_a^2 + volume_b^2) / m_a @@ -100,8 +105,8 @@ end # This is basically the continuity equation times `sound_speed^2` artificial_eos = m_b * rho_a / rho_b * sound_speed^2 * dot(v_diff, grad_kernel) - eta_a = rho_a * particle_system.nu_edac - eta_b = rho_b * particle_system.nu_edac + eta_a = rho_a * nu_edac + eta_b = rho_b * nu_edac eta_tilde = 2 * eta_a * eta_b / (eta_a + eta_b) smoothing_length_average = (smoothing_length(particle_system, particle) + @@ -121,24 +126,8 @@ end # This is similar to density diffusion in WCSPH damping_term = volume_term * tmp * pressure_diff * dot(grad_kernel, pos_diff) - dv[end, particle] += artificial_eos + damping_term - - return dv -end - -# We need a separate method for EDAC since the density is stored in `v[end-1,:]`. -@inline function continuity_equation!(dv, density_calculator::ContinuityDensity, - vdiff, particle, m_b, rho_a, rho_b, - particle_system::EntropicallyDampedSPHSystem, - grad_kernel) - dv[end - 1, particle] += rho_a / rho_b * m_b * dot(vdiff, grad_kernel) - - return dv -end + # Pressure is stored in `v` right after the velocity + dv[ndims(particle_system) + 1, particle] += artificial_eos + damping_term -@inline function continuity_equation!(dv, density_calculator, - vdiff, particle, m_b, rho_a, rho_b, - particle_system::EntropicallyDampedSPHSystem, - grad_kernel) return dv end diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl index a66577d87d..08518c48ad 100644 --- a/src/schemes/fluid/entropically_damped_sph/system.jl +++ b/src/schemes/fluid/entropically_damped_sph/system.jl @@ -250,10 +250,6 @@ end system_correction(system::EntropicallyDampedSPHSystem) = system.correction -@inline function current_pressure(v, system::EntropicallyDampedSPHSystem, particle) - return v[end, particle] -end - @inline function current_velocity(v, system::EntropicallyDampedSPHSystem) return view(v, 1:ndims(system), :) end @@ -264,11 +260,11 @@ end @inline shifting_technique(system::EntropicallyDampedSPHSystem) = system.shifting_technique -@inline function average_pressure(system::EntropicallyDampedSPHSystem, particle) +@propagate_inbounds function average_pressure(system::EntropicallyDampedSPHSystem, particle) average_pressure(system, system.average_pressure_reduction, particle) end -@inline function average_pressure(system, ::Val{true}, particle) +@propagate_inbounds function average_pressure(system, ::Val{true}, particle) return system.cache.pressure_average[particle] end @@ -286,12 +282,12 @@ end @inline function current_density(v, ::ContinuityDensity, system::EntropicallyDampedSPHSystem) - # When using `ContinuityDensity`, the density is stored in the second to last row of `v` - return view(v, size(v, 1) - 1, :) + # When using `ContinuityDensity`, the density is stored in the last row of `v` + return view(v, size(v, 1), :) end -@inline function current_pressure(v, ::EntropicallyDampedSPHSystem) - return view(v, size(v, 1), :) +@inline function current_pressure(v, system::EntropicallyDampedSPHSystem) + return view(v, ndims(system) + 1, :) end function update_quantities!(system::EntropicallyDampedSPHSystem, v, u, @@ -364,15 +360,15 @@ end function write_v0!(v0, system::EntropicallyDampedSPHSystem, ::SummationDensity) # Note that `.=` is very slightly faster, but not GPU-compatible - v0[end, :] = system.initial_condition.pressure + v0[ndims(system) + 1, :] = system.initial_condition.pressure return v0 end function write_v0!(v0, system::EntropicallyDampedSPHSystem, ::ContinuityDensity) # Note that `.=` is very slightly faster, but not GPU-compatible - v0[end - 1, :] = system.initial_condition.density - v0[end, :] = system.initial_condition.pressure + v0[end, :] = system.initial_condition.density + v0[ndims(system) + 1, :] = system.initial_condition.pressure return v0 end diff --git a/src/schemes/fluid/fluid.jl b/src/schemes/fluid/fluid.jl index ed8e19780f..a787a69841 100644 --- a/src/schemes/fluid/fluid.jl +++ b/src/schemes/fluid/fluid.jl @@ -125,6 +125,42 @@ function compute_density!(system, u, u_ode, semi, ::SummationDensity) summation_density!(system, semi, u, u_ode, density) end +# With 'SummationDensity', density is calculated in wcsph/system.jl:compute_density! +@inline function continuity_equation!(dv, density_calculator::SummationDensity, + particle_system, neighbor_system, + v_particle_system, v_neighbor_system, + particle, neighbor, pos_diff, distance, + m_b, rho_a, rho_b, grad_kernel) + return dv +end + +# This formulation was chosen to be consistent with the used pressure_acceleration formulations +@propagate_inbounds function continuity_equation!(dv, density_calculator::ContinuityDensity, + particle_system::AbstractFluidSystem, + neighbor_system, + v_particle_system, v_neighbor_system, + particle, neighbor, pos_diff, distance, + m_b, rho_a, rho_b, grad_kernel) + vdiff = current_velocity(v_particle_system, particle_system, particle) - + current_velocity(v_neighbor_system, neighbor_system, neighbor) + + dv[end, particle] += rho_a / rho_b * m_b * dot(vdiff, grad_kernel) + + # Artificial density diffusion should only be applied to systems representing a fluid + # with the same physical properties i.e. density and viscosity. + # TODO: shouldn't be applied to particles on the interface (depends on PR #539) + if particle_system === neighbor_system + density_diffusion!(dv, density_diffusion(particle_system), + v_particle_system, particle, neighbor, + pos_diff, distance, m_b, rho_a, rho_b, particle_system, + grad_kernel) + end + + continuity_equation_shifting!(dv, shifting_technique(particle_system), + particle_system, neighbor_system, + particle, neighbor, grad_kernel, rho_a, rho_b, m_b) +end + function calculate_dt(v_ode, u_ode, cfl_number, system::AbstractFluidSystem, semi) (; viscosity, acceleration, surface_tension) = system diff --git a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl index 41c3a9f55f..ff75ed21d6 100644 --- a/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl +++ b/src/schemes/fluid/weakly_compressible_sph/density_diffusion.jl @@ -15,6 +15,8 @@ See [Density Diffusion](@ref density_diffusion) for a comparison and more detail """ abstract type AbstractDensityDiffusion end +@inline density_diffusion(system) = nothing + # Most density diffusion formulations don't need updating function update!(density_diffusion, v, u, system, semi) return density_diffusion @@ -217,8 +219,7 @@ end distance < sqrt(eps(typeof(distance))) && return (; delta) = density_diffusion - (; state_equation) = particle_system - (; sound_speed) = state_equation + sound_speed = system_sound_speed(particle_system) volume_b = m_b / rho_b diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl index 2b62825a53..413a58aaf6 100644 --- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl +++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl @@ -111,43 +111,6 @@ function interact!(dv, v_particle_system, u_particle_system, return dv end -# With 'SummationDensity', density is calculated in wcsph/system.jl:compute_density! -@inline function continuity_equation!(dv, density_calculator::SummationDensity, - particle_system, neighbor_system, - v_particle_system, v_neighbor_system, - particle, neighbor, pos_diff, distance, - m_b, rho_a, rho_b, grad_kernel) - return dv -end - -# This formulation was chosen to be consistent with the used pressure_acceleration formulations. -@propagate_inbounds function continuity_equation!(dv, density_calculator::ContinuityDensity, - particle_system::WeaklyCompressibleSPHSystem, - neighbor_system, - v_particle_system, v_neighbor_system, - particle, neighbor, pos_diff, distance, - m_b, rho_a, rho_b, grad_kernel) - (; density_diffusion) = particle_system - - vdiff = current_velocity(v_particle_system, particle_system, particle) - - current_velocity(v_neighbor_system, neighbor_system, neighbor) - - dv[end, particle] += rho_a / rho_b * m_b * dot(vdiff, grad_kernel) - - # Artificial density diffusion should only be applied to systems representing a fluid - # with the same physical properties i.e. density and viscosity. - # TODO: shouldn't be applied to particles on the interface (depends on PR #539) - if particle_system === neighbor_system - density_diffusion!(dv, density_diffusion, v_particle_system, particle, neighbor, - pos_diff, distance, m_b, rho_a, rho_b, particle_system, - grad_kernel) - end - - continuity_equation_shifting!(dv, shifting_technique(particle_system), - particle_system, neighbor_system, - particle, neighbor, grad_kernel, rho_a, rho_b, m_b) -end - @propagate_inbounds function particle_neighbor_pressure(v_particle_system, v_neighbor_system, particle_system, neighbor_system, diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl index 1b7da00d5e..13c69dcf7f 100644 --- a/src/schemes/fluid/weakly_compressible_sph/system.jl +++ b/src/schemes/fluid/weakly_compressible_sph/system.jl @@ -285,6 +285,8 @@ end @inline shifting_technique(system::WeaklyCompressibleSPHSystem) = system.shifting_technique +@inline density_diffusion(system::WeaklyCompressibleSPHSystem) = system.density_diffusion + function update_quantities!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_ode, semi, t) (; density_calculator, density_diffusion, correction) = system diff --git a/test/schemes/fluid/rhs.jl b/test/schemes/fluid/rhs.jl index 8ce6c01013..de14020a87 100644 --- a/test/schemes/fluid/rhs.jl +++ b/test/schemes/fluid/rhs.jl @@ -151,7 +151,7 @@ # Density is integrated with `ContinuityDensity` if system isa EntropicallyDampedSPHSystem - v = vcat(fluid.velocity, fluid.density', fluid.pressure') + v = vcat(fluid.velocity, fluid.pressure', fluid.density') else v = vcat(fluid.velocity, fluid.density') end @@ -185,14 +185,10 @@ @test isapprox(deriv_angular_momentum, zeros(3), atol=4e-15) # Total energy conservation - function drho(::ContinuityDensity, ::WeaklyCompressibleSPHSystem, + function drho(::ContinuityDensity, ::TrixiParticles.AbstractFluidSystem, particle) return dv[end, particle] end - function drho(::ContinuityDensity, ::EntropicallyDampedSPHSystem, - particle) - return dv[end - 1, particle] - end function drho(::SummationDensity, system, particle) return sum(neighbor -> drho_particle(particle, neighbor), diff --git a/test/validation/validation.jl b/test/validation/validation.jl index 70aceb4bf4..c156cbbd95 100644 --- a/test/validation/validation.jl +++ b/test/validation/validation.jl @@ -65,8 +65,8 @@ @test isapprox(error_wcsph_P2, 0, atol=5e-4) else # Reference values are computed with 1.11 - @test isapprox(error_edac_P1, 0, atol=eps()) - @test isapprox(error_edac_P2, 0, atol=eps()) + @test isapprox(error_edac_P1, 0, atol=5e-7) + @test isapprox(error_edac_P2, 0, atol=4e-11) @test isapprox(error_wcsph_P1, 0, atol=eps()) @test isapprox(error_wcsph_P2, 0, atol=eps()) end From 1545e1b636cd880951531e77df0f1f75508a54ee Mon Sep 17 00:00:00 2001 From: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:36:45 +0200 Subject: [PATCH 13/15] Define boundary zone from STL-file (#597) * add oriented bounding box * fix * fix * fix * fix default lib dependency * add tests and docs * add test data * try to fix docs * implement suggestions * fix docs * rename `plane` to `boundary_face` * fix tests `boundary_face` * add doc test * fix doc tests * fix unit tests * rename `plane_normal` to `face_normal` * rename parameter type * apply formatter * add NEWS entry * implement suggerstions * update NEWS --------- Co-authored-by: LasNikas Co-authored-by: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> --- NEWS.md | 2 + Project.toml | 1 + docs/src/systems/boundary.md | 5 + examples/fluid/pipe_flow_2d.jl | 10 +- examples/fluid/pipe_flow_3d.jl | 8 +- src/TrixiParticles.jl | 3 +- src/io/io.jl | 2 +- src/preprocessing/geometries/geometries.jl | 70 +++++++++ .../boundary/open_boundary/boundary_zones.jl | 140 +++++++++-------- .../method_of_characteristics.jl | 8 +- .../boundary/open_boundary/mirroring.jl | 32 ++-- src/schemes/boundary/open_boundary/system.jl | 6 +- test/general/buffer.jl | 4 +- test/preprocessing/data/inflow.stl | Bin 0 -> 460184 bytes test/preprocessing/data/inflow_geometry.stl | Bin 0 -> 9734 bytes test/preprocessing/geometries/geometries.jl | 15 ++ .../boundary/open_boundary/boundary_zone.jl | 146 ++++++++++-------- .../open_boundary/characteristic_variables.jl | 26 ++-- .../boundary/open_boundary/mirroring.jl | 42 ++--- test/systems/open_boundary_system.jl | 10 +- 20 files changed, 322 insertions(+), 208 deletions(-) create mode 100644 test/preprocessing/data/inflow.stl create mode 100644 test/preprocessing/data/inflow_geometry.stl diff --git a/NEWS.md b/NEWS.md index da99ff8101..601e39d313 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,6 +28,8 @@ used in the Julia ecosystem. Notable changes will be documented in this file for It is now possible to pass multiple `BoundaryZone`s to a single `OpenBoundarySystem`. Reference values are now assigned individually to each `BoundaryZone`. (#866) +- Rename keyword arguments `plane` and `plane_normal` for `BoundaryZone` to `boundary_face` and `face_normal` (#597). + - The argument of `TransportVelocityAdami` is now a keyword argument. `TransportVelocityAdami(1000.0)` now becomes `TransportVelocityAdami(background_pressure=1000.0)` (#884). diff --git a/Project.toml b/Project.toml index 9c8aa85fb3..bf6028cb3e 100644 --- a/Project.toml +++ b/Project.toml @@ -27,6 +27,7 @@ RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StrideArraysCore = "7792a7ef-975c-4747-a70f-980b88e8d1da" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" TrixiBase = "9a0f1c46-06d5-4909-a5a3-ce25d3fa3284" diff --git a/docs/src/systems/boundary.md b/docs/src/systems/boundary.md index 6c41262935..25a082e803 100644 --- a/docs/src/systems/boundary.md +++ b/docs/src/systems/boundary.md @@ -233,6 +233,11 @@ Modules = [TrixiParticles] Pages = [joinpath("schemes", "boundary", "open_boundary", "boundary_zones.jl")] ``` +```@autodocs +Modules = [TrixiParticles] +Filter = t -> typeof(t) === typeof(TrixiParticles.planar_geometry_to_face) +``` + # [Open Boundary Models](@id open_boundary_models) We offer two models for open boundaries, with the choice depending on the specific problem and flow characteristics near the boundary: 1. [**Method of characteristics**](@ref method_of_characteristics): The method of characteristics is typically used in problems where tracking of wave propagation diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl index 6c116efe26..df4652b80d 100644 --- a/examples/fluid/pipe_flow_2d.jl +++ b/examples/fluid/pipe_flow_2d.jl @@ -113,9 +113,9 @@ reference_velocity_in = velocity_function2d reference_pressure_in = nothing reference_density_in = nothing boundary_type_in = InFlow() -plane_in = ([0.0, 0.0], [0.0, domain_size[2]]) -inflow = BoundaryZone(; plane=plane_in, plane_normal=flow_direction, open_boundary_layers, - density=fluid_density, particle_spacing, +face_in = ([0.0, 0.0], [0.0, domain_size[2]]) +inflow = BoundaryZone(; boundary_face=face_in, face_normal=flow_direction, + open_boundary_layers, density=fluid_density, particle_spacing, reference_density=reference_density_in, reference_pressure=reference_pressure_in, reference_velocity=reference_velocity_in, @@ -125,8 +125,8 @@ reference_velocity_out = nothing reference_pressure_out = nothing reference_density_out = nothing boundary_type_out = OutFlow() -plane_out = ([min_coords_outlet[1], 0.0], [min_coords_outlet[1], domain_size[2]]) -outflow = BoundaryZone(; plane=plane_out, plane_normal=(-flow_direction), +face_out = ([min_coords_outlet[1], 0.0], [min_coords_outlet[1], domain_size[2]]) +outflow = BoundaryZone(; boundary_face=face_out, face_normal=(-flow_direction), open_boundary_layers, density=fluid_density, particle_spacing, reference_density=reference_density_out, reference_pressure=reference_pressure_out, diff --git a/examples/fluid/pipe_flow_3d.jl b/examples/fluid/pipe_flow_3d.jl index 595859affd..a0f16b57a6 100644 --- a/examples/fluid/pipe_flow_3d.jl +++ b/examples/fluid/pipe_flow_3d.jl @@ -40,7 +40,7 @@ trixi_include(@__MODULE__, joinpath(examples_dir(), "fluid", "pipe_flow_2d.jl"), tspan=tspan, prescribed_velocity=prescribed_velocity, open_boundary_layers=open_boundary_layers, min_coords_inlet=min_coords_inlet, min_coords_outlet=min_coords_outlet, - plane_in=([0.0, 0.0, 0.0], [0.0, domain_size[2], 0.0], - [0.0, 0.0, domain_size[3]]), - plane_out=([domain_size[1], 0.0, 0.0], [domain_size[1], domain_size[2], 0.0], - [domain_size[1], 0.0, domain_size[3]])) + face_in=([0.0, 0.0, 0.0], [0.0, domain_size[2], 0.0], + [0.0, 0.0, domain_size[3]]), + face_out=([domain_size[1], 0.0, 0.0], [domain_size[1], domain_size[2], 0.0], + [domain_size[1], 0.0, domain_size[3]])) diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl index 853ba9a3d2..b75a219855 100644 --- a/src/TrixiParticles.jl +++ b/src/TrixiParticles.jl @@ -27,6 +27,7 @@ using SciMLBase: CallbackSet, DiscreteCallback, DynamicalODEProblem, u_modified! get_du @reexport using StaticArrays: SVector using StaticArrays: @SMatrix, SMatrix, setindex +using Statistics: Statistics using StrideArraysCore: PtrArray, StaticInt using TimerOutputs: TimerOutput, TimerOutputs, print_timer, reset_timer!, @notimeit using TrixiBase: @trixi_timeit, timer, timeit_debug_enabled, @@ -92,7 +93,7 @@ export RectangularTank, RectangularShape, SphereShape, ComplexShape export ParticlePackingSystem, SignedDistanceField export WindingNumberHormann, WindingNumberJacobson export VoxelSphere, RoundSphere, reset_wall!, extrude_geometry, load_geometry, - sample_boundary + sample_boundary, planar_geometry_to_face export SourceTermDamping export ShepardKernelCorrection, KernelCorrection, AkinciFreeSurfaceCorrection, GradientCorrection, BlendedGradientCorrection, MixedKernelGradientCorrection diff --git a/src/io/io.jl b/src/io/io.jl index 19b4a22cc2..d8c075c523 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -287,7 +287,7 @@ function add_system_data!(system_data, boundary_zone::BoundaryZone, indice) system_data[zone_name]["zone_origin"] = boundary_zone.zone_origin system_data[zone_name]["zone_width"] = boundary_zone.zone_width system_data[zone_name]["flow_direction"] = boundary_zone.flow_direction - system_data[zone_name]["plane_normal"] = boundary_zone.plane_normal + system_data[zone_name]["face_normal"] = boundary_zone.face_normal system_data[zone_name]["reference_values"] = boundary_zone.reference_values system_data[zone_name]["average_inflow_velocity"] = boundary_zone.average_inflow_velocity system_data[zone_name]["prescribed_density"] = boundary_zone.prescribed_density diff --git a/src/preprocessing/geometries/geometries.jl b/src/preprocessing/geometries/geometries.jl index f18e344186..3a7b633522 100644 --- a/src/preprocessing/geometries/geometries.jl +++ b/src/preprocessing/geometries/geometries.jl @@ -55,3 +55,73 @@ function Base.intersect(initial_condition::InitialCondition, return intersect(result, Base.tail(geometries)...) end + +""" + planar_geometry_to_face(planar_geometry::TriangleMesh) + +Extracts a simplified rectangular face and its normal vector from an arbitrary planar geometry +(`TriangleMesh` loaded via [`load_geometry`](@ref)) +for use as a boundary zone interface in [`BoundaryZone`](@ref). +This function computes the corner points of an oriented bounding box +that best represents the essential orientation and extent of the input geometry. +The geometry must be planar (all vertices should lie in the same plane), +but can have complex or non-rectangular boundaries. + +!!! note "Face Normal Orientation" + All face normals of the input geometry must point inside the fluid domain. + The returned plane normal is computed by averaging all face normals, so consistent orientation is required. + +# Arguments +- `planar_geometry`: A planar geometry (`TriangleMesh` loaded via [`load_geometry`](@ref)). + +# Returns +- `face_vertices`: Tuple of three vertices defining the rectangular face (corner points of the oriented bounding box). +- `face_normal`: Normalized normal vector of the face. + +# Example +```jldoctest; output=false, filter=r"face = .*" +file = pkgdir(TrixiParticles, "test", "preprocessing", "data") +planar_geometry = load_geometry(joinpath(file, "inflow_geometry.stl")) + +face, face_normal = planar_geometry_to_face(planar_geometry) + +# output +(face = ...) +``` +""" +function planar_geometry_to_face(planar_geometry::TriangleMesh) + face_normal = normalize(sum(planar_geometry.face_normals) / nfaces(planar_geometry)) + + face_vertices = oriented_bounding_box(stack(planar_geometry.vertices)) + + # Vectors spanning the face + edge1 = face_vertices[:, 2] - face_vertices[:, 1] + edge2 = face_vertices[:, 3] - face_vertices[:, 1] + + if abs(dot(edge1, face_normal)) > 1e-2 || abs(dot(edge2, face_normal)) > 1e-2 + throw(ArgumentError("geometry is not planar")) + end + + return (; face=(face_vertices[:, 1], face_vertices[:, 2], face_vertices[:, 3]), + face_normal=face_normal) +end + +# According to: +# https://logicatcore.github.io/scratchpad/lidar/sensor-fusion/jupyter/2021/04/20/3D-Oriented-Bounding-Box.html +function oriented_bounding_box(point_cloud) + covariance_matrix = Statistics.cov(point_cloud; dims=2) + eigen_vectors = Statistics.eigvecs(covariance_matrix) + means = Statistics.mean(point_cloud, dims=2) + + centered_data = point_cloud .- means + + aligned_coords = eigen_vectors' * centered_data + + min_corner = minimum(aligned_coords, dims=2) + max_corner = maximum(aligned_coords, dims=2) + + face_vertices = hcat(min_corner, max_corner, + [min_corner[1], max_corner[2], min_corner[3]]) + + return eigen_vectors * face_vertices .+ means +end diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl index d1fc5ce619..30d1f444f3 100644 --- a/src/schemes/boundary/open_boundary/boundary_zones.jl +++ b/src/schemes/boundary/open_boundary/boundary_zones.jl @@ -5,7 +5,7 @@ struct InFlow end struct OutFlow end @doc raw""" - BoundaryZone(; plane, plane_normal, density, particle_spacing, + BoundaryZone(; boundary_face, face_normal, density, particle_spacing, initial_condition=nothing, extrude_geometry=nothing, open_boundary_layers::Integer, average_inflow_velocity=true, boundary_type=BidirectionalFlow(), @@ -14,19 +14,22 @@ struct OutFlow end Boundary zone for [`OpenBoundarySystem`](@ref). -The specified plane (line in 2D or rectangle in 3D) will be extruded in the direction -opposite to `plane_normal` to create a box for the boundary zone. +The specified `boundary_face` (line in 2D or rectangle in 3D) will be extruded in the direction +opposite to `face_normal` to create a box for the boundary zone. +To specify the `boundary_face`, pass the required vertices as described below. +For complex 3D simulations, these vertices can also be extracted from an STL file +(see [`planar_geometry_to_face`](@ref)). There are three ways to specify the actual shape of the boundary zone: 1. Don't pass `initial_condition` or `extrude_geometry`. The boundary zone box will then be filled with boundary particles (default). 2. Specify `extrude_geometry` by passing a 1D shape in 2D or a 2D shape in 3D, - which is then extruded in the direction opposite to `plane_normal` to create the boundary particles. + which is then extruded in the direction opposite to `face_normal` to create the boundary particles. - In 2D, the shape must be either an initial condition with 2D coordinates, which lies - on the line specified by `plane`, or an initial condition with 1D coordinates, which lies - on the line specified by `plane` when a y-coordinate of `0` is added. + on the line specified by `boundary_face`, or an initial condition with 1D coordinates, + which lies on the line specified by `boundary_face` when a y-coordinate of `0` is added. - In 3D, the shape must be either an initial condition with 3D coordinates, which lies - in the rectangle specified by `plane`, or an initial condition with 2D coordinates, - which lies in the rectangle specified by `plane` when a z-coordinate of `0` is added. + in the rectangle specified by `boundary_face`, or an initial condition with 2D coordinates, + which lies in the rectangle specified by `boundary_face` when a z-coordinate of `0` is added. 3. Specify `initial_condition` by passing a 2D initial condition in 2D or a 3D initial condition in 3D, which will be used for the boundary particles. @@ -34,27 +37,27 @@ There are three ways to specify the actual shape of the boundary zone: Particles outside the boundary zone box will be removed. # Keywords -- `plane`: Tuple of points defining a part of the surface of the domain. - The points must either span a line in 2D or a rectangle in 3D. - This line or rectangle is then extruded in upstream direction to obtain - the boundary zone. - In 2D, pass two points ``(A, B)``, so that the interval ``[A, B]`` is - the inflow surface. - In 3D, pass three points ``(A, B, C)``, so that the rectangular inflow surface - is spanned by the vectors ``\widehat{AB}`` and ``\widehat{AC}``. - These two vectors must be orthogonal. -- `plane_normal`: Vector defining the plane normal. It always points inside the fluid domain. +- `boundary_face`: Tuple of vertices defining a part of the surface of the domain. + The vertices must either span a line in 2D or a rectangle in 3D. + This line or rectangle is then extruded in upstream direction to obtain + the boundary zone. + In 2D, pass two vertices ``(A, B)``, so that the interval ``[A, B]`` is + the inflow surface. + In 3D, pass three vertices ``(A, B, C)``, so that the rectangular inflow surface + is spanned by the vectors ``\widehat{AB}`` and ``\widehat{AC}``. + These two vectors must be orthogonal. +- `face_normal`: Vector defining the normal of the `boundary_face`. It always points inside the fluid domain. - `boundary_type=BidirectionalFlow()`: Specify the type of the boundary. Available types are - `InFlow()` for an inflow boundary - `OutFlow()` for an outflow boundary - `BidirectionalFlow()` (default) for an bidirectional flow boundary -- `open_boundary_layers`: Number of particle layers in the direction opposite to `plane_normal`. +- `open_boundary_layers`: Number of particle layers in the direction opposite to `face_normal`. - `particle_spacing`: The spacing between the particles (see [`InitialCondition`](@ref)). - `density`: Particle density (see [`InitialCondition`](@ref)). - `initial_condition=nothing`: `InitialCondition` for the inflow particles. Particles outside the boundary zone will be removed. Do not use together with `extrude_geometry`. -- `extrude_geometry=nothing`: 1D shape in 2D or 2D shape in 3D, which lies on the plane +- `extrude_geometry=nothing`: 1D shape in 2D or 2D shape in 3D, which lies on the `boundary_face` and is extruded upstream to obtain the inflow particles. See point 2 above for more details. - `average_inflow_velocity=true`: If `true`, the extrapolated inflow velocity is averaged @@ -82,46 +85,47 @@ There are three ways to specify the actual shape of the boundary zone: # Examples ```jldoctest; output=false # 2D -plane_points = ([0.0, 0.0], [0.0, 1.0]) -plane_normal = [1.0, 0.0] +face_vertices = ([0.0, 0.0], [0.0, 1.0]) +face_normal = [1.0, 0.0] # Constant reference velocity: velocity_const = [1.0, 0.0] -inflow_1 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, +inflow_1 = BoundaryZone(; boundary_face=face_vertices, face_normal, particle_spacing=0.1, density=1.0, open_boundary_layers=4, boundary_type=InFlow(), reference_velocity=velocity_const) # Reference velocity as a function (parabolic velocity profile): velocity_func = (pos, t) -> SVector(4.0 * pos[2] * (1.0 - pos[2]), 0.0) -inflow_2 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, +inflow_2 = BoundaryZone(; boundary_face=face_vertices, face_normal, particle_spacing=0.1, density=1.0, open_boundary_layers=4, boundary_type=InFlow(), reference_velocity=velocity_func) # 3D -plane_points = ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) -plane_normal = [0.0, 0.0, 1.0] +face_vertices = ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) +face_normal = [0.0, 0.0, 1.0] # Constant reference pressure: pressure_const = 0.0 -outflow_1 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, - open_boundary_layers=4, boundary_type=OutFlow(), +outflow_1 = BoundaryZone(; boundary_face=face_vertices, face_normal, particle_spacing=0.1, + density=1.0, open_boundary_layers=4, boundary_type=OutFlow(), reference_pressure=pressure_const) # Reference pressure as a function (y-dependent profile, sinusoidal in time): pressure_func = (pos, t) -> pos[2] * sin(2pi * t) -outflow_2 = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, density=1.0, - open_boundary_layers=4, boundary_type=OutFlow(), +outflow_2 = BoundaryZone(; boundary_face=face_vertices, face_normal, particle_spacing=0.1, + density=1.0, open_boundary_layers=4, boundary_type=OutFlow(), reference_pressure=pressure_func) # 3D particles sampled as cylinder circle = SphereShape(0.1, 0.5, (0.5, 0.5), 1.0, sphere_type=RoundSphere()) -bidirectional_flow = BoundaryZone(; plane=plane_points, plane_normal, particle_spacing=0.1, - density=1.0, boundary_type=BidirectionalFlow(), +bidirectional_flow = BoundaryZone(; boundary_face=face_vertices, face_normal, + particle_spacing=0.1, density=1.0, + boundary_type=BidirectionalFlow(), extrude_geometry=circle, open_boundary_layers=4) # output @@ -137,13 +141,13 @@ bidirectional_flow = BoundaryZone(; plane=plane_points, plane_normal, particle_s !!! warning "Experimental Implementation" This is an experimental feature and may change in any future releases. """ -struct BoundaryZone{IC, S, ZO, ZW, FD, PN, R} +struct BoundaryZone{IC, S, ZO, ZW, FD, FN, R} initial_condition :: IC spanning_set :: S zone_origin :: ZO zone_width :: ZW flow_direction :: FD - plane_normal :: PN + face_normal :: FN reference_values :: R # Note that the following can't be static type parameters, as all boundary zones in a system # must have the same type, so that we can loop over them in a type-stable way. @@ -153,7 +157,7 @@ struct BoundaryZone{IC, S, ZO, ZW, FD, PN, R} prescribed_velocity :: Bool end -function BoundaryZone(; plane, plane_normal, density, particle_spacing, +function BoundaryZone(; boundary_face, face_normal, density, particle_spacing, initial_condition=nothing, extrude_geometry=nothing, open_boundary_layers::Integer, average_inflow_velocity=true, boundary_type=BidirectionalFlow(), @@ -163,12 +167,12 @@ function BoundaryZone(; plane, plane_normal, density, particle_spacing, throw(ArgumentError("`open_boundary_layers` must be positive and greater than zero")) end - # `plane_normal` always points in fluid domain - plane_normal_ = normalize(SVector(plane_normal...)) + # `face_normal` always points in fluid domain + face_normal_ = normalize(SVector(face_normal...)) ic, flow_direction, spanning_set_, zone_origin, - zone_width = set_up_boundary_zone(plane, plane_normal_, density, particle_spacing, - initial_condition, extrude_geometry, + zone_width = set_up_boundary_zone(boundary_face, face_normal_, density, + particle_spacing, initial_condition, extrude_geometry, open_boundary_layers, boundary_type) NDIMS = ndims(ic) @@ -245,17 +249,17 @@ function BoundaryZone(; plane, plane_normal, density, particle_spacing, end return BoundaryZone(ic, spanning_set_, zone_origin, zone_width, - flow_direction, plane_normal_, reference_values, + flow_direction, face_normal_, reference_values, average_inflow_velocity, prescribed_density, prescribed_pressure, prescribed_velocity) end function boundary_type_name(boundary_zone::BoundaryZone) - (; flow_direction, plane_normal) = boundary_zone + (; flow_direction, face_normal) = boundary_zone if isnothing(flow_direction) return "bidirectional_flow" - elseif signbit(dot(flow_direction, plane_normal)) + elseif signbit(dot(flow_direction, face_normal)) return "outflow" else return "inflow" @@ -283,30 +287,30 @@ function Base.show(io::IO, ::MIME"text/plain", boundary_zone::BoundaryZone) end end -function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, +function set_up_boundary_zone(boundary_face, face_normal, density, particle_spacing, initial_condition, extrude_geometry, open_boundary_layers, boundary_type) if boundary_type isa InFlow # Unit vector pointing in downstream direction - flow_direction = plane_normal + flow_direction = face_normal elseif boundary_type isa OutFlow # Unit vector pointing in downstream direction - flow_direction = -plane_normal + flow_direction = -face_normal elseif boundary_type isa BidirectionalFlow flow_direction = nothing end # Sample particles in boundary zone if isnothing(initial_condition) && isnothing(extrude_geometry) - initial_condition = TrixiParticles.extrude_geometry(plane; particle_spacing, + initial_condition = TrixiParticles.extrude_geometry(boundary_face; particle_spacing, density, - direction=(-plane_normal), + direction=(-face_normal), n_extrude=open_boundary_layers) elseif !isnothing(extrude_geometry) initial_condition = TrixiParticles.extrude_geometry(extrude_geometry; particle_spacing, density, - direction=(-plane_normal), + direction=(-face_normal), n_extrude=open_boundary_layers) else initial_condition = initial_condition @@ -318,17 +322,17 @@ function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, zone_width = open_boundary_layers * initial_condition.particle_spacing # Vectors spanning the boundary zone/box - spanning_set, zone_origin = calculate_spanning_vectors(plane, zone_width) + spanning_set, zone_origin = calculate_spanning_vectors(boundary_face, zone_width) - # First vector of `spanning_vectors` is normal to the boundary plane. - dot_plane_normal = dot(normalize(spanning_set[:, 1]), plane_normal) + # First vector of `spanning_vectors` is normal to the boundary face. + dot_face_normal = dot(normalize(spanning_set[:, 1]), face_normal) - if !isapprox(abs(dot_plane_normal), 1.0, atol=1e-7) - throw(ArgumentError("`plane_normal` is not normal to the boundary plane")) + if !isapprox(abs(dot_face_normal), 1, rtol=1e-5) + throw(ArgumentError("`face_normal` is not normal to the boundary face")) end if boundary_type isa InFlow - # First vector of `spanning_vectors` is normal to the boundary plane + # First vector of `spanning_vectors` is normal to the boundary face dot_flow = dot(normalize(spanning_set[:, 1]), flow_direction) # The vector must point in upstream direction for an inflow boundary. @@ -336,7 +340,7 @@ function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, spanning_set[:, 1] .*= -sign(dot_flow) elseif boundary_type isa OutFlow - # First vector of `spanning_vectors` is normal to the boundary plane + # First vector of `spanning_vectors` is normal to the boundary face dot_flow = dot(normalize(spanning_set[:, 1]), flow_direction) # The vector must point in downstream direction for an outflow boundary. @@ -345,7 +349,7 @@ function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, elseif boundary_type isa BidirectionalFlow # Flip the normal vector to point opposite to fluid domain - spanning_set[:, 1] .*= -sign(dot_plane_normal) + spanning_set[:, 1] .*= -sign(dot_face_normal) end spanning_set_ = reinterpret(reshape, SVector{NDIMS, ELTYPE}, spanning_set) @@ -357,30 +361,30 @@ function set_up_boundary_zone(plane, plane_normal, density, particle_spacing, return ic, flow_direction, spanning_set_, zone_origin, zone_width end -function calculate_spanning_vectors(plane, zone_width) - return spanning_vectors(Tuple(plane), zone_width), SVector(plane[1]...) +function calculate_spanning_vectors(boundary_face, zone_width) + return spanning_vectors(Tuple(boundary_face), zone_width), SVector(boundary_face[1]...) end -function spanning_vectors(plane_points::NTuple{2}, zone_width) - plane_size = plane_points[2] - plane_points[1] +function spanning_vectors(face_vertices::NTuple{2}, zone_width) + face_size = face_vertices[2] - face_vertices[1] - # Calculate normal vector of plane - b = normalize([-plane_size[2], plane_size[1]]) * zone_width + # Calculate normal vector of `boundary_face` + b = normalize([-face_size[2], face_size[1]]) * zone_width - return hcat(b, plane_size) + return hcat(b, face_size) end -function spanning_vectors(plane_points::NTuple{3}, zone_width) - # Vectors spanning the plane - edge1 = plane_points[2] - plane_points[1] - edge2 = plane_points[3] - plane_points[1] +function spanning_vectors(face_vertices::NTuple{3}, zone_width) + # Vectors spanning the `boundary_face` + edge1 = face_vertices[2] - face_vertices[1] + edge2 = face_vertices[3] - face_vertices[1] # Check if the edges are linearly dependent (to avoid degenerate planes) if isapprox(norm(cross(edge1, edge2)), 0.0; atol=eps()) throw(ArgumentError("the vectors `AB` and `AC` must not be collinear")) end - # Calculate normal vector of plane + # Calculate normal vector of `boundary_face` c = Vector(normalize(cross(edge2, edge1)) * zone_width) return hcat(c, edge1, edge2) diff --git a/src/schemes/boundary/open_boundary/method_of_characteristics.jl b/src/schemes/boundary/open_boundary/method_of_characteristics.jl index d650a3e7cf..aebbacc724 100644 --- a/src/schemes/boundary/open_boundary/method_of_characteristics.jl +++ b/src/schemes/boundary/open_boundary/method_of_characteristics.jl @@ -203,10 +203,10 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t) end boundary_zone = current_boundary_zone(system, particle) - (; flow_direction, plane_normal) = boundary_zone + (; flow_direction, face_normal) = boundary_zone # Outflow - if signbit(dot(flow_direction, plane_normal)) + if signbit(dot(flow_direction, face_normal)) # J3 is prescribed (i.e. determined from the exterior of the domain). # J1 and J2 is transmitted from the domain interior. characteristics[3, particle] = zero(eltype(characteristics)) @@ -223,10 +223,10 @@ end function average_velocity!(v, u, system, ::BoundaryModelCharacteristicsLastiwka, boundary_zone, semi) - (; flow_direction, plane_normal) = boundary_zone + (; flow_direction, face_normal) = boundary_zone # This is an outflow. Only apply averaging at the inflow. - signbit(dot(flow_direction, plane_normal)) && return v + signbit(dot(flow_direction, face_normal)) && return v particles_in_zone = findall(particle -> boundary_zone == current_boundary_zone(system, particle), diff --git a/src/schemes/boundary/open_boundary/mirroring.jl b/src/schemes/boundary/open_boundary/mirroring.jl index 4f1269df94..c516a4acb0 100644 --- a/src/schemes/boundary/open_boundary/mirroring.jl +++ b/src/schemes/boundary/open_boundary/mirroring.jl @@ -225,8 +225,8 @@ function extrapolate_values!(system, # See https://doi.org/10.1016/j.jcp.2020.110029 Section 3.3.: # "Because flow from the inlet interface occurs perpendicular to the boundary, # only this component of interpolated velocity is kept [...]" - project_velocity_on_plane_normal!(v_open_boundary, system, particle, - boundary_zone) + project_velocity_on_face_normal!(v_open_boundary, system, particle, + boundary_zone) end # No else: `correction_matrix[][1, 1] <= eps()` means no fluid neighbors @@ -266,8 +266,8 @@ function extrapolate_values!(system, # See https://doi.org/10.1016/j.jcp.2020.110029 Section 3.3.: # "Because flow from the inlet interface occurs perpendicular to the boundary, # only this component of interpolated velocity is kept [...]" - project_velocity_on_plane_normal!(v_open_boundary, system, particle, - boundary_zone) + project_velocity_on_face_normal!(v_open_boundary, system, particle, + boundary_zone) end end end @@ -353,8 +353,8 @@ function extrapolate_values!(system, mirror_method::ZerothOrderMirroring, # See https://doi.org/10.1016/j.jcp.2020.110029 Section 3.3.: # "Because flow from the inlet interface occurs perpendicular to the boundary, # only this component of interpolated velocity is kept [...]" - project_velocity_on_plane_normal!(v_open_boundary, system, particle, - boundary_zone) + project_velocity_on_face_normal!(v_open_boundary, system, particle, + boundary_zone) end end end @@ -476,27 +476,27 @@ end function mirror_position(particle_coords, boundary_zone) particle_position = particle_coords - boundary_zone.zone_origin - dist = dot(particle_position, boundary_zone.plane_normal) + dist = dot(particle_position, boundary_zone.face_normal) - return particle_coords - 2 * dist * boundary_zone.plane_normal + return particle_coords - 2 * dist * boundary_zone.face_normal end # Only for inflow boundary zones function average_velocity!(v, u, system, boundary_zone, semi) - (; plane_normal, zone_origin, initial_condition, flow_direction) = boundary_zone + (; face_normal, zone_origin, initial_condition, flow_direction) = boundary_zone # Bidirectional flow isnothing(flow_direction) && return v # Outflow - signbit(dot(flow_direction, plane_normal)) && return v + signbit(dot(flow_direction, face_normal)) && return v # We only use the extrapolated velocity in the vicinity of the transition region. # Otherwise, if the boundary zone is too large, averaging would be excessively influenced # by the fluid velocity further away from the boundary. max_dist = initial_condition.particle_spacing * 110 / 100 - candidates = findall(x -> dot(x - zone_origin, -plane_normal) <= max_dist, + candidates = findall(x -> dot(x - zone_origin, -face_normal) <= max_dist, reinterpret(reshape, SVector{ndims(system), eltype(u)}, u)) particles_in_zone = findall(particle -> boundary_zone == @@ -521,22 +521,22 @@ function average_velocity!(v, u, system, boundary_zone, semi) end # Only for inflow boundary zones -function project_velocity_on_plane_normal!(v, system, particle, boundary_zone) - (; plane_normal, flow_direction) = boundary_zone +function project_velocity_on_face_normal!(v, system, particle, boundary_zone) + (; face_normal, flow_direction) = boundary_zone # Bidirectional flow isnothing(flow_direction) && return v # Outflow - signbit(dot(flow_direction, plane_normal)) && return v + signbit(dot(flow_direction, face_normal)) && return v # Project `vel` on the normal direction of the boundary zone # See https://doi.org/10.1016/j.jcp.2020.110029 Section 3.3.: # "Because flow from the inlet interface occurs perpendicular to the boundary, # only this component of interpolated velocity is kept [...]" v_particle = current_velocity(v, system, particle) - v_particle_projected = dot(v_particle, boundary_zone.plane_normal) * - boundary_zone.plane_normal + v_particle_projected = dot(v_particle, boundary_zone.face_normal) * + boundary_zone.face_normal for dim in eachindex(v_particle) @inbounds v[dim, particle] = v_particle_projected[dim] diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl index 08f332e928..9cf5233c17 100644 --- a/src/schemes/boundary/open_boundary/system.jl +++ b/src/schemes/boundary/open_boundary/system.jl @@ -89,7 +89,7 @@ function OpenBoundarySystem(boundary_zones::Union{BoundaryZone, Nothing}...; zone.zone_origin, zone.zone_width, zone.flow_direction, - zone.plane_normal, + zone.face_normal, nothing, zone.average_inflow_velocity, zone.prescribed_density, @@ -304,8 +304,8 @@ end relative_position = current_coords(u, system, particle) - boundary_zone.zone_origin # Check if particle is in- or outside the fluid domain. - # `plane_normal` is always pointing into the fluid domain. - if signbit(dot(relative_position, boundary_zone.plane_normal)) + # `face_normal` is always pointing into the fluid domain. + if signbit(dot(relative_position, boundary_zone.face_normal)) deactivate_particle!(system, particle, u) return system diff --git a/test/general/buffer.jl b/test/general/buffer.jl index fbfefc8656..28fa1748e0 100644 --- a/test/general/buffer.jl +++ b/test/general/buffer.jl @@ -4,8 +4,8 @@ TrixiParticles.initial_smoothing_length(system::FluidSystemMock3) = 1.0 TrixiParticles.nparticles(system::FluidSystemMock3) = 1 - zone = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.2, - open_boundary_layers=2, density=1.0, plane_normal=[1.0, 0.0], + zone = BoundaryZone(; boundary_face=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.2, + open_boundary_layers=2, density=1.0, face_normal=[1.0, 0.0], reference_density=1.0, reference_pressure=0.0, reference_velocity=[0, 0], boundary_type=InFlow()) system = OpenBoundarySystem(zone; fluid_system=FluidSystemMock3(), diff --git a/test/preprocessing/data/inflow.stl b/test/preprocessing/data/inflow.stl new file mode 100644 index 0000000000000000000000000000000000000000..028f34b16efa85673e1e306a39d4a107c512b374 GIT binary patch literal 460184 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 literal 0 HcmV?d00001 diff --git a/test/preprocessing/data/inflow_geometry.stl b/test/preprocessing/data/inflow_geometry.stl new file mode 100644 index 0000000000000000000000000000000000000000..b435306f1102836805f83f6137a28635d44b9908 GIT binary patch literal 9734 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 1000.0 * (t + sqrt(eps())) - # Plane points of open boundary - plane_points_1 = [[0.0, 0.0], [0.5, -0.5], [1.0, 0.5]] - plane_points_2 = [[0.0, 1.0], [0.2, 2.0], [2.3, 0.5]] + # Face vertices of open boundary + face_vertices_1 = [[0.0, 0.0], [0.5, -0.5], [1.0, 0.5]] + face_vertices_2 = [[0.0, 1.0], [0.2, 2.0], [2.3, 0.5]] - @testset "Points $i" for i in eachindex(plane_points_1) + @testset "Points $i" for i in eachindex(face_vertices_1) n_influenced = influenced_particles[i] - plane_points = [plane_points_1[i], plane_points_2[i]] + face_vertices = [face_vertices_1[i], face_vertices_2[i]] - plane_size = plane_points[2] - plane_points[1] + face_size = face_vertices[2] - face_vertices[1] flow_directions = [ - normalize([-plane_size[2], plane_size[1]]), - -normalize([-plane_size[2], plane_size[1]]) + normalize([-face_size[2], face_size[1]]), + -normalize([-face_size[2], face_size[1]]) ] @testset "Flow Direction $j" for j in eachindex(flow_directions) flow_direction = flow_directions[j] - inflow = BoundaryZone(; plane=plane_points, particle_spacing, density, - plane_normal=flow_direction, open_boundary_layers, + inflow = BoundaryZone(; boundary_face=face_vertices, particle_spacing, density, + face_normal=flow_direction, open_boundary_layers, boundary_type=InFlow(), reference_velocity, reference_pressure, reference_density) - outflow = BoundaryZone(; plane=plane_points, particle_spacing, density, - plane_normal=(-flow_direction), open_boundary_layers, + outflow = BoundaryZone(; boundary_face=face_vertices, particle_spacing, density, + face_normal=(-flow_direction), open_boundary_layers, boundary_type=OutFlow(), reference_velocity, reference_pressure, reference_density) @@ -54,7 +54,7 @@ sign_ = (TrixiParticles.boundary_type_name(boundary_zone) == "inflow") ? 1 : -1 - fluid = extrude_geometry(plane_points; particle_spacing, n_extrude=4, + fluid = extrude_geometry(face_vertices; particle_spacing, n_extrude=4, density, pressure, direction=(sign_ * flow_direction)) diff --git a/test/schemes/boundary/open_boundary/mirroring.jl b/test/schemes/boundary/open_boundary/mirroring.jl index 40832e539a..fc55c4411f 100644 --- a/test/schemes/boundary/open_boundary/mirroring.jl +++ b/test/schemes/boundary/open_boundary/mirroring.jl @@ -11,11 +11,11 @@ particle_spacing = 0.05 domain_length = 1.0 - plane_boundary = [ + boundary_faces = [ ([0.0, 0.0], [0.0, domain_length]), ([0.0, 0.0], [-domain_length, domain_length]) ] - plane_boundary_normal = [[1.0, 0.0], [1.0, 1.0]] + boundary_face_normal = [[1.0, 0.0], [1.0, 1.0]] function pressure_function(pos) t = 0 @@ -54,9 +54,9 @@ fluid_system.cache.density .= domain_fluid.density - @testset verbose=true "plane normal $i" for i in eachindex(files) - inflow = BoundaryZone(; plane=plane_boundary[i], boundary_type=InFlow(), - plane_normal=plane_boundary_normal[i], + @testset verbose=true "face normal $i" for i in eachindex(files) + inflow = BoundaryZone(; boundary_face=boundary_faces[i], boundary_type=InFlow(), + face_normal=boundary_face_normal[i], average_inflow_velocity=false, open_boundary_layers=10, density=1000.0, particle_spacing) @@ -105,12 +105,12 @@ particle_spacing = 0.05 domain_length = 1.0 - plane_boundary = [ + boundary_faces = [ ([0.0, 0.0, 0.0], [domain_length, 0.0, 0.0], [0.0, domain_length, 0.0]), ([0.0, 0.0, 0.0], [domain_length, 0.0, 0.0], [0.0, domain_length, domain_length]) ] - plane_boundary_normal = [[0.0, 0.0, 1.0], [0.0, -1.0, 1.0]] + boundary_face_normal = [[0.0, 0.0, 1.0], [0.0, -1.0, 1.0]] function pressure_function(pos) t = 0 @@ -150,9 +150,9 @@ fluid_system.cache.density .= domain_fluid.density - @testset verbose=true "plane normal $i" for i in eachindex(files) - inflow = BoundaryZone(; plane=plane_boundary[i], boundary_type=InFlow(), - plane_normal=plane_boundary_normal[i], + @testset verbose=true "face normal $i" for i in eachindex(files) + inflow = BoundaryZone(; boundary_face=boundary_faces[i], boundary_type=InFlow(), + face_normal=boundary_face_normal[i], average_inflow_velocity=false, open_boundary_layers=10, density=1000.0, particle_spacing) @@ -218,14 +218,14 @@ fluid_system.cache.density .= 1000.0 if i == 2 - plane_in = ([0.0, 0.0], [0.0, domain_length]) + face_in = ([0.0, 0.0], [0.0, domain_length]) else - plane_in = ([0.0, 0.0, 0.0], [0.0, domain_length, 0.0], - [0.0, 0.0, domain_length]) + face_in = ([0.0, 0.0, 0.0], [0.0, domain_length, 0.0], + [0.0, 0.0, domain_length]) end - inflow = BoundaryZone(; plane=plane_in, boundary_type=InFlow(), - plane_normal=(i == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]), + inflow = BoundaryZone(; boundary_face=face_in, boundary_type=InFlow(), + face_normal=(i == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]), open_boundary_layers=open_boundary_layers, density=1000.0, particle_spacing, average_inflow_velocity=true) open_boundary_in = OpenBoundarySystem(inflow; fluid_system, @@ -274,10 +274,10 @@ fluid_system.cache.density .= domain_fluid.density - plane_out = ([domain_size[1], 0.0], [domain_size[1], domain_size[2]]) + face_out = ([domain_size[1], 0.0], [domain_size[1], domain_size[2]]) - outflow = BoundaryZone(; plane=plane_out, boundary_type=OutFlow(), - plane_normal=[-1.0, 0.0], + outflow = BoundaryZone(; boundary_face=face_out, boundary_type=OutFlow(), + face_normal=[-1.0, 0.0], open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary_out = OpenBoundarySystem(outflow; fluid_system, @@ -299,10 +299,10 @@ outflow.initial_condition.coordinates, domain_fluid.coordinates, semi) - plane_in = ([0.0, 0.0], [0.0, domain_size[2]]) + face_in = ([0.0, 0.0], [0.0, domain_size[2]]) - inflow = BoundaryZone(; plane=plane_in, boundary_type=InFlow(), - plane_normal=[1.0, 0.0], + inflow = BoundaryZone(; boundary_face=face_in, boundary_type=InFlow(), + face_normal=[1.0, 0.0], open_boundary_layers=10, density=1000.0, particle_spacing) open_boundary_in = OpenBoundarySystem(inflow; fluid_system, boundary_model=BoundaryModelMirroringTafuni(), diff --git a/test/systems/open_boundary_system.jl b/test/systems/open_boundary_system.jl index 24086395b3..f864208004 100644 --- a/test/systems/open_boundary_system.jl +++ b/test/systems/open_boundary_system.jl @@ -6,8 +6,9 @@ TrixiParticles.initial_smoothing_length(system::FluidSystemMock2) = 1.0 TrixiParticles.nparticles(system::FluidSystemMock2) = 1 - inflow = BoundaryZone(; plane=([0.0, 0.0], [0.0, 1.0]), particle_spacing=0.05, - plane_normal=(1.0, 0.0), density=1.0, + inflow = BoundaryZone(; boundary_face=([0.0, 0.0], [0.0, 1.0]), + particle_spacing=0.05, + face_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=InFlow()) system = OpenBoundarySystem(inflow; buffer_size=0, boundary_model=BoundaryModelCharacteristicsLastiwka(), @@ -28,8 +29,9 @@ @test repr("text/plain", system) == show_box - outflow = BoundaryZone(; plane=([5.0, 0.0], [5.0, 1.0]), particle_spacing=0.05, - plane_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, + outflow = BoundaryZone(; boundary_face=([5.0, 0.0], [5.0, 1.0]), + particle_spacing=0.05, + face_normal=(1.0, 0.0), density=1.0, open_boundary_layers=4, boundary_type=OutFlow()) system = OpenBoundarySystem(outflow; buffer_size=0, boundary_model=BoundaryModelMirroringTafuni(), From 921bafc21b5beadb7c25e6ed9ebf3befbcd7cf18 Mon Sep 17 00:00:00 2001 From: Niklas Neher <73897120+LasNikas@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:22:13 +0200 Subject: [PATCH 14/15] Fix GPU tests, GPU interpolation and GPU postprocessing (#912) * fix gpu * rm saving callback * implement suggestions * rm solution_saving=nothing --------- Co-authored-by: LasNikas Co-authored-by: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> --- src/callbacks/post_process.jl | 6 ++++-- src/general/interpolation.jl | 5 +++++ src/io/io.jl | 8 +------- src/io/write_vtk.jl | 16 ++++++++++++---- test/examples/gpu.jl | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/callbacks/post_process.jl b/src/callbacks/post_process.jl index 638ca9aa61..5f5256d96f 100644 --- a/src/callbacks/post_process.jl +++ b/src/callbacks/post_process.jl @@ -252,8 +252,10 @@ function (pp::PostprocessCallback)(integrator) system_index = system_indices(system, semi) for (key, f) in pp.func - result = custom_quantity(f, system, dv_ode, du_ode, v_ode, u_ode, semi, t) - if result !== nothing + result_ = custom_quantity(f, system, dv_ode, du_ode, v_ode, u_ode, semi, t) + if result_ !== nothing + # Transfer to CPU if data is on the GPU. Do nothing if already on CPU. + result = transfer2cpu(result_) add_entry!(pp, string(key), t, result, filenames[system_index]) new_data = true end diff --git a/src/general/interpolation.jl b/src/general/interpolation.jl index c6cb8a1319..bf26f80906 100644 --- a/src/general/interpolation.jl +++ b/src/general/interpolation.jl @@ -628,6 +628,11 @@ end return (; velocity, jacobian, von_mises_stress, cauchy_stress) end +function interpolate_system!(cache, v, neighbor_system, + point, neighbor, volume_b, W_ab, clip_negative_pressure) + return cache +end + @inline function interpolate_system!(cache, v, system::AbstractFluidSystem, point, neighbor, volume_b, W_ab, clip_negative_pressure) diff --git a/src/io/io.jl b/src/io/io.jl index d8c075c523..ed245a4c7e 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -135,12 +135,8 @@ function add_system_data!(system_data, system::OpenBoundarySystem) system_data["system_type"] = type2string(system) system_data["fluid_system_index"] = system.fluid_system_index[] system_data["smoothing_length"] = system.smoothing_length + system_data["number_of_boundary_zones"] = length(system.boundary_zones) add_system_data!(system_data, system.boundary_model) - - system_data["boundary_zones"] = Dict{String, Any}() - for (indice, boundary_zone) in enumerate(system.boundary_zones) - add_system_data!(system_data["boundary_zones"], boundary_zone, indice) - end end function add_system_data!(system_data, system::ParticlePackingSystem) @@ -299,8 +295,6 @@ function add_system_data!(system_data, motion::PrescribedMotion) system_data["prescribed_motion"] = Dict{String, Any}() system_data["prescribed_motion"]["model"] = type2string(motion) system_data["prescribed_motion"]["movement_function"] = type2string(motion.movement_function) - system_data["prescribed_motion"]["is_moving"] = type2string(motion.is_moving) - system_data["prescribed_motion"]["moving_particles"] = motion.moving_particles end function add_system_data!(system_data, penalty_force::PenaltyForceGanzenmueller) diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl index 303c8aa5d7..f1baa51671 100644 --- a/src/io/write_vtk.jl +++ b/src/io/write_vtk.jl @@ -140,8 +140,8 @@ function trixi2vtk(system_, dvdu_ode_, vu_ode_, semi_, t, periodic_box; # Extract custom quantities for this system if !isempty(custom_quantities) - dv_ode, du_ode = dvdu_ode_.x - dv_ode, du_ode = transfer2cpu(dv_ode, du_ode) + dv_ode_, du_ode_ = dvdu_ode_.x + dv_ode, du_ode = transfer2cpu(dv_ode_, du_ode_) for (key, quantity) in custom_quantities value = custom_quantity(quantity, system, dv_ode, du_ode, v_ode, u_ode, @@ -173,8 +173,8 @@ function transfer2cpu(v_, u_, system_, semi_) end function transfer2cpu(v_::AbstractGPUArray, u_) - v = Adapt.adapt(Array, v_) - u = Adapt.adapt(Array, u_) + v = transfer2cpu(v_) + u = transfer2cpu(u_) return v, u end @@ -183,6 +183,14 @@ function transfer2cpu(v_, u_) return v_, u_ end +function transfer2cpu(a_::AbstractGPUArray) + return Adapt.adapt(Array, a_) +end + +function transfer2cpu(a_) + return a_ +end + function custom_quantity(quantity::AbstractArray, system, dv_ode, du_ode, v_ode, u_ode, semi, t) return quantity diff --git a/test/examples/gpu.jl b/test/examples/gpu.jl index 7ac1f663f6..3acba3e36d 100644 --- a/test/examples/gpu.jl +++ b/test/examples/gpu.jl @@ -453,7 +453,7 @@ end # Neighborhood search with `FullGridCellList` for GPU compatibility min_corner = minimum(tank.boundary.coordinates, dims=2) max_corner = maximum(tank.boundary.coordinates, dims=2) - max_corner[2] = gate_height + movement_function(0.1)[2] + max_corner[2] = gate_height + movement_function([0, 0], 0.1f0)[2] # We need a very high `max_points_per_cell` because the plate resolution # is much finer than the fluid resolution. cell_list = FullGridCellList(; min_corner, max_corner) From e60a7546c83d5c28aa178c3ebe25b53e48b7e653 Mon Sep 17 00:00:00 2001 From: Erik Faulhaber <44124897+efaulhaber@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:37:19 +0200 Subject: [PATCH 15/15] Update version number in Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9c8aa85fb3..8ec0bdad92 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TrixiParticles" uuid = "66699cd8-9c01-4e9d-a059-b96c86d16b3a" authors = ["erik.faulhaber <44124897+efaulhaber@users.noreply.github.com>"] -version = "0.3.2" +version = "0.4" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"

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 z 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 z