diff --git a/NEWS.md b/NEWS.md index 8abe050384..5220c74632 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,11 +4,39 @@ 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.5.3 - -### Features - -- Added the computation of boundary normals for `RectangularTank`s and `SphereShape`s. +## Version 0.5.3 + +### API Changes + +- Corrected `SurfaceTensionMorris` to apply its local CSF acceleration once per particle and + retain the required one-phase surface delta. Previous coefficients compensated implicitly + for a dimensionally incomplete force repeated once per fluid neighbor and must be recalibrated. +- For `SurfaceTensionMorris` and `SurfaceTensionMomentumMorris` with + `ColorfieldSurfaceNormal`, `ideal_density_threshold` now denotes a fraction of the continuous + complete-support kernel moment instead of an integer neighbor-count fraction. The default zero + still disables interior filtering. + +### Features + +- Reworked `SurfaceTensionMomentumMorris` as a balanced continuum-surface-stress operator with a + symmetric support correction. It conserves pairwise linear momentum without a cached stress + tensor or global reduction. +- Added opt-in activity-weighted Shepard normal smoothing for Morris CSF/CSS and expanded VTK + output with raw and capillary normals, surface delta, activity, support, force, and reconstructed + stress diagnostics. +- Added `FreeSurfaceTangentialShifting`, an opt-in treatment that smoothly removes the + interface-normal component of Sun particle shifting while retaining full interior shifting. +- Added `InterfaceAwareTensileInstabilityControl`, an opt-in pressure formulation that applies + tensile-instability control in fluid interiors and smoothly disables it at free surfaces. +- Added optional per-particle `surface_measure` quadrature data to dummy-particle boundaries. +- Added `WettedAreaContactAngle`, an opt-in Young wall-energy model for supported 3D Morris CSS + simulations with force- and torque-conserving fixed-wall and rigid-body reactions. +- Added C1 interface activation for Morris CSF and CSS with `ColorfieldSurfaceNormal`. + Color-gradient and continuous support-moment indicators taper the physical surface delta without + another neighbor pass. +- Added `CorrectedCSFSurfaceNormal`, an explicit free-surface implementation of the C-CSF + interface geometry from Vergnaud et al. (2022) for `SurfaceTensionMorris`. +- Added the computation of boundary normals for `RectangularTank`s and `SphereShape`s. - Added `flush` keyword argument to `InfoCallback` to flush `stdout` after each output, useful for monitoring progress in real-time on clusters or batch systems (#1246). - Added the number of split integration time steps to the `InfoCallback` output diff --git a/README.md b/README.md index bf3c1c9cbc..415b72c23f 100644 --- a/README.md +++ b/README.md @@ -45,24 +45,24 @@ It offers intuitive configuration, robust pre- and post-processing, and vendor-a We provide several example simulation setups in the `examples` folder (which can be accessed from Julia via `examples_dir()`).
|
- + |
- + |
|
- + |
- + |
|
- + |
- + |
|
- + |
- + |
+```
+
+Load 2D geometry files, fill them with particles using `ComplexShape`, and build genuine 2D setups such as a curved pipe and a coastline dam break.
+
+- Focus: `load_geometry`, `ComplexShape`, `setdiff`, 2D `Polygon`s
+- Choose this if: you want a true 2D setup from line-based geometry data
+
### [Particle packing tutorial](tutorials/tut_packing.md)
```@raw html
diff --git a/docs/src/visualization.md b/docs/src/visualization.md
index 9dda94bfba..ac58596792 100644
--- a/docs/src/visualization.md
+++ b/docs/src/visualization.md
@@ -1,6 +1,6 @@
# Visualization
-## Export VTK files
+## Export VTK Files
You can export particle data as VTK files by using the [`SolutionSavingCallback`](@ref).
All [predefined examples](examples.md) already use this callback to export VTK files to the `out`
directory relative to the current working directory.
@@ -18,6 +18,7 @@ Follow these steps to view the exported VTK files in ParaView:
5. Hold the left mouse button to move the solution around.
You will now see the following:
+

It is useful to make the dot size dependent on the actual particle size.
@@ -28,9 +29,10 @@ Then, in the Properties panel (bottom left), adjust the following settings:
3. Activate "Scale by Array" and select "`particle_spacing`" in "Gaussian Scale Array".
4. Deactivate "Use Scale Function".
5. Set the "Gaussian Radius" to "`0.5`".
+

-#### Visualization with Macro
+#### Visualization with a Macro
To simplify visualization of particle data in ParaView, you can use a macro.
It reduces the manual steps from the previous section to a single click.
Install the macro as follows.
@@ -44,7 +46,6 @@ Install the macro as follows.
6. Click on the macro name in the **Macros** menu (or toolbar, if pinned) to run it.
7. The Point Gaussian representation with `particle_spacing` scaling will be applied automatically.
-
---
#### Macro Code
@@ -78,11 +79,12 @@ sourceDisplay.UseScaleFunction = 0
sourceDisplay.GaussianRadius = 0.5
```
-#### Show results
+#### Show Results
To view the result variables, first make sure that "fluid_1.pvd" is highlighted in the
"Pipeline Browser", then select a variable in the variable-selection combo box
(see the image below). For example, choose "density". To view the time evolution,
press the play button (also shown below).
+

## API
diff --git a/examples/fluid/dam_break_2d.jl b/examples/fluid/dam_break_2d.jl
index 11ee987599..22969a427c 100644
--- a/examples/fluid/dam_break_2d.jl
+++ b/examples/fluid/dam_break_2d.jl
@@ -80,12 +80,8 @@ viscosity_wall = nothing
# viscosity_wall = viscosity_fluid
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
- correction=nothing,
- reference_particle_spacing=0,
viscosity=viscosity_wall,
clip_negative_pressure=true)
diff --git a/examples/fluid/dam_break_3d.jl b/examples/fluid/dam_break_3d.jl
index ae9b580960..0e5438ac53 100644
--- a/examples/fluid/dam_break_3d.jl
+++ b/examples/fluid/dam_break_3d.jl
@@ -57,10 +57,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid; smoothing_kernel, smoothi
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fluid/falling_water_column_2d.jl b/examples/fluid/falling_water_column_2d.jl
index 694c13ab76..c9c4b00fa5 100644
--- a/examples/fluid/falling_water_column_2d.jl
+++ b/examples/fluid/falling_water_column_2d.jl
@@ -55,10 +55,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid; smoothing_kernel, smoothi
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fluid/falling_water_spheres_2d.jl b/examples/fluid/falling_water_spheres_2d.jl
index 1f0016ac6d..657f496a48 100644
--- a/examples/fluid/falling_water_spheres_2d.jl
+++ b/examples/fluid/falling_water_spheres_2d.jl
@@ -82,12 +82,11 @@ boundary_density_calculator = AdamiPressureExtrapolation()
wall_viscosity = nu
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary;
+ fluid_system=sphere_surface_tension,
boundary_density_calculator,
- fluid_smoothing_kernel, fluid_smoothing_length;
state_equation,
viscosity=ViscosityAdami(nu=wall_viscosity),
- reference_particle_spacing=fluid_particle_spacing,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model;
diff --git a/examples/fluid/hydrostatic_water_column_2d.jl b/examples/fluid/hydrostatic_water_column_2d.jl
index 58b381c0c0..93e6665c8f 100644
--- a/examples/fluid/hydrostatic_water_column_2d.jl
+++ b/examples/fluid/hydrostatic_water_column_2d.jl
@@ -60,10 +60,8 @@ boundary_density_calculator = AdamiPressureExtrapolation()
# This is to set wall viscosity with `trixi_include`
viscosity_wall = nothing
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
viscosity=viscosity_wall)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model,
prescribed_motion=nothing)
diff --git a/examples/fluid/lid_driven_cavity_2d.jl b/examples/fluid/lid_driven_cavity_2d.jl
index c1aa4ae2a8..747caf2763 100644
--- a/examples/fluid/lid_driven_cavity_2d.jl
+++ b/examples/fluid/lid_driven_cavity_2d.jl
@@ -84,20 +84,14 @@ is_moving(t) = true
lid_movement = PrescribedMotion(lid_movement_function, is_moving)
-boundary_model_cavity = BoundaryModelDummyParticles(cavity.boundary.density,
- cavity.boundary.mass,
- AdamiPressureExtrapolation(),
- smoothing_kernel, smoothing_length;
- viscosity, state_equation)
-
-boundary_model_lid = BoundaryModelDummyParticles(lid.density, lid.mass,
- AdamiPressureExtrapolation(),
- smoothing_kernel, smoothing_length;
- viscosity, state_equation)
-
-boundary_system_cavity = WallBoundarySystem(cavity.boundary, boundary_model_cavity)
-
-boundary_system_lid = WallBoundarySystem(lid, boundary_model_lid,
+cavity_boundary_model = BoundaryModelDummyParticles(cavity.boundary;
+ fluid_system=fluid_system,
+ viscosity=viscosity)
+boundary_system_cavity = WallBoundarySystem(cavity.boundary, cavity_boundary_model)
+
+lid_boundary_model = BoundaryModelDummyParticles(lid; fluid_system=fluid_system,
+ viscosity=viscosity)
+boundary_system_lid = WallBoundarySystem(lid, lid_boundary_model,
prescribed_motion=lid_movement)
# ==========================================================================================
diff --git a/examples/fluid/moving_wall_2d.jl b/examples/fluid/moving_wall_2d.jl
index 6e0b0f9dfe..253386981c 100644
--- a/examples/fluid/moving_wall_2d.jl
+++ b/examples/fluid/moving_wall_2d.jl
@@ -59,10 +59,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid; smoothing_kernel, smoothi
# ==========================================================================================
# ==== Boundary
boundary_density_calculator = AdamiPressureExtrapolation()
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
- boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation)
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
+ boundary_density_calculator)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model,
prescribed_motion=boundary_movement)
diff --git a/examples/fluid/periodic_array_of_cylinders_2d.jl b/examples/fluid/periodic_array_of_cylinders_2d.jl
index f2899b6bfd..ec1344eafc 100644
--- a/examples/fluid/periodic_array_of_cylinders_2d.jl
+++ b/examples/fluid/periodic_array_of_cylinders_2d.jl
@@ -71,10 +71,8 @@ fluid_system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_le
# ==========================================================================================
# ==== Boundary
-boundary_model = BoundaryModelDummyParticles(boundary.density, boundary.mass,
- AdamiPressureExtrapolation(), smoothing_kernel,
- smoothing_length;
- viscosity=ViscosityAdami(; nu), state_equation)
+boundary_model = BoundaryModelDummyParticles(boundary; fluid_system=fluid_system,
+ viscosity=ViscosityAdami(; nu))
boundary_system = WallBoundarySystem(boundary, boundary_model)
diff --git a/examples/fluid/periodic_channel_2d.jl b/examples/fluid/periodic_channel_2d.jl
index 9635f48a78..097e9b5f16 100644
--- a/examples/fluid/periodic_channel_2d.jl
+++ b/examples/fluid/periodic_channel_2d.jl
@@ -59,10 +59,8 @@ viscosity_wall = nothing
# Activate to switch to no-slip walls
#viscosity_wall = ViscosityAdami(nu=0.0025 * smoothing_length * sound_speed / 8)
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
viscosity=viscosity_wall)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fluid/pipe_flow_2d.jl b/examples/fluid/pipe_flow_2d.jl
index 6022b980e4..1539d9c0f0 100644
--- a/examples/fluid/pipe_flow_2d.jl
+++ b/examples/fluid/pipe_flow_2d.jl
@@ -136,16 +136,13 @@ outflow = BoundaryZone(; boundary_face=face_out, face_normal=(-flow_direction),
initial_condition=outlet.fluid, boundary_type=boundary_type_out)
open_boundary = OpenBoundarySystem(inflow, outflow; fluid_system,
- boundary_model=open_boundary_model,
- buffer_size=n_buffer_particles)
+ boundary_model=open_boundary_model)
# ==========================================================================================
# ==== Boundary
wall = union(pipe.boundary, inlet.boundary, outlet.boundary)
viscosity_boundary = viscosity
-boundary_model = BoundaryModelDummyParticles(wall.density, wall.mass,
- AdamiPressureExtrapolation(), smoothing_kernel,
- smoothing_length; state_equation,
+boundary_model = BoundaryModelDummyParticles(wall; fluid_system=fluid_system,
viscosity=viscosity_boundary)
boundary_system = WallBoundarySystem(wall, boundary_model)
diff --git a/examples/fluid/poiseuille_flow_2d.jl b/examples/fluid/poiseuille_flow_2d.jl
index ba36a2f471..175571a8f2 100644
--- a/examples/fluid/poiseuille_flow_2d.jl
+++ b/examples/fluid/poiseuille_flow_2d.jl
@@ -145,16 +145,14 @@ outlet_boundary_zone = BoundaryZone(; boundary_face=outlet_face,
open_boundary = OpenBoundarySystem(inlet_boundary_zone, outlet_boundary_zone; fluid_system,
boundary_model=open_boundary_model,
- calculate_flow_rate=true,
- buffer_size=n_buffer_particles)
+ calculate_flow_rate=true)
# ==========================================================================================
# ==== Boundary
wall_boundary = union(channel.boundary)
-boundary_model = BoundaryModelDummyParticles(wall_boundary.density, wall_boundary.mass,
- AdamiPressureExtrapolation(), smoothing_kernel,
- smoothing_length; state_equation, viscosity)
+boundary_model = BoundaryModelDummyParticles(wall_boundary; fluid_system=fluid_system,
+ viscosity)
boundary_system = WallBoundarySystem(wall_boundary, boundary_model)
diff --git a/examples/fluid/poiseuille_flow_3d.jl b/examples/fluid/poiseuille_flow_3d.jl
index 4459df0648..7d76cc8ffb 100644
--- a/examples/fluid/poiseuille_flow_3d.jl
+++ b/examples/fluid/poiseuille_flow_3d.jl
@@ -162,14 +162,12 @@ outlet_zone = BoundaryZone(; boundary_face=outlet_face,
boundary_type=outlet_boundary_type)
open_boundary = OpenBoundarySystem(inlet_zone, outlet_zone; fluid_system,
- boundary_model=open_boundary_model,
- buffer_size=n_buffer_particles)
+ boundary_model=open_boundary_model)
# ==========================================================================================
# ==== Boundary
-boundary_model = BoundaryModelDummyParticles(wall_boundary.density, wall_boundary.mass,
- AdamiPressureExtrapolation(), smoothing_kernel,
- smoothing_length; state_equation, viscosity)
+boundary_model = BoundaryModelDummyParticles(wall_boundary; fluid_system=fluid_system,
+ viscosity)
boundary_system = WallBoundarySystem(wall_boundary, boundary_model)
diff --git a/examples/fsi/dam_break_gate_2d.jl b/examples/fsi/dam_break_gate_2d.jl
index 49d72f2aee..9cc9e66271 100644
--- a/examples/fsi/dam_break_gate_2d.jl
+++ b/examples/fsi/dam_break_gate_2d.jl
@@ -120,20 +120,15 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid; smoothing_kernel, smoothi
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model_tank = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+tank_boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
clip_negative_pressure=true)
-
-boundary_model_gate = BoundaryModelDummyParticles(gate.density, gate.mass,
+gate_boundary_model = BoundaryModelDummyParticles(gate; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
clip_negative_pressure=true)
-boundary_system_tank = WallBoundarySystem(tank.boundary, boundary_model_tank)
-boundary_system_gate = WallBoundarySystem(gate, boundary_model_gate,
+boundary_system_tank = WallBoundarySystem(tank.boundary, tank_boundary_model)
+boundary_system_gate = WallBoundarySystem(gate, gate_boundary_model,
prescribed_motion=gate_movement)
# ==========================================================================================
diff --git a/examples/fsi/dam_break_plate_2d.jl b/examples/fsi/dam_break_plate_2d.jl
index 00edc5c216..2bfd4f609e 100644
--- a/examples/fsi/dam_break_plate_2d.jl
+++ b/examples/fsi/dam_break_plate_2d.jl
@@ -94,10 +94,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid; smoothing_kernel, smoothi
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length;
- state_equation,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fsi/falling_rigid_spheres_2d.jl b/examples/fsi/falling_rigid_spheres_2d.jl
index 7eaa2b4dcb..59c05ce448 100644
--- a/examples/fsi/falling_rigid_spheres_2d.jl
+++ b/examples/fsi/falling_rigid_spheres_2d.jl
@@ -70,10 +70,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid;
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- fluid_smoothing_kernel, fluid_smoothing_length;
- state_equation,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fsi/falling_rotating_rigid_squares_2d.jl b/examples/fsi/falling_rotating_rigid_squares_2d.jl
index 4a14905cf0..0ab46fa9fd 100644
--- a/examples/fsi/falling_rotating_rigid_squares_2d.jl
+++ b/examples/fsi/falling_rotating_rigid_squares_2d.jl
@@ -86,10 +86,9 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid;
boundary_density_calculator = AdamiPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- fluid_smoothing_kernel, fluid_smoothing_length;
- state_equation, clip_negative_pressure=true)
+ clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fsi/falling_spheres_2d.jl b/examples/fsi/falling_spheres_2d.jl
index 035f9ad3da..136161624f 100644
--- a/examples/fsi/falling_spheres_2d.jl
+++ b/examples/fsi/falling_spheres_2d.jl
@@ -74,10 +74,8 @@ fluid_system = WeaklyCompressibleSPHSystem(tank.fluid;
boundary_density_calculator = BernoulliPressureExtrapolation()
# Clip negative boundary pressure values to avoid sticking artifacts at the boundary.
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- fluid_smoothing_kernel, fluid_smoothing_length;
- state_equation,
clip_negative_pressure=true)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
diff --git a/examples/fsi/hydrostatic_water_column_2d.jl b/examples/fsi/hydrostatic_water_column_2d.jl
index 9fd7e15a36..3815a28d68 100644
--- a/examples/fsi/hydrostatic_water_column_2d.jl
+++ b/examples/fsi/hydrostatic_water_column_2d.jl
@@ -113,9 +113,8 @@ else
damping_coefficient=0.05))
end
-boundary_model = BoundaryModelDummyParticles(tank.boundary.density, tank.boundary.mass,
+boundary_model = BoundaryModelDummyParticles(tank.boundary; fluid_system=fluid_system,
boundary_density_calculator,
- smoothing_kernel, smoothing_length_fluid;
state_equation)
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
boundary_model_structure = BoundaryModelDummyParticles(hydrodynamic_densities,
diff --git a/examples/preprocessing/complex_shape_2d.jl b/examples/preprocessing/complex_shape_2d.jl
index fa2b762988..62de84ad49 100644
--- a/examples/preprocessing/complex_shape_2d.jl
+++ b/examples/preprocessing/complex_shape_2d.jl
@@ -7,8 +7,8 @@
# 3. Utilize the Winding Number algorithm to determine if points are inside or outside.
# 4. Visualize the sampled particles and the winding number field.
#
-# The example uses an "inverted_open_curve" geometry, where standard inside/outside
-# definitions might be ambiguous without a robust point-in-polygon test like winding numbers.
+# The example uses a polygonal star geometry, where standard inside/outside
+# definitions benefit from a robust point-in-polygon test like winding numbers.
# ==========================================================================================
using TrixiParticles
@@ -16,7 +16,7 @@ using Plots
particle_spacing = 0.05
-filename = "inverted_open_curve"
+filename = "star"
file = joinpath("examples", "preprocessing", "data", filename * ".asc")
geometry = load_geometry(file)
@@ -24,7 +24,6 @@ geometry = load_geometry(file)
trixi2vtk(geometry)
point_in_geometry_algorithm = WindingNumberJacobson(; geometry,
- winding_number_factor=0.4,
hierarchical_winding=true)
# Returns `InitialCondition`
diff --git a/examples/preprocessing/data/coastline_profile_2d.asc b/examples/preprocessing/data/coastline_profile_2d.asc
new file mode 100644
index 0000000000..c250c87be4
--- /dev/null
+++ b/examples/preprocessing/data/coastline_profile_2d.asc
@@ -0,0 +1,20 @@
+# ASCII
+0.15 -0.12 0
+2.68 -0.12 0
+2.68 1.08 0
+2.62 0.66 0
+2.53 0.52 0
+2.42 0.40 0
+2.30 0.42 0
+2.18 0.33 0
+2.05 0.24 0
+1.92 0.26 0
+1.78 0.18 0
+1.62 0.11 0
+1.46 0.14 0
+1.28 0.06 0
+1.05 0.02 0
+0.82 0.05 0
+0.55 0.03 0
+0.15 0.03 0
+0.15 -0.12 0
diff --git a/examples/preprocessing/data/curved_pipe_channel_2d.asc b/examples/preprocessing/data/curved_pipe_channel_2d.asc
new file mode 100644
index 0000000000..76a1d7b8fe
--- /dev/null
+++ b/examples/preprocessing/data/curved_pipe_channel_2d.asc
@@ -0,0 +1,20 @@
+# ASCII
+0.00 0.12 0
+0.60 0.12 0
+0.72423 0.13646 0
+0.84000 0.18431 0
+0.93941 0.26059 0
+1.01569 0.36000 0
+1.06354 0.47577 0
+1.08 0.60 0
+1.08 1.20 0
+0.72 1.20 0
+0.72 0.60 0
+0.71591 0.56894 0
+0.70392 0.54000 0
+0.68485 0.51515 0
+0.66000 0.49608 0
+0.63106 0.48409 0
+0.60 0.48 0
+0.00 0.48 0
+0.00 0.12 0
diff --git a/examples/preprocessing/data/curved_pipe_outer_2d.asc b/examples/preprocessing/data/curved_pipe_outer_2d.asc
new file mode 100644
index 0000000000..1b26afac92
--- /dev/null
+++ b/examples/preprocessing/data/curved_pipe_outer_2d.asc
@@ -0,0 +1,14 @@
+# ASCII
+0.00 0.00 0
+0.60 0.00 0
+0.75529 0.02044 0
+0.90000 0.08038 0
+1.02426 0.17574 0
+1.11962 0.30000 0
+1.17956 0.44471 0
+1.20 0.60 0
+1.20 1.20 0
+0.60 1.20 0
+0.60 0.60 0
+0.00 0.60 0
+0.00 0.00 0
diff --git a/examples/preprocessing/packing_2d.jl b/examples/preprocessing/packing_2d.jl
index fcf0d40c64..7055a2dd98 100644
--- a/examples/preprocessing/packing_2d.jl
+++ b/examples/preprocessing/packing_2d.jl
@@ -71,7 +71,8 @@ packing_system = ParticlePackingSystem(shape_sampled; smoothing_length,
boundary_system = ParticlePackingSystem(boundary_sampled; smoothing_length,
is_boundary=true, signed_distance_field,
- place_on_shell, boundary_compress_factor=0.8,
+ place_on_shell, boundary_thickness,
+ boundary_compress_factor=0.8,
background_pressure)
# ==========================================================================================
diff --git a/src/TrixiParticles.jl b/src/TrixiParticles.jl
index 7101be8e49..354ef9555c 100644
--- a/src/TrixiParticles.jl
+++ b/src/TrixiParticles.jl
@@ -16,12 +16,13 @@ using ForwardDiff: ForwardDiff
using GPUArraysCore: AbstractGPUArray
using JSON: JSON
using KernelAbstractions: KernelAbstractions, @kernel, @index
-using LinearAlgebra: norm, normalize, cross, dot, I, tr, inv, pinv, det
+using LinearAlgebra: norm, normalize, cross, dot, I, tr, inv, pinv, det, eigvals,
+ Symmetric
using Polyester: Polyester, @batch
using Printf: @printf, @sprintf
using ReadVTK: ReadVTK
using RecipesBase: RecipesBase, @series
-using Random: seed!
+using Random: MersenneTwister
using SciMLBase: SciMLBase, CallbackSet, DiscreteCallback, DynamicalODEProblem,
derivative_discontinuity!, get_tmp_cache, set_proposed_dt!,
ODESolution, ODEProblem, terminate!, add_tstop!
@@ -80,6 +81,7 @@ export InfoCallback, SolutionSavingCallback, DensityReinitializationCallback,
export ContinuityDensity, SummationDensity
export PenaltyForceGanzenmueller, TransportVelocityAdami, ParticleShiftingTechnique,
ParticleShiftingTechniqueSun2017, ConsistentShiftingSun2019,
+ FreeSurfaceTangentialShifting,
ContinuityEquationTermSun2019, MomentumEquationTermSun2019, VelocityAveraging
export SchoenbergCubicSplineKernel, SchoenbergQuarticSplineKernel,
SchoenbergQuinticSplineKernel, GaussianKernel, WendlandC2Kernel, WendlandC4Kernel,
@@ -88,7 +90,7 @@ export StateEquationCole, StateEquationIdealGas, StateEquationAdaptiveCole
export ArtificialViscosityMonaghan, ViscosityAdami, ViscosityMorris, ViscosityAdamiSGS,
ViscosityMorrisSGS, ViscosityCarreauYasuda
export DensityDiffusionMolteniColagrossi, DensityDiffusionFerrari, DensityDiffusionAntuono
-export tensile_instability_control
+export tensile_instability_control, InterfaceAwareTensileInstabilityControl
export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureExtrapolation,
PressureMirroring, PressureZeroing, BoundaryModelCharacteristicsLastiwka,
BoundaryModelMirroringTafuni, BoundaryModelDynamicalPressureZhang,
@@ -102,7 +104,7 @@ export trixi2vtk, vtk2trixi
export RectangularTank, RectangularShape, SphereShape, ComplexShape
export ParticlePackingSystem, SignedDistanceField
export WindingNumberHormann, WindingNumberJacobson
-export VoxelSphere, RoundSphere, reset_wall!, extrude_geometry, load_geometry,
+export VoxelSphere, RoundSphere, reset_wall!, extrude_geometry, load_geometry, delete_faces,
sample_boundary, planar_geometry_to_face
export SourceTermDamping
export ShepardKernelCorrection, KernelCorrection, AkinciFreeSurfaceCorrection,
@@ -114,7 +116,7 @@ export interpolate_line, interpolate_points, interpolate_plane_3d, interpolate_p
interpolate_plane_2d_vtk
export SurfaceTensionAkinci, CohesionForceAkinci, SurfaceTensionMorris,
SurfaceTensionMomentumMorris
-export ColorfieldSurfaceNormal
+export ColorfieldSurfaceNormal, CorrectedCSFSurfaceNormal, WettedAreaContactAngle
export SymplecticPositionVerlet
export coordinates_eltype
diff --git a/src/callbacks/density_reinit.jl b/src/callbacks/density_reinit.jl
index 430f3b5644..14f850dec9 100644
--- a/src/callbacks/density_reinit.jl
+++ b/src/callbacks/density_reinit.jl
@@ -32,7 +32,8 @@ end
Callback to reinitialize the density field when using [`ContinuityDensity`](@ref) [Panizzo2007](@cite).
-Pass `system` and the [`Semidiscretization`](@ref) containing it. The callback stores
+Pass `system` and the [`Semidiscretization`](@ref TrixiParticles.Semidiscretization)
+containing it. The callback stores
the system index and uses the corresponding system from the integrator semidiscretization
at runtime, which remains valid if [`semidiscretize`](@ref) replaces systems internally.
diff --git a/src/callbacks/mechanical_work_calculator.jl b/src/callbacks/mechanical_work_calculator.jl
index f20672af09..07895673fd 100644
--- a/src/callbacks/mechanical_work_calculator.jl
+++ b/src/callbacks/mechanical_work_calculator.jl
@@ -28,7 +28,8 @@ The accumulated value can be retrieved via [`calculated_mechanical_work`](@ref).
# Arguments
- `system`: The [`TotalLagrangianSPHSystem`](@ref) whose particles should be monitored.
-- `semi`: The [`Semidiscretization`](@ref) that contains `system`.
+- `semi`: The [`Semidiscretization`](@ref TrixiParticles.Semidiscretization)
+ that contains `system`.
# Keywords
- `interval=1`: Interval (in number of time steps) at which to compute the instantaneous power.
diff --git a/src/callbacks/stepsize.jl b/src/callbacks/stepsize.jl
index c34c3d666b..73c0cfe164 100644
--- a/src/callbacks/stepsize.jl
+++ b/src/callbacks/stepsize.jl
@@ -16,11 +16,11 @@ The step size is therefore only applied once at the beginning of the simulation.
The step size ``\Delta t`` is chosen as the minimum
```math
- \Delta t = \min(\Delta t_\eta, \Delta t_a, \Delta t_c),
+ \Delta t = \min(\Delta t_\nu, \Delta t_a, \Delta t_c),
```
where
```math
- \Delta t_\eta = 0.125 \, h^2 / \eta, \quad \Delta t_a = 0.25 \sqrt{h / \lVert g \rVert},
+ \Delta t_\nu = 0.125 \, h^2 / \nu, \quad \Delta t_a = 0.25 \sqrt{h / \lVert g \rVert},
\quad \Delta t_c = \text{CFL} \, h / c,
```
with ``\nu = \alpha h c / (2n + 4)``, where ``\alpha`` is the parameter of the viscosity
diff --git a/src/general/custom_quantities.jl b/src/general/custom_quantities.jl
index 3f1f74c637..0b28b48bdb 100644
--- a/src/general/custom_quantities.jl
+++ b/src/general/custom_quantities.jl
@@ -20,11 +20,10 @@ function kinetic_energy(system::AbstractStructureSystem,
dv_ode, du_ode, v_ode, u_ode, semi, t)
v = wrap_v(v_ode, system, semi)
mass = system.mass
- energy = zero(eltype(system))
- return sum(each_active_particle(system)) do particle
+ return sum(each_active_particle(system); init=zero(eltype(system))) do particle
v_i = current_velocity(v, system, particle)
- energy += mass[particle] * dot(v_i, v_i) / 2
+ return mass[particle] * dot(v_i, v_i) / 2
end
end
@@ -39,7 +38,7 @@ end
Returns the total mass of all particles in a system.
"""
function total_mass(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
- return sum(system.mass)
+ return sum(active_values(system.mass, system))
end
function total_mass(system::AbstractBoundarySystem, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -63,7 +62,7 @@ Returns the maximum pressure over all particles in a system.
"""
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))
+ return maximum(active_values(current_pressure(v, system), system))
end
function max_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -77,7 +76,7 @@ Returns the minimum pressure over all particles in a system.
"""
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))
+ return minimum(active_values(current_pressure(v, system), system))
end
function min_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -91,8 +90,8 @@ Returns the average pressure over all particles in a system.
"""
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)
+ pressure = active_values(current_pressure(v, system), system)
+ return sum(pressure) / length(pressure)
end
function avg_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -106,7 +105,7 @@ Returns the maximum density over all particles in a system.
"""
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))
+ return maximum(active_values(current_density(v, system), system))
end
function max_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -120,7 +119,7 @@ Returns the minimum density over all particles in a system.
"""
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))
+ return minimum(active_values(current_density(v, system), system))
end
function min_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
@@ -134,10 +133,12 @@ Returns the average_density over all particles in a system.
"""
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)
+ density = active_values(current_density(v, system), system)
+ return sum(density) / length(density)
end
function avg_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
return NaN
end
+
+@inline active_values(values, system) = view(values, each_active_particle(system))
diff --git a/src/general/semidiscretization.jl b/src/general/semidiscretization.jl
index 959ed70deb..10fca415e3 100644
--- a/src/general/semidiscretization.jl
+++ b/src/general/semidiscretization.jl
@@ -189,14 +189,17 @@ end
Create an `ODEProblem` from the semidiscretization with the specified `tspan`.
# Arguments
-- `semi`: A [`Semidiscretization`](@ref) holding the systems involved in the simulation.
+- `semi`: A [`Semidiscretization`](@ref TrixiParticles.Semidiscretization)
+ holding the systems involved in the simulation.
- `tspan`: The time span over which the simulation will be run.
# Keywords
- `restart_with`: Can be used to restart the simulation from VTK solution files (see [`SolutionSavingCallback`](@ref)).
This can be either `nothing` (default, no restart) or a `Tuple` of filenames,
- one for each system in the [`Semidiscretization`](@ref).
- The order of the filenames must match the order of the systems in the [`Semidiscretization`](@ref).
+ one for each system in the
+ [`Semidiscretization`](@ref TrixiParticles.Semidiscretization).
+ The order of the filenames must match the order of the systems in the
+ [`Semidiscretization`](@ref TrixiParticles.Semidiscretization).
Note that `semidiscretize` replaces the initial time (`tspan[1]`) with the timestamp read
from the VTK files. If the user-provided `tspan[1]` does not match the restart time,
it is adjusted and an info message is logged. If multiple files are provided, their
@@ -356,7 +359,8 @@ end
Set the initial coordinates and velocities of all systems in `semi` to the final values
in the solution `sol`.
[`semidiscretize`](@ref) has to be called again afterwards, or another
-[`Semidiscretization`](@ref) can be created with the updated systems.
+[`Semidiscretization`](@ref TrixiParticles.Semidiscretization) can be created
+with the updated systems.
# Arguments
- `semi`: The semidiscretization
diff --git a/src/io/io.jl b/src/io/io.jl
index 692dfd8528..d5ef92c83f 100644
--- a/src/io/io.jl
+++ b/src/io/io.jl
@@ -75,6 +75,17 @@ end
add_system_data!(system_data, data::Nothing) = system_data
+@inline pressure_acceleration_name(formulation) = nameof(formulation)
+@inline pressure_acceleration_name(control::InterfaceAwareTensileInstabilityControl) = nameof(typeof(control))
+
+@inline add_pressure_acceleration_data!(system_data, formulation) = system_data
+
+function add_pressure_acceleration_data!(system_data,
+ control::InterfaceAwareTensileInstabilityControl)
+ system_data["interface_aware_tic_strength"] = control.strength
+ return system_data
+end
+
function add_system_data!(system_data, system::AbstractFluidSystem)
system_data["system_type"] = type2string(system)
system_data["particle_spacing"] = particle_spacing(system, 1)
@@ -83,7 +94,9 @@ function add_system_data!(system_data, system::AbstractFluidSystem)
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)
+ formulation = system.pressure_acceleration_formulation
+ system_data["pressure_acceleration_formulation"] = pressure_acceleration_name(formulation)
+ add_pressure_acceleration_data!(system_data, 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)
@@ -105,7 +118,7 @@ function add_system_data!(system_data, system::ImplicitIncompressibleSPHSystem)
system_data["smoothing_kernel"] = type2string(system.smoothing_kernel)
system_data["smoothing_length"] = system.cache.smoothing_length
system_data["acceleration"] = system.acceleration
- system_data["pressure_acceleration_formulation"] = nameof(system.pressure_acceleration_formulation)
+ system_data["pressure_acceleration_formulation"] = pressure_acceleration_name(system.pressure_acceleration_formulation)
add_system_data!(system_data, shifting_technique(system))
add_system_data!(system_data, system.viscosity)
end
@@ -319,7 +332,23 @@ function add_system_data!(system_data, surface_normal_method::ColorfieldSurfaceN
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"]["interface_threshold"] = surface_normal_method.interface_threshold
system_data["surface_normal_method"]["ideal_density_threshold"] = surface_normal_method.ideal_density_threshold
+ system_data["surface_normal_method"]["interface_taper_start"] = surface_normal_method.interface_taper_start
+ system_data["surface_normal_method"]["support_taper_width"] = surface_normal_method.support_taper_width
+ system_data["surface_normal_method"]["normal_smoothing"] = surface_normal_method.normal_smoothing
+ contact_model = surface_normal_method.contact_model
+ system_data["surface_normal_method"]["contact_model"] = isnothing(contact_model) ?
+ nothing :
+ type2string(contact_model)
+ system_data["surface_normal_method"]["contact_angle"] = isnothing(contact_model) ?
+ nothing :
+ contact_model.contact_angle
+end
+
+function add_system_data!(system_data, surface_normal_method::CorrectedCSFSurfaceNormal)
+ system_data["surface_normal_method"] = Dict{String, Any}()
+ system_data["surface_normal_method"]["model"] = type2string(surface_normal_method)
end
function add_system_data!(system_data, boundary_zone::BoundaryZone, indice)
@@ -358,6 +387,10 @@ 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)
+ treatment = shifting_technique.free_surface_treatment
+ system_data["shifting_technique"]["free_surface_treatment"] = isnothing(treatment) ?
+ nothing :
+ type2string(treatment)
end
function add_system_data!(system_data, viscosity::ViscosityCarreauYasuda)
diff --git a/src/io/write_vtk.jl b/src/io/write_vtk.jl
index fa3fb80985..5e247f9f22 100644
--- a/src/io/write_vtk.jl
+++ b/src/io/write_vtk.jl
@@ -339,33 +339,57 @@ 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_integrated_particles(system)))
- system_coords = current_coordinates(u, system)
-
surface_tension_a = surface_tension_model(system)
- surface_tension_b = surface_tension_model(system)
- nhs = create_neighborhood_search(nothing, system, system)
-
- foreach_point_neighbor(system_coords, system_coords,
- nhs) do particle, neighbor, pos_diff, distance
- rho_a = current_density(v, system, particle)
- rho_b = current_density(v, system, neighbor)
- grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
-
- dv_surface_tension = Ref(zero(pos_diff))
- surface_tension_force!(dv_surface_tension,
- surface_tension_a, surface_tension_b,
- system, system, particle, neighbor,
- pos_diff, distance, rho_a, rho_b, grad_kernel, 1)
-
- surface_tension[1:ndims(system), particle] .+= dv_surface_tension[]
+ if surface_tension_a isa SurfaceTensionMorris
+ for particle in each_integrated_particle(system)
+ rho_a = current_density(v, system, particle)
+ velocity = current_velocity(v, system, particle)
+ acceleration = surface_tension_acceleration(surface_tension_a, system,
+ particle, rho_a, velocity)
+ surface_tension[1:ndims(system), particle] .= acceleration
+ end
+ else
+ system_coords = current_coordinates(u, system)
+ nhs = create_neighborhood_search(nothing, system, system)
+ foreach_point_neighbor(system_coords, system_coords,
+ nhs) do particle, neighbor, pos_diff, distance
+ rho_a = current_density(v, system, particle)
+ rho_b = current_density(v, system, neighbor)
+ grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+
+ dv_surface_tension = Ref(zero(pos_diff))
+ surface_tension_force!(dv_surface_tension,
+ surface_tension_a, surface_tension_a,
+ system, system, particle, neighbor,
+ pos_diff, distance, rho_a, rho_b, grad_kernel, 1)
+
+ surface_tension[1:ndims(system), particle] .+= dv_surface_tension[]
+ end
end
vtk["surface_tension"] = surface_tension
+ vtk["surface_delta"] = system.cache.delta_s
+ vtk["interface_activity"] = system.cache.interface_activity
+ vtk["surface_tension_normal"] = [surface_tension_normal(system, particle)
+ for particle in eachparticle(system)]
if system.surface_tension isa SurfaceTensionMorris
vtk["curvature"] = system.cache.curvature
+ vtk["surface_support_moment"] = system.cache.support_moment
end
if system.surface_tension isa SurfaceTensionMomentumMorris
- vtk["surface_stress_tensor"] = system.cache.stress_tensor
+ stress_tensor = zeros(eltype(system), ndims(system), ndims(system),
+ n_integrated_particles(system))
+ for particle in each_integrated_particle(system)
+ normal = surface_tension_normal(system, particle)
+ delta_s = system.cache.delta_s[particle]
+ for i in 1:ndims(system), j in 1:ndims(system)
+ stress_tensor[i, j,
+ particle] = delta_s *
+ ((i == j) - normal[i] * normal[j])
+ end
+ end
+ vtk["surface_divergence_correction"] = system.cache.divergence_correction
+ vtk["surface_stress_tensor"] = stress_tensor
end
end
diff --git a/src/preprocessing/geometries/geometries.jl b/src/preprocessing/geometries/geometries.jl
index c1c0948522..5336c10c67 100644
--- a/src/preprocessing/geometries/geometries.jl
+++ b/src/preprocessing/geometries/geometries.jl
@@ -4,6 +4,70 @@ include("io.jl")
@inline eachface(mesh) = Base.OneTo(nfaces(mesh))
+"""
+ is_closed_geometry(geometry)
+
+Return `true` if a polygon or triangle mesh forms a closed region or surface.
+"""
+function is_closed_geometry(polygon::Polygon)
+ vertex_degrees = polygon_vertex_degrees(polygon)
+
+ return !isempty(vertex_degrees) && all(==(2), values(vertex_degrees))
+end
+
+function polygon_vertex_degrees(polygon)
+ VERTEX = typeof(first(first(polygon.edge_vertices)))
+ vertex_degrees = Dict{VERTEX, Int}()
+
+ for edge in polygon.edge_vertices
+ for vertex in edge
+ vertex_degrees[vertex] = get(vertex_degrees, vertex, 0) + 1
+ end
+ end
+
+ return vertex_degrees
+end
+
+function is_closed_geometry(mesh::TriangleMesh)
+ return all(==(2), edge_face_counts(mesh))
+end
+
+function require_closed_geometry(geometry, operation)
+ is_closed_geometry(geometry) && return nothing
+
+ msg = "`$operation` requires a closed geometry. " *
+ closure_error_detail(geometry)
+
+ throw(ArgumentError(msg))
+end
+
+function closure_error_detail(polygon::Polygon)
+ invalid_vertices = count(!=(2), values(polygon_vertex_degrees(polygon)))
+
+ return "Found $invalid_vertices polygon vertices with an incident-edge count " *
+ "different from 2. If the vertices already trace a complete 2D boundary, " *
+ "construct or load the geometry with `close_curve=true`; otherwise provide " *
+ "a closed boundary."
+end
+
+function closure_error_detail(mesh::TriangleMesh)
+ invalid_edges = count(!=(2), edge_face_counts(mesh))
+
+ return "Found $invalid_edges mesh edges with an incident-face count different from 2."
+end
+
+function edge_face_counts(mesh::TriangleMesh)
+ edge_face_counts = zeros(Int, length(mesh.edge_vertices_ids))
+
+ for face_edges in mesh.face_edges_ids
+ edge_face_counts[face_edges[1]] += 1
+ edge_face_counts[face_edges[2]] += 1
+ edge_face_counts[face_edges[3]] += 1
+ end
+
+ return edge_face_counts
+end
+
function Base.setdiff(initial_condition::InitialCondition,
geometries::Union{Polygon, TriangleMesh}...)
geometry = first(geometries)
@@ -11,6 +75,7 @@ function Base.setdiff(initial_condition::InitialCondition,
if ndims(geometry) != ndims(initial_condition)
throw(ArgumentError("all passed geometries must have the same dimensionality as the initial condition"))
end
+ require_closed_geometry(geometry, "setdiff")
coords = reinterpret(reshape,
SVector{ndims(geometry), eltype(initial_condition.coordinates)},
@@ -41,6 +106,7 @@ function Base.intersect(initial_condition::InitialCondition,
if ndims(geometry) != ndims(initial_condition)
throw(ArgumentError("all passed geometries must have the same dimensionality as the initial condition"))
end
+ require_closed_geometry(geometry, "intersect")
coords = reinterpret(reshape,
SVector{ndims(geometry), eltype(initial_condition.coordinates)},
diff --git a/src/preprocessing/geometries/io.jl b/src/preprocessing/geometries/io.jl
index bbccc276ec..a7c6106827 100644
--- a/src/preprocessing/geometries/io.jl
+++ b/src/preprocessing/geometries/io.jl
@@ -1,5 +1,5 @@
"""
- load_geometry(filename; element_type=Float64)
+ load_geometry(filename; element_type=Float64, close_curve=true)
Load file and return corresponding type for [`ComplexShape`](@ref).
Supported file formats are `.stl`, `.asc` and `dxf`.
@@ -18,16 +18,20 @@ For comprehensive information about the supported file formats, refer to the doc
# Keywords
- `element_type`: Element type (default is `Float64`)
+- `close_curve`: Close 2D `.asc` and `.dxf` curves by appending the first point
+ when it is not already repeated. This assumes the vertices already
+ trace a complete, ordered boundary. Set this to `false` for intentional
+ open curves. Region sampling and classification reject open geometries.
"""
-function load_geometry(filename; element_type=Float64)
+function load_geometry(filename; element_type=Float64, close_curve=true)
ELTYPE = element_type
file_extension = splitext(filename)[end]
if file_extension == ".asc"
- geometry = load_ascii(filename; ELTYPE, skipstart=1)
+ geometry = load_ascii(filename; ELTYPE, skipstart=1, close_curve)
elseif file_extension == ".dxf"
- geometry = load_dxf(filename; ELTYPE)
+ geometry = load_dxf(filename; ELTYPE, close_curve)
elseif file_extension == ".stl"
geometry = load(FileIO.query(filename); ELTYPE)
else
@@ -37,21 +41,21 @@ function load_geometry(filename; element_type=Float64)
return geometry
end
-function load_ascii(filename; ELTYPE=Float64, skipstart=1)
+function load_ascii(filename; ELTYPE=Float64, skipstart=1, close_curve=true)
# Read the data from the ASCII file in as a matrix of coordinates.
# Ignore the first `skipstart` lines of the file (e.g. headers).
points = DelimitedFiles.readdlm(filename, ' ', ELTYPE, '\n'; skipstart)[:, 1:2]
- return Polygon(copy(points'))
+ return Polygon(copy(points'); close_curve)
end
-function load_dxf(filename; ELTYPE=Float64)
+function load_dxf(filename; ELTYPE=Float64, close_curve=true)
points = Tuple{ELTYPE, ELTYPE}[]
load_dxf!(points, filename)
- return Polygon(stack(points))
+ return Polygon(stack(points); close_curve)
end
function load_dxf!(points::Vector{Tuple{T, T}}, filename) where {T}
diff --git a/src/preprocessing/geometries/polygon.jl b/src/preprocessing/geometries/polygon.jl
index c56315b729..c2614f4571 100644
--- a/src/preprocessing/geometries/polygon.jl
+++ b/src/preprocessing/geometries/polygon.jl
@@ -8,21 +8,30 @@ struct Polygon{NDIMS, ELTYPE}
min_corner :: SVector{NDIMS, ELTYPE}
max_corner :: SVector{NDIMS, ELTYPE}
- function Polygon(vertices)
+ function Polygon(vertices; close_curve=true)
NDIMS = size(vertices, 1)
- return Polygon{NDIMS}(vertices)
+ return Polygon{NDIMS}(vertices; close_curve)
end
# Function barrier to make `NDIMS` static and therefore `SVector`s type-stable
- function Polygon{NDIMS}(vertices_) where {NDIMS}
- n_vertices = size(vertices_, 2)
+ function Polygon{NDIMS}(vertices_; close_curve=true) where {NDIMS}
ELTYPE = eltype(vertices_)
- min_corner = SVector{NDIMS}(minimum(vertices_, dims=2))
- max_corner = SVector{NDIMS}(maximum(vertices_, dims=2))
+ vertices = collect(reinterpret(reshape, SVector{NDIMS, ELTYPE}, vertices_))
- vertices = reinterpret(reshape, SVector{NDIMS, ELTYPE}, vertices_)
+ if length(vertices) < 3
+ throw(ArgumentError("polygon requires at least three vertices"))
+ end
+
+ if close_curve && !isapprox(first(vertices), last(vertices))
+ push!(vertices, first(vertices))
+ end
+
+ n_vertices = length(vertices)
+
+ min_corner = SVector([minimum(v[i] for v in vertices) for i in 1:NDIMS]...)
+ max_corner = SVector([maximum(v[i] for v in vertices) for i in 1:NDIMS]...)
# Sum over all the edges and determine if the vertices are in clockwise order
# to make sure that all normals pointing outwards.
@@ -63,6 +72,10 @@ struct Polygon{NDIMS, ELTYPE}
push!(edge_normals, edge_normal)
end
+ if length(edge_vertices) < 3
+ throw(ArgumentError("polygon requires at least three non-degenerate edges"))
+ end
+
vertex_normals = Vector{NTuple{2, SVector{NDIMS, ELTYPE}}}()
# Calculate vertex pseudo-normals.
@@ -95,6 +108,63 @@ struct Polygon{NDIMS, ELTYPE}
return new{NDIMS, ELTYPE}(vertices, edge_vertices, vertex_normals, edge_normals,
edge_vertices_ids, min_corner, max_corner)
end
+
+ function Polygon{NDIMS, ELTYPE}(vertices, edge_vertices, vertex_normals,
+ edge_normals, edge_vertices_ids,
+ min_corner, max_corner) where {NDIMS, ELTYPE}
+ return new{NDIMS, ELTYPE}(vertices, edge_vertices, vertex_normals, edge_normals,
+ edge_vertices_ids, min_corner, max_corner)
+ end
+end
+
+function vertex_normals_from_edges(edge_vertices, edge_normals)
+ VERTEX = typeof(first(first(edge_vertices)))
+ normal_sums = Dict{VERTEX, VERTEX}()
+
+ for (edge, edge_normal) in zip(edge_vertices, edge_normals)
+ for vertex in edge
+ normal_sums[vertex] = get(normal_sums, vertex, zero(edge_normal)) + edge_normal
+ end
+ end
+
+ return map(edge_vertices, edge_normals) do edge, edge_normal
+ normals = map(edge) do vertex
+ normal_sum = normal_sums[vertex]
+ normal_norm = norm(normal_sum)
+
+ return iszero(normal_norm) ? edge_normal : normal_sum / normal_norm
+ end
+
+ return Tuple(normals)
+ end
+end
+
+function rebuild_polygon_from_edges(edge_vertices, edge_normals)
+ NDIMS = length(first(edge_normals))
+ ELTYPE = eltype(first(edge_normals))
+ vertices = SVector{NDIMS, ELTYPE}[]
+ vertex_ids = Dict{SVector{NDIMS, ELTYPE}, Int}()
+
+ edge_vertices_ids = map(edge_vertices) do edge
+ v1, v2 = edge
+ id1 = get!(vertex_ids, v1) do
+ push!(vertices, v1)
+ return length(vertices)
+ end
+ id2 = get!(vertex_ids, v2) do
+ push!(vertices, v2)
+ return length(vertices)
+ end
+
+ return (id1, id2)
+ end
+
+ min_corner = SVector([minimum(v[i] for v in vertices) for i in 1:NDIMS]...)
+ max_corner = SVector([maximum(v[i] for v in vertices) for i in 1:NDIMS]...)
+ vertex_normals = vertex_normals_from_edges(edge_vertices, edge_normals)
+
+ return Polygon{NDIMS, ELTYPE}(vertices, edge_vertices, vertex_normals, edge_normals,
+ edge_vertices_ids, min_corner, max_corner)
end
function Base.show(io::IO, geometry::Polygon)
@@ -119,14 +189,23 @@ end
@inline Base.eltype(::Polygon{NDIMS, ELTYPE}) where {NDIMS, ELTYPE} = ELTYPE
-@inline function Base.deleteat!(polygon::Polygon, indices)
- (; edge_vertices, edge_normals, edge_vertices_ids) = polygon
+"""
+ delete_faces(geometry, indices)
+
+Return a geometry with the faces at `indices` removed and derived geometry data rebuilt.
+"""
+@inline function delete_faces(polygon::Polygon, indices)
+ edge_vertices = copy(polygon.edge_vertices)
+ edge_normals = copy(polygon.edge_normals)
deleteat!(edge_vertices, indices)
- deleteat!(edge_vertices_ids, indices)
deleteat!(edge_normals, indices)
- return polygon
+ if isempty(edge_vertices)
+ throw(ArgumentError("cannot delete all polygon edges"))
+ end
+
+ return rebuild_polygon_from_edges(edge_vertices, edge_normals)
end
@inline nfaces(mesh::Polygon) = length(mesh.edge_normals)
diff --git a/src/preprocessing/geometries/triangle_mesh.jl b/src/preprocessing/geometries/triangle_mesh.jl
index ede02d4682..7677784854 100644
--- a/src/preprocessing/geometries/triangle_mesh.jl
+++ b/src/preprocessing/geometries/triangle_mesh.jl
@@ -129,19 +129,16 @@ struct TriangleMesh{NDIMS, ELTYPE}
min_corner = SVector([minimum(v[i] for v in vertices) for i in 1:NDIMS]...)
max_corner = SVector([maximum(v[i] for v in vertices) for i in 1:NDIMS]...)
- for i in eachindex(edge_normals)
- # Skip zero normals, which would be normalized to `NaN` vectors.
- # The edge normals are only used for the `SignedDistanceField`, which is
- # essential for the packing.
- # Zero normals are caused by exactly or nearly duplicated faces.
- if !iszero(norm(edge_normals[i]))
- edge_normals[i] = normalize(edge_normals[i])
+ for normals in (edge_normals, vertex_normals)
+ for i in eachindex(normals)
+ normals_norm = norm(normals[i])
+ !iszero(normals_norm) && (normals[i] = normals[i] / normals_norm)
end
end
return new{NDIMS, ELTYPE}(vertices, face_vertices, face_vertices_ids,
face_edges_ids, edge_vertices_ids,
- normalize.(vertex_normals), edge_normals,
+ vertex_normals, edge_normals,
face_normals, min_corner, max_corner)
end
end
@@ -171,15 +168,19 @@ end
@inline face_normal(triangle, geometry::TriangleMesh) = geometry.face_normals[triangle]
-@inline function Base.deleteat!(mesh::TriangleMesh, indices)
- (; face_vertices, face_vertices_ids, face_edges_ids, face_normals) = mesh
+@inline function delete_faces(mesh::TriangleMesh, indices)
+ face_vertices = copy(mesh.face_vertices)
+ face_normals = copy(mesh.face_normals)
deleteat!(face_vertices, indices)
- deleteat!(face_vertices_ids, indices)
- deleteat!(face_edges_ids, indices)
deleteat!(face_normals, indices)
- return mesh
+ if isempty(face_vertices)
+ throw(ArgumentError("cannot delete all triangle mesh faces"))
+ end
+
+ vertices = collect(Iterators.flatten(face_vertices))
+ return TriangleMesh(face_vertices, face_normals, vertices)
end
@inline nfaces(mesh::TriangleMesh) = length(mesh.face_normals)
diff --git a/src/preprocessing/particle_packing/signed_distance.jl b/src/preprocessing/particle_packing/signed_distance.jl
index 01e862365f..2ef2c5eae4 100644
--- a/src/preprocessing/particle_packing/signed_distance.jl
+++ b/src/preprocessing/particle_packing/signed_distance.jl
@@ -16,11 +16,15 @@ to this surface.
distance of `abs(max_signed_distance)` to the surface of the shape
will be sampled.
- `points`: Points on which the signed distance is computed.
+ Pass a collection of static vectors or an `NDIMS`-by-`N` matrix with
+ one point per column.
When set to `nothing` (default), the bounding box of the shape will be
sampled with a uniform grid of points.
- `use_for_boundary_packing`: Set to `true` if [`SignedDistanceField`] is used to pack
a boundary [`ParticlePackingSystem`](@ref).
Use the default of `false` when packing without a boundary.
+ This requires a closed geometry, since boundary packing
+ needs a well-defined outside region.
"""
struct SignedDistanceField{ELTYPE, P, N, D}
positions :: P
@@ -38,6 +42,11 @@ function SignedDistanceField(geometry, particle_spacing;
NDIMS = ndims(geometry)
ELTYPE = eltype(particle_spacing)
+ if use_for_boundary_packing
+ require_closed_geometry(geometry,
+ "SignedDistanceField with `use_for_boundary_packing=true`")
+ end
+
sdf_factor = use_for_boundary_packing ? 2 : 1
search_radius = sdf_factor * max_signed_distance
@@ -62,9 +71,11 @@ function SignedDistanceField(geometry, particle_spacing;
min_corner; place_on_shell=true)
points = reinterpret(reshape, SVector{NDIMS, eltype(grid)}, grid)
+ else
+ points = wrap_points(points, Val(NDIMS))
end
- positions = copy(points)
+ positions = collect(points)
# This gives a performance boost for large geometries
delete_positions_in_empty_cells!(positions, nhs)
diff --git a/src/preprocessing/particle_packing/system.jl b/src/preprocessing/particle_packing/system.jl
index a4bb7e9aa7..45585541ee 100644
--- a/src/preprocessing/particle_packing/system.jl
+++ b/src/preprocessing/particle_packing/system.jl
@@ -5,6 +5,7 @@
smoothing_length=shape.particle_spacing,
smoothing_length_interpolation=smoothing_length,
is_boundary=false, boundary_compress_factor=1,
+ boundary_thickness=nothing,
neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(),
background_pressure, place_on_shell=false, fixed_system=false)
@@ -26,10 +27,6 @@ For more information on the methods, see [particle packing](@ref particle_packin
- `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`.
- The thickness of the boundary is specified by creating
- `signed_distance_field` with:
- - `use_for_boundary_packing=true`
- - `max_signed_distance=boundary_thickness`
See [`SignedDistanceField`](@ref).
- `fixed_system`: When set to `true`, the system remains static, meaning particles
will not move and the `InitialCondition` will stay unchanged.
@@ -54,6 +51,10 @@ For more information on the methods, see [particle packing](@ref particle_packin
Compression can be useful for highly convex geometries,
where the boundary volume increases significantly while the mass of the boundary particles remains constant.
Recommended values are `0.8` or `0.9`.
+- `boundary_thickness`: Thickness of the sampled boundary when `is_boundary=true`.
+ By default, this is `signed_distance_field.max_signed_distance`.
+ If [`sample_boundary`](@ref) used a smaller `boundary_thickness`
+ than the `SignedDistanceField`, pass the same value here.
"""
struct ParticlePackingSystem{S, F, NDIMS, ELTYPE <: Real, PR, C, AV,
IC, M, D, K, N, SD} <: AbstractFluidSystem{NDIMS}
@@ -106,6 +107,7 @@ function ParticlePackingSystem(shape::InitialCondition;
smoothing_length=shape.particle_spacing,
smoothing_length_interpolation=smoothing_length,
is_boundary=false, boundary_compress_factor=1,
+ boundary_thickness=nothing,
neighborhood_search=GridNeighborhoodSearch{ndims(shape)}(),
background_pressure, place_on_shell=false,
fixed_system=false)
@@ -147,10 +149,30 @@ 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 = place_on_shell ? shape.particle_spacing : shape.particle_spacing / 2
+ if isnothing(signed_distance_field)
+ fixed_system ||
+ throw(ArgumentError("`signed_distance_field` is required when `is_boundary=true`"))
+
+ shift_length = zero(ELTYPE)
+ else
+ boundary_thickness_ = isnothing(boundary_thickness) ?
+ signed_distance_field.max_signed_distance :
+ convert(ELTYPE, boundary_thickness)
+
+ if boundary_thickness_ > signed_distance_field.max_signed_distance
+ throw(ArgumentError("`boundary_thickness` is greater than " *
+ "`max_signed_distance` of `SignedDistanceField`."))
+ end
+
+ if boundary_thickness_ < zero(boundary_thickness_)
+ throw(ArgumentError("`boundary_thickness` must be non-negative"))
+ end
- shift_length = -boundary_compress_factor *
- signed_distance_field.max_signed_distance - offset
+ offset = place_on_shell ? shape.particle_spacing : shape.particle_spacing / 2
+
+ shift_length = -boundary_compress_factor *
+ boundary_thickness_ - offset
+ end
else
shift_length = place_on_shell ? zero(ELTYPE) : shape.particle_spacing / 2
end
diff --git a/src/preprocessing/point_in_poly/winding_number_hormann.jl b/src/preprocessing/point_in_poly/winding_number_hormann.jl
index a93cf789c4..9f9b849acb 100644
--- a/src/preprocessing/point_in_poly/winding_number_hormann.jl
+++ b/src/preprocessing/point_in_poly/winding_number_hormann.jl
@@ -14,6 +14,7 @@ struct WindingNumberHormann end
# https://doi.org/10.1016/S0925-7721(01)00012-8
function (point_in_poly::WindingNumberHormann)(geometry, points; store_winding_number=false)
(; edge_vertices) = geometry
+ points = wrap_points(points, Val(ndims(geometry)))
# We cannot use a `BitVector` here, as writing to a `BitVector` is not thread-safe
inpoly = fill(false, length(points))
diff --git a/src/preprocessing/point_in_poly/winding_number_jacobson.jl b/src/preprocessing/point_in_poly/winding_number_jacobson.jl
index 145a6d9a28..14a0d62cae 100644
--- a/src/preprocessing/point_in_poly/winding_number_jacobson.jl
+++ b/src/preprocessing/point_in_poly/winding_number_jacobson.jl
@@ -51,14 +51,16 @@ end
"""
WindingNumberJacobson(; geometry=nothing, winding_number_factor=sqrt(eps()),
- hierarchical_winding=false)
+ hierarchical_winding=!isnothing(geometry))
Algorithm for inside-outside segmentation of a complex geometry proposed by [Jacobson2013](@cite).
# Keywords
- `geometry`: Complex geometry returned by [`load_geometry`](@ref) and is only required when using
`hierarchical_winding=true`.
- `hierarchical_winding`: If set to `true`, an optimized hierarchical approach will be used,
- which gives a significant speedup. For further information see [Hierarchical Winding](@ref hierarchical_winding).
+ which gives a significant speedup. It defaults to `true` when `geometry`
+ is passed and `false` otherwise. For further information see
+ [Hierarchical Winding](@ref hierarchical_winding).
- `winding_number_factor`: For leaky geometries, a factor of `0.4` will give a better inside-outside segmentation.
!!! warning "Experimental Implementation"
@@ -69,7 +71,7 @@ struct WindingNumberJacobson{ELTYPE, W}
winding :: W
function WindingNumberJacobson(; geometry=nothing, winding_number_factor=sqrt(eps()),
- hierarchical_winding=true)
+ hierarchical_winding=(!isnothing(geometry)))
if hierarchical_winding && geometry isa Nothing
throw(ArgumentError("`geometry` must be of type `Polygon` (2D) or `TriangleMesh` (3D) when using hierarchical winding"))
end
@@ -104,6 +106,7 @@ end
function (point_in_poly::WindingNumberJacobson)(geometry, points;
store_winding_number=false)
(; winding_number_factor, winding) = point_in_poly
+ points = wrap_points(points, Val(ndims(geometry)))
# We cannot use a `BitVector` here, as writing to a `BitVector` is not thread-safe
inpoly = fill(false, length(points))
diff --git a/src/preprocessing/preprocessing.jl b/src/preprocessing/preprocessing.jl
index c538280bb4..1f29263cae 100644
--- a/src/preprocessing/preprocessing.jl
+++ b/src/preprocessing/preprocessing.jl
@@ -1,3 +1,19 @@
+function wrap_points(points, ::Val{NDIMS}) where {NDIMS}
+ if points isa AbstractMatrix
+ if size(points, 1) != NDIMS
+ throw(ArgumentError("point matrix must have $NDIMS rows"))
+ end
+
+ # Interpret an `NDIMS`-by-`N` matrix as one static vector per column. Constructing
+ # the vectors explicitly also supports non-contiguous matrix views.
+ return map(eachcol(points)) do point
+ return SVector{NDIMS, eltype(points)}(point)
+ end
+ end
+
+ return points
+end
+
include("geometries/geometries.jl")
include("point_in_poly/point_in_poly.jl")
include("particle_packing/particle_packing.jl")
diff --git a/src/schemes/boundary/open_boundary/boundary_zones.jl b/src/schemes/boundary/open_boundary/boundary_zones.jl
index c7f29b1e36..175eee289b 100644
--- a/src/schemes/boundary/open_boundary/boundary_zones.jl
+++ b/src/schemes/boundary/open_boundary/boundary_zones.jl
@@ -384,6 +384,16 @@ function set_up_boundary_zone(boundary_face, face_normal, density, particle_spac
flow_direction = zero(face_normal)
end
+ # Validate boundary geometry before sampling particles.
+ unit_spanning_set,
+ _ = calculate_spanning_vectors(boundary_face,
+ one(eltype(face_normal)))
+ dot_face_normal = dot(normalize(unit_spanning_set[:, 1]), face_normal)
+
+ if !isapprox(abs(dot_face_normal), 1)
+ throw(ArgumentError("`face_normal` is not normal to the boundary face"))
+ end
+
# Sample particles in boundary zone
if isnothing(initial_condition) && isnothing(extrude_geometry)
initial_condition = TrixiParticles.extrude_geometry(boundary_face; particle_spacing,
@@ -408,13 +418,6 @@ function set_up_boundary_zone(boundary_face, face_normal, density, particle_spac
# Vectors spanning the boundary zone/box
spanning_set, zone_origin = calculate_spanning_vectors(boundary_face, zone_width)
- # 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_face_normal), 1)
- 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 face
dot_flow = dot(normalize(spanning_set[:, 1]), flow_direction)
@@ -463,11 +466,22 @@ function spanning_vectors(face_vertices::NTuple{3}, zone_width)
edge1 = face_vertices[2] - face_vertices[1]
edge2 = face_vertices[3] - face_vertices[1]
+ edge1_norm = norm(edge1)
+ edge2_norm = norm(edge2)
+ edge_tolerance = sqrt(eps(typeof(edge1_norm * edge2_norm))) * edge1_norm *
+ edge2_norm
+
# Check if the edges are linearly dependent (to avoid degenerate planes)
- if isapprox(norm(cross(edge1, edge2)), 0.0; atol=eps())
+ cross_norm = norm(cross(edge1, edge2))
+ if isapprox(cross_norm, zero(cross_norm); atol=edge_tolerance)
throw(ArgumentError("the vectors `AB` and `AC` must not be collinear"))
end
+ edge_dot = dot(edge1, edge2)
+ if !isapprox(edge_dot, zero(edge_dot); atol=edge_tolerance)
+ throw(ArgumentError("the vectors `AB` and `AC` must be orthogonal"))
+ end
+
# Calculate normal vector of `boundary_face`
c = Vector(normalize(cross(edge2, edge1)) * zone_width)
diff --git a/src/schemes/boundary/open_boundary/method_of_characteristics.jl b/src/schemes/boundary/open_boundary/method_of_characteristics.jl
index ce04478a0c..5023b9023f 100644
--- a/src/schemes/boundary/open_boundary/method_of_characteristics.jl
+++ b/src/schemes/boundary/open_boundary/method_of_characteristics.jl
@@ -169,6 +169,7 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t)
# Particle is outside of the influence of fluid particles.
# `volume` is in the order of 1 / h^d, so volume * h^d is in the order of 1.
if volume[particle] * smoothing_length^ndims(system) < eps(eltype(smoothing_length))
+ zone_id = system.boundary_zone_indices[particle]
# Using the average of the values at the previous time step for particles which
# are outside of the influence of fluid particles.
@@ -178,6 +179,8 @@ function evaluate_characteristics!(system, v, u, v_ode, u_ode, semi, t)
counter = 0
for neighbor in each_integrated_particle(system)
+ system.boundary_zone_indices[neighbor] == zone_id || continue
+
# Make sure that only neighbors in the influence of
# the fluid particles are used.
# `volume` is in the order of 1 / h^d, so volume * h^d is in the order of 1.
diff --git a/src/schemes/boundary/open_boundary/system.jl b/src/schemes/boundary/open_boundary/system.jl
index 9da559481e..92c9b4d2d6 100644
--- a/src/schemes/boundary/open_boundary/system.jl
+++ b/src/schemes/boundary/open_boundary/system.jl
@@ -1,7 +1,9 @@
@doc raw"""
OpenBoundarySystem(boundary_zone::BoundaryZone;
- fluid_system::AbstractFluidSystem, buffer_size::Integer,
- boundary_model, calculate_flow_rate=false)
+ fluid_system::AbstractFluidSystem,
+ buffer_size=default_open_boundary_buffer_size(fluid_system),
+ boundary_model=BoundaryModelMirroringTafuni(),
+ calculate_flow_rate=false)
Open boundary system for in- and outflow particles.
@@ -10,7 +12,10 @@ Open boundary system for in- and outflow particles.
# Keywords
- `fluid_system`: The corresponding fluid system
-- `boundary_model`: Boundary model (see [Open Boundary Models](@ref open_boundary_models))
+- `buffer_size`: Number of buffer particles for the boundary system.
+ Defaults to the buffer size of `fluid_system`.
+- `boundary_model`: Boundary model (see [Open Boundary Models](@ref open_boundary_models)).
+ Defaults to [`BoundaryModelMirroringTafuni`](@ref).
- `calculate_flow_rate=false`: Set to `true` to calculate the volumetric flow rate through each boundary zone.
This value is automatically enabled when using [`RCRWindkesselModel`](@ref).
Otherwise, it is useful only for postprocessing.
@@ -49,6 +54,18 @@ struct OpenBoundarySystem{BM, ELTYPE, NDIMS, IC, FS, FSI, K, ARRAY1D, BC, FC, BZ
cache :: C
end
+function default_open_boundary_buffer_size(fluid_system)
+ fluid_buffer = buffer(fluid_system)
+
+ if fluid_buffer isa SystemBuffer
+ return fluid_buffer.buffer_size
+ end
+
+ throw(ArgumentError("`buffer_size` could not be inferred for `OpenBoundarySystem` " *
+ "because `fluid_system` has no buffer. Pass `buffer_size=...` " *
+ "explicitly or construct `fluid_system` with `buffer_size=...`."))
+end
+
function OpenBoundarySystem(boundary_model, initial_condition, fluid_system,
fluid_system_index, smoothing_kernel, smoothing_length, mass,
volume, boundary_candidates, fluid_candidates,
@@ -70,8 +87,10 @@ function OpenBoundarySystem(boundary_model, initial_condition, fluid_system,
end
function OpenBoundarySystem(boundary_zones::Union{BoundaryZone, Nothing}...;
- fluid_system::AbstractFluidSystem, buffer_size::Integer,
- boundary_model, calculate_flow_rate=false,
+ fluid_system::AbstractFluidSystem,
+ buffer_size=default_open_boundary_buffer_size(fluid_system),
+ boundary_model=BoundaryModelMirroringTafuni(),
+ calculate_flow_rate=false,
pressure_acceleration=fluid_system.pressure_acceleration_formulation,
shifting_technique=boundary_model isa
BoundaryModelDynamicalPressureZhang ?
@@ -807,9 +826,9 @@ function check_configuration(system::OpenBoundarySystem, systems, neighborhood_s
system.fluid_system_index[] = fluid_system_index
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()`."))
+ any(zone -> zone.is_bidirectional, boundary_zones)
+ throw(ArgumentError("`BoundaryModelCharacteristicsLastiwka` needs a directed boundary zone. " *
+ "Please specify `InFlow()` or `OutFlow()` instead of `BidirectionalFlow()`."))
end
if first(PointNeighbors.requires_update(neighborhood_search))
diff --git a/src/schemes/boundary/wall_boundary/dummy_particles.jl b/src/schemes/boundary/wall_boundary/dummy_particles.jl
index 55aa20226b..68dd17af34 100644
--- a/src/schemes/boundary/wall_boundary/dummy_particles.jl
+++ b/src/schemes/boundary/wall_boundary/dummy_particles.jl
@@ -4,7 +4,8 @@
smoothing_length; viscosity=nothing,
state_equation=nothing, correction=nothing,
clip_negative_pressure=false,
- reference_particle_spacing=0.0)
+ reference_particle_spacing=0.0,
+ surface_measure=nothing)
Boundary model for [`WallBoundarySystem`](@ref).
@@ -34,6 +35,9 @@ Boundary model for [`WallBoundarySystem`](@ref).
shifting technique is fighting.
- `reference_particle_spacing`: The reference particle spacing used for weighting values at the boundary,
which currently is only needed when using surface tension.
+- `surface_measure=nothing`: Optional nonnegative quadrature weight for each boundary particle.
+ Positive values represent samples on the physical boundary surface;
+ zero marks particles in deeper dummy-particle layers.
# Examples
```jldoctest; output = false, setup = :(densities = [1.0, 2.0, 3.0]; masses = [0.1, 0.2, 0.3]; smoothing_kernel = SchoenbergCubicSplineKernel{2}(); smoothing_length = 0.1)
# Free-slip condition
@@ -76,12 +80,55 @@ struct BoundaryModelDummyParticles{DC, SE, CLIP, ELTYPE <: Real, VECTOR, K, V, C
end
end
+@doc raw"""
+ BoundaryModelDummyParticles(initial_condition;
+ fluid_system::AbstractFluidSystem,
+ initial_density=initial_condition.density,
+ hydrodynamic_mass=initial_condition.mass,
+ boundary_density_calculator=AdamiPressureExtrapolation(),
+ smoothing_kernel=system_smoothing_kernel(fluid_system),
+ smoothing_length=initial_smoothing_length(fluid_system),
+ viscosity=nothing,
+ state_equation=system_state_equation(fluid_system),
+ correction=system_correction(fluid_system),
+ clip_negative_pressure=false,
+ reference_particle_spacing=default_reference_particle_spacing(fluid_system),
+ surface_measure=nothing)
+
+High-level convenience constructor for dummy-particle wall models that infers the kernel,
+smoothing length, correction, and equation-of-state-related settings from the adjacent
+`fluid_system`.
+"""
+function BoundaryModelDummyParticles(initial_condition;
+ fluid_system::AbstractFluidSystem,
+ initial_density=initial_condition.density,
+ hydrodynamic_mass=initial_condition.mass,
+ boundary_density_calculator=AdamiPressureExtrapolation(),
+ smoothing_kernel=system_smoothing_kernel(fluid_system),
+ smoothing_length=initial_smoothing_length(fluid_system),
+ viscosity=nothing,
+ state_equation=system_state_equation(fluid_system),
+ correction=system_correction(fluid_system),
+ clip_negative_pressure=false,
+ reference_particle_spacing=default_reference_particle_spacing(fluid_system),
+ surface_measure=nothing)
+ return BoundaryModelDummyParticles(initial_density, hydrodynamic_mass,
+ boundary_density_calculator, smoothing_kernel,
+ smoothing_length;
+ viscosity, state_equation, correction,
+ clip_negative_pressure,
+ reference_particle_spacing, surface_measure)
+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 BoundaryModelDummyParticles(initial_density, hydrodynamic_mass,
density_calculator, smoothing_kernel,
smoothing_length; viscosity=nothing,
state_equation=nothing, correction=nothing,
clip_negative_pressure=false,
- reference_particle_spacing=0.0)
+ reference_particle_spacing=0.0,
+ surface_measure=nothing)
pressure = initial_boundary_pressure(initial_density, density_calculator,
state_equation)
NDIMS = ndims(smoothing_kernel)
@@ -91,7 +138,8 @@ function BoundaryModelDummyParticles(initial_density, hydrodynamic_mass,
cache = (; create_cache_model(viscosity, n_particles, NDIMS)...,
create_cache_model(initial_density, density_calculator, NDIMS)...,
- create_cache_model(correction, initial_density, NDIMS, n_particles)...)
+ create_cache_model(correction, initial_density, NDIMS, n_particles)...,
+ create_cache_surface_measure(surface_measure, ELTYPE, NDIMS, n_particles)...)
# If the `reference_density_spacing` is set calculate the `ideal_neighbor_count`
if reference_particle_spacing > 0
@@ -109,6 +157,41 @@ function BoundaryModelDummyParticles(initial_density, hydrodynamic_mass,
clip_negative_pressure)
end
+@inline create_cache_surface_measure(::Nothing, ELTYPE, NDIMS, n_particles) = (;)
+
+function create_cache_surface_measure(surface_measure, ELTYPE, NDIMS, n_particles)
+ surface_measure isa AbstractVector ||
+ throw(ArgumentError("`surface_measure` must be a vector with one value per boundary particle"))
+ length(surface_measure) == n_particles ||
+ throw(ArgumentError("`surface_measure` must contain $n_particles values, got $(length(surface_measure))"))
+ all(value -> value isa Real && isfinite(value) && value >= 0, surface_measure) ||
+ throw(ArgumentError("`surface_measure` values must be finite, real, and nonnegative"))
+
+ converted_measure = collect(ELTYPE, surface_measure)
+ all(isfinite, converted_measure) ||
+ throw(ArgumentError("`surface_measure` values must remain finite when converted to $ELTYPE"))
+
+ wetted_area_weight = zeros(ELTYPE, n_particles)
+ wetted_area_flooded_reference = zeros(ELTYPE, n_particles)
+ wetted_area_reaction = zeros(ELTYPE, NDIMS, n_particles)
+ wetted_area_reaction_buffer = zeros(ELTYPE, NDIMS, n_particles,
+ Threads.nthreads())
+ wetted_area_active = Ref(false)
+
+ return (; surface_measure=converted_measure, wetted_area_weight,
+ wetted_area_flooded_reference, wetted_area_reaction,
+ wetted_area_reaction_buffer, wetted_area_active)
+end
+
+@inline function default_reference_particle_spacing(fluid_system)
+ if hasproperty(fluid_system, :cache) &&
+ hasproperty(fluid_system.cache, :reference_particle_spacing)
+ return fluid_system.cache.reference_particle_spacing
+ end
+
+ return zero(eltype(fluid_system))
+end
+
@inline function Base.ndims(boundary_model::BoundaryModelDummyParticles)
return ndims(boundary_model.smoothing_kernel)
end
diff --git a/src/schemes/boundary/wall_boundary/system.jl b/src/schemes/boundary/wall_boundary/system.jl
index f9864ecc05..679d9e2e56 100644
--- a/src/schemes/boundary/wall_boundary/system.jl
+++ b/src/schemes/boundary/wall_boundary/system.jl
@@ -231,6 +231,36 @@ function update_boundary_interpolation!(system::WallBoundarySystem, v, u, v_ode,
return system
end
+function reset_interaction_caches!(system::WallBoundarySystem)
+ boundary_cache = wetted_area_boundary_cache(system)
+ if !isnothing(boundary_cache) && boundary_cache.wetted_area_active[]
+ set_zero!(boundary_cache.wetted_area_reaction)
+ set_zero!(boundary_cache.wetted_area_reaction_buffer)
+ end
+ return system
+end
+
+function finalize_interaction!(system::WallBoundarySystem,
+ dv, v, u, dv_ode, v_ode, u_ode, semi)
+ boundary_cache = wetted_area_boundary_cache(system)
+ if isnothing(boundary_cache) || !boundary_cache.wetted_area_active[]
+ return system
+ end
+
+ reaction = boundary_cache.wetted_area_reaction
+ reaction_buffer = boundary_cache.wetted_area_reaction_buffer
+ @threaded semi for particle in eachparticle(system)
+ for dim in 1:ndims(system)
+ value = zero(eltype(system))
+ for thread in axes(reaction_buffer, 3)
+ @inbounds value += reaction_buffer[dim, particle, thread]
+ end
+ @inbounds reaction[dim, particle] = value
+ end
+ end
+ return system
+end
+
function write_u0!(u0, ::WallBoundarySystem)
return u0
end
diff --git a/src/schemes/fluid/entropically_damped_sph/rhs.jl b/src/schemes/fluid/entropically_damped_sph/rhs.jl
index 2ea1e4dc4d..9c96b72287 100644
--- a/src/schemes/fluid/entropically_damped_sph/rhs.jl
+++ b/src/schemes/fluid/entropically_damped_sph/rhs.jl
@@ -10,6 +10,7 @@ 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)
+ surface_normal_method_a = surface_normal_method(particle_system)
# For `distance == 0`, the analytical gradient is zero, but the unsafe gradient
# and the density diffusion divide by zero.
@@ -20,6 +21,19 @@ function interact!(dv, v_particle_system, u_particle_system,
h = initial_smoothing_length(particle_system)
almostzero = sqrt(eps(h^2))
+ if particle_system === neighbor_system
+ @threaded semi for particle in each_integrated_particle(particle_system)
+ rho_a = @inbounds current_density(v_particle_system, particle_system,
+ particle)
+ v_a = @inbounds current_velocity(v_particle_system, particle_system, particle)
+ acceleration = surface_tension_acceleration(surface_tension_a, particle_system,
+ particle, rho_a, v_a)
+ for i in 1:ndims(particle_system)
+ @inbounds dv[i, particle] += acceleration[i]
+ end
+ end
+ end
+
# Loop over all pairs of particles and neighbors within the kernel cutoff
foreach_point_neighbor(particle_system, neighbor_system,
system_coords, neighbor_coords, semi;
@@ -85,10 +99,23 @@ function interact!(dv, v_particle_system, u_particle_system,
particle, neighbor, pos_diff, distance,
rho_a, rho_b, grad_kernel, 1)
+ dv_particle[] += wetted_area_density_acceleration(surface_normal_method_a,
+ particle_system,
+ neighbor_system, particle,
+ neighbor, rho_a, rho_b, m_b,
+ grad_kernel)
+
@inbounds adhesion_force!(dv_particle, surface_tension_a, particle_system,
neighbor_system,
particle, neighbor, pos_diff, distance)
+ dv_particle[] += wetted_area_explicit_acceleration(surface_tension_a,
+ surface_normal_method_a,
+ particle_system,
+ neighbor_system, particle,
+ neighbor, m_a, rho_a,
+ grad_kernel)
+
for i in 1:ndims(particle_system)
@inbounds dv[i, particle] += dv_particle[][i]
end
diff --git a/src/schemes/fluid/entropically_damped_sph/system.jl b/src/schemes/fluid/entropically_damped_sph/system.jl
index 2b085d3ebb..776fb8b4e8 100644
--- a/src/schemes/fluid/entropically_damped_sph/system.jl
+++ b/src/schemes/fluid/entropically_damped_sph/system.jl
@@ -30,6 +30,8 @@ See [Entropically Damped Artificial Compressibility for SPH](@ref edac) for more
- `pressure_acceleration`: Pressure acceleration formulation (default: inter-particle averaged pressure).
When set to `nothing`, the pressure acceleration formulation for the
corresponding [density calculator](@ref density_calculator) is chosen.
+ [`InterfaceAwareTensileInstabilityControl`](@ref) can be used
+ with a supported Morris CSF/CSS free surface.
- `density_calculator`: [Density calculator](@ref density_calculator) (default: [`SummationDensity`](@ref))
- `shifting_technique`: [Shifting technique](@ref shifting) or [transport velocity
formulation](@ref transport_velocity_formulation) to use
@@ -122,9 +124,15 @@ function EntropicallyDampedSPHSystem(initial_condition; smoothing_kernel, smooth
if surface_tension !== nothing && surface_normal_method === nothing
surface_normal_method = ColorfieldSurfaceNormal()
end
+ validate_corrected_csf(surface_normal_method, surface_tension)
+ validate_free_surface_shifting(shifting_technique, surface_normal_method,
+ surface_tension)
+ validate_interface_aware_tic(pressure_acceleration, density_calculator,
+ nothing, surface_normal_method,
+ surface_tension, correction)
if surface_normal_method !== nothing && reference_particle_spacing < eps()
- throw(ArgumentError("`reference_particle_spacing` must be set to a positive value when using `ColorfieldSurfaceNormal` or a surface tension model"))
+ throw(ArgumentError("`reference_particle_spacing` must be set to a positive value when using a surface-normal method or a surface tension model"))
end
if correction isa ShepardKernelCorrection &&
@@ -237,6 +245,8 @@ end
return ELTYPE
end
+@inline wetted_area_supported_fluid(::EntropicallyDampedSPHSystem) = true
+
@inline function v_nvariables(system::EntropicallyDampedSPHSystem)
return v_nvariables(system, system.density_calculator)
end
@@ -299,9 +309,51 @@ function update_quantities!(system::EntropicallyDampedSPHSystem, v, u,
end
function update_pressure!(system::EntropicallyDampedSPHSystem, v, u, v_ode, u_ode, semi, t)
+ (; correction, density_calculator) = system
+
+ # These are only computed when using corrections
+ compute_correction_values!(system, correction, u, v_ode, u_ode, semi)
+ compute_gradient_correction_matrix!(correction, system, u, v_ode, u_ode, semi)
+ # `kernel_correct_density!` only performed for `SummationDensity`
+ kernel_correct_density!(system, v, u, v_ode, u_ode, semi, correction,
+ density_calculator)
+
compute_surface_normal!(system, system.surface_normal_method, v, u, v_ode, u_ode, semi,
t)
- compute_surface_delta_function!(system, system.surface_tension, semi)
+end
+
+function kernel_correct_density!(system::EntropicallyDampedSPHSystem, v, u, v_ode, u_ode,
+ semi, correction, density_calculator)
+ return system
+end
+
+function kernel_correct_density!(system::EntropicallyDampedSPHSystem, v, u, v_ode, u_ode,
+ semi, corr::ShepardKernelCorrection, ::SummationDensity)
+ system.cache.density ./= system.cache.kernel_correction_coefficient
+end
+
+function compute_gradient_correction_matrix!(correction,
+ system::EntropicallyDampedSPHSystem, u,
+ v_ode, u_ode, semi)
+ return system
+end
+
+function compute_gradient_correction_matrix!(corr::Union{GradientCorrection,
+ BlendedGradientCorrection,
+ MixedKernelGradientCorrection},
+ system::EntropicallyDampedSPHSystem, u,
+ v_ode, u_ode, semi)
+ (; cache, correction, smoothing_kernel) = system
+ (; correction_matrix) = cache
+
+ system_coords = current_coordinates(u, system)
+
+ compute_gradient_correction_matrix!(correction_matrix, system, system_coords,
+ v_ode, u_ode, semi, correction, smoothing_kernel)
+end
+
+@inline function correction_matrix(system::EntropicallyDampedSPHSystem, particle)
+ extract_smatrix(system.cache.correction_matrix, system, particle)
end
function update_final!(system::EntropicallyDampedSPHSystem, v, u, v_ode, u_ode, semi, t;
@@ -310,7 +362,6 @@ function update_final!(system::EntropicallyDampedSPHSystem, 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_average_pressure!(system, system.average_pressure_reduction, v_ode, u_ode, semi)
update_shifting!(system, shifting_technique(system), v, u, v_ode, u_ode, semi)
end
@@ -381,6 +432,6 @@ function restart_with!(system::EntropicallyDampedSPHSystem, v, u)
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]
+ system.initial_condition.pressure[particle] = v[ndims(system) + 1, particle]
end
end
diff --git a/src/schemes/fluid/fluid.jl b/src/schemes/fluid/fluid.jl
index fb49a65359..1d3a483ead 100644
--- a/src/schemes/fluid/fluid.jl
+++ b/src/schemes/fluid/fluid.jl
@@ -297,11 +297,18 @@ function restart_v(system::AbstractFluidSystem, data)
end
function check_configuration(fluid_system::AbstractFluidSystem, systems, nhs)
- if !(fluid_system isa ParticlePackingSystem) && !isnothing(fluid_system.surface_tension)
+ if !(fluid_system isa ParticlePackingSystem)
+ check_wetted_area_configuration!(fluid_system,
+ surface_normal_method(fluid_system), systems)
+ end
+
+ if !(fluid_system isa ParticlePackingSystem) &&
+ (!isnothing(fluid_system.surface_tension) ||
+ !isnothing(fluid_system.surface_normal_method))
foreach_system(systems) do neighbor
- if neighbor isa AbstractFluidSystem &&
- isnothing(fluid_system.surface_tension) &&
- isnothing(fluid_system.surface_normal_method)
+ if neighbor isa AbstractFluidSystem && !(neighbor isa ParticlePackingSystem) &&
+ isnothing(neighbor.surface_tension) &&
+ isnothing(neighbor.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
diff --git a/src/schemes/fluid/implicit_incompressible_sph/system.jl b/src/schemes/fluid/implicit_incompressible_sph/system.jl
index 9b8fedc288..c7df1ce185 100644
--- a/src/schemes/fluid/implicit_incompressible_sph/system.jl
+++ b/src/schemes/fluid/implicit_incompressible_sph/system.jl
@@ -199,6 +199,8 @@ end
return system.density
end
+@inline system_state_equation(system::ImplicitIncompressibleSPHSystem) = nothing
+
# TODO: What do we do with the sound speed? This is needed for the viscosity.
@inline system_sound_speed(system::ImplicitIncompressibleSPHSystem) = system.artificial_sound_speed
@@ -487,7 +489,8 @@ function calculate_sum_d_ij_pj!(sum_d_ij_pj, system,
(; time_step) = system
system_coords = current_coordinates(u, system)
- neighbor_coords = current_coordinates(u, neighbor_system)
+ u_neighbor_system = wrap_u(u_ode, neighbor_system, semi)
+ neighbor_coords = current_coordinates(u_neighbor_system, neighbor_system)
foreach_point_neighbor(system, neighbor_system, system_coords, neighbor_coords, semi;
points=each_integrated_particle(system)) do particle, neighbor,
@@ -574,11 +577,13 @@ function pressure_update(system, pressure, reference_density, a_ii, sum_term, om
pressure[particle] = zero(pressure[particle])
end
# Calculate the average density error for the termination condition
- if (pressure[particle] != 0.0)
+ if pressure[particle] != 0.0
new_density = a_ii[particle] * pressure[particle] + sum_term[particle] -
iisph_source_term(system, particle) +
reference_density
- density_error[particle] = (new_density - reference_density)
+ density_error[particle] = abs(new_density - reference_density)
+ else
+ density_error[particle] = zero(eltype(density_error))
end
end
relative_density_error = sum(density_error) / reference_density
@@ -736,9 +741,11 @@ end
function check_configuration(system::ImplicitIncompressibleSPHSystem, systems, nhs)
(; time_step, omega) = system
foreach_system(systems) do neighbor
- if neighbor isa WeaklyCompressibleSPHSystem
- throw(ArgumentError("`ImplicitIncompressibleSPHSystem` cannot be used together with
- `WeaklyCompressibleSPHSystem`"))
+ if neighbor isa WeaklyCompressibleSPHSystem ||
+ neighbor isa EntropicallyDampedSPHSystem
+ neighbor_name = neighbor |> typeof |> nameof
+ throw(ArgumentError("`ImplicitIncompressibleSPHSystem` cannot be used " *
+ "together with `$neighbor_name`"))
end
if neighbor isa WallBoundarySystem
if (neighbor.boundary_model isa BoundaryModelDummyParticles &&
diff --git a/src/schemes/fluid/pressure_acceleration.jl b/src/schemes/fluid/pressure_acceleration.jl
index b6114c0bc1..6dd04249c8 100644
--- a/src/schemes/fluid/pressure_acceleration.jl
+++ b/src/schemes/fluid/pressure_acceleration.jl
@@ -61,9 +61,9 @@ the [`WeaklyCompressibleSPHSystem`](@ref) constructor.
See [Tensile Instability Control](@ref tic) for more information on this technique.
!!! warning
- Tensile Instability Control 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.**
+ Direct use of this function must be disabled close to a free surface. For supported
+ Morris/CSS free-surface simulations, pass
+ [`InterfaceAwareTensileInstabilityControl`](@ref) as `pressure_acceleration` instead.
"""
@inline function tensile_instability_control(m_a, m_b, rho_a, rho_b, p_a, p_b, W_a)
# Same as `pressure_acceleration_continuity_density`, but using the minus formulation
@@ -71,6 +71,67 @@ See [Tensile Instability Control](@ref tic) for more information on this techniq
return -m_b * (abs(p_a) + p_b) / (rho_a * rho_b) * W_a
end
+"""
+ InterfaceAwareTensileInstabilityControl(; strength=1.0)
+
+Apply [`tensile_instability_control`](@ref) in fully supported fluid interiors and blend back
+to the conservative continuity-density pressure acceleration across a represented free surface.
+The control is disabled for fluid-boundary interactions.
+
+This explicit opt-in requires [`ContinuityDensity`](@ref), unclipped pressure, no asymmetric
+kernel-gradient correction, and an interface-activity-providing surface-normal method: either a
+[`ColorfieldSurfaceNormal`](@ref) with Morris CSF/CSS surface tension or a
+[`CorrectedCSFSurfaceNormal`](@ref) with [`SurfaceTensionMorris`](@ref). The `strength` in
+`(0, 1]` scales only the tensile correction; `1` recovers the complete interior TIC
+formulation.
+"""
+struct InterfaceAwareTensileInstabilityControl{T <: Real}
+ strength::T
+
+ function InterfaceAwareTensileInstabilityControl(; strength=1.0)
+ strength isa Real && isfinite(strength) && 0 < strength <= 1 ||
+ throw(ArgumentError("`strength` must be finite and in (0, 1]"))
+ new{typeof(strength)}(strength)
+ end
+end
+
+@inline validate_interface_aware_tic(pressure_acceleration, density_calculator,
+ state_equation, surface_normal_method,
+ surface_tension, correction) = nothing
+
+function validate_interface_aware_tic(::InterfaceAwareTensileInstabilityControl,
+ density_calculator, state_equation,
+ surface_normal_method, surface_tension, correction)
+ density_calculator isa ContinuityDensity ||
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` requires `ContinuityDensity`"))
+ uses_asymmetric_kernel_gradient(correction) &&
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` does not support asymmetric kernel-gradient corrections"))
+ supports_interface_aware_tic(surface_normal_method, surface_tension) ||
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` requires " *
+ "`ColorfieldSurfaceNormal` with Morris CSF/CSS surface tension " *
+ "or `CorrectedCSFSurfaceNormal` with `SurfaceTensionMorris`"))
+ if !isnothing(state_equation) && applicable(clip_negative_pressure, state_equation) &&
+ clip_negative_pressure(state_equation)
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` requires unclipped negative pressure"))
+ end
+ return nothing
+end
+
+@inline function interface_aware_tensile_acceleration(m_a, m_b, rho_a, rho_b, p_a, p_b,
+ W_a, activity_a, activity_b,
+ strength)
+ standard = pressure_acceleration_continuity_density(m_a, m_b, rho_a, rho_b,
+ p_a, p_b, W_a)
+ interface_activity = max(activity_a, activity_b)
+ isfinite(interface_activity) || return standard
+
+ controlled = tensile_instability_control(m_a, m_b, rho_a, rho_b, p_a, p_b, W_a)
+ interface_activity = clamp(interface_activity, zero(interface_activity),
+ one(interface_activity))
+ interior_weight = strength * (one(interface_activity) - interface_activity)
+ return standard + interior_weight * (controlled - standard)
+end
+
# This formulation was introduced by Hu and Adams (2006). https://doi.org/10.1016/j.jcp.2005.09.001
# They argued that the formulation is more flexible because of the possibility to formulate
# different inter-particle averages or to assume different inter-particle distributions.
@@ -93,13 +154,17 @@ end
return -volume_term * pressure_tilde * W_a
end
+@inline function uses_asymmetric_kernel_gradient(correction)
+ return correction isa Union{KernelCorrection,
+ GradientCorrection,
+ BlendedGradientCorrection,
+ MixedKernelGradientCorrection}
+end
+
function choose_pressure_acceleration_formulation(pressure_acceleration,
density_calculator, NDIMS, ELTYPE,
correction)
- if correction isa KernelCorrection ||
- correction isa GradientCorrection ||
- correction isa BlendedGradientCorrection ||
- correction isa MixedKernelGradientCorrection
+ if uses_asymmetric_kernel_gradient(correction)
if isempty(methods(pressure_acceleration,
(ELTYPE, ELTYPE, ELTYPE, ELTYPE, ELTYPE, ELTYPE,
SVector{NDIMS, ELTYPE}, SVector{NDIMS, ELTYPE})))
@@ -123,6 +188,16 @@ function choose_pressure_acceleration_formulation(pressure_acceleration,
return pressure_acceleration
end
+function choose_pressure_acceleration_formulation(control::InterfaceAwareTensileInstabilityControl,
+ density_calculator, NDIMS, ELTYPE,
+ correction)
+ density_calculator isa ContinuityDensity ||
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` requires `ContinuityDensity`"))
+ uses_asymmetric_kernel_gradient(correction) &&
+ throw(ArgumentError("`InterfaceAwareTensileInstabilityControl` does not support asymmetric kernel-gradient corrections"))
+ return control
+end
+
function choose_pressure_acceleration_formulation(pressure_acceleration::Nothing,
density_calculator::SummationDensity,
NDIMS, ELTYPE,
@@ -143,14 +218,57 @@ end
@inline pressure_acceleration_formulation(system) = system.pressure_acceleration_formulation
+@inline function evaluate_pressure_acceleration(formulation, particle_system,
+ neighbor_system, particle, neighbor,
+ m_a, m_b, rho_a, rho_b, p_a, p_b, W_a)
+ return formulation(m_a, m_b, rho_a, rho_b, p_a, p_b, W_a)
+end
+
+@inline function evaluate_pressure_acceleration(::InterfaceAwareTensileInstabilityControl,
+ particle_system, neighbor_system,
+ particle, neighbor, m_a, m_b,
+ rho_a, rho_b, p_a, p_b, W_a)
+ return pressure_acceleration_continuity_density(m_a, m_b, rho_a, rho_b,
+ p_a, p_b, W_a)
+end
+
+@inline function interface_tic_activity(system::AbstractFluidSystem, particle)
+ supports_interface_aware_tic(surface_normal_method(system),
+ surface_tension_model(system)) ||
+ return one(eltype(system))
+ return surface_interface_activity(system, particle)
+end
+
+@inline function evaluate_pressure_acceleration(control::InterfaceAwareTensileInstabilityControl,
+ particle_system,
+ neighbor_system::AbstractFluidSystem,
+ particle, neighbor, m_a, m_b,
+ rho_a, rho_b, p_a, p_b, W_a)
+ activity_a = interface_tic_activity(particle_system, particle)
+ activity_b = interface_tic_activity(neighbor_system, neighbor)
+ return interface_aware_tensile_acceleration(m_a, m_b, rho_a, rho_b, p_a, p_b, W_a,
+ activity_a, activity_b, control.strength)
+end
+
+@inline function evaluate_pressure_acceleration(::InterfaceAwareTensileInstabilityControl,
+ particle_system,
+ neighbor_system::AbstractFluidSystem,
+ particle, neighbor, m_a, m_b,
+ rho_a, rho_b, p_a::SMatrix, p_b, W_a)
+ return pressure_acceleration_continuity_density(m_a, m_b, rho_a, rho_b,
+ p_a, p_b, W_a)
+end
+
# Formulation using symmetric gradient formulation for corrections not depending on local neighborhood.
@inline function pressure_acceleration(particle_system, neighbor_system, particle, neighbor,
m_a, m_b, p_a, p_b, rho_a, rho_b, pos_diff,
distance, W_a, correction)
# Without correction or with `AkinciFreeSurfaceCorrection`, the kernel gradient is
# symmetric, so call the symmetric version of the pressure acceleration formulation.
- return pressure_acceleration_formulation(particle_system)(m_a, m_b, rho_a, rho_b,
- p_a, p_b, W_a)
+ formulation = pressure_acceleration_formulation(particle_system)
+ return evaluate_pressure_acceleration(formulation, particle_system, neighbor_system,
+ particle, neighbor, m_a, m_b, rho_a, rho_b,
+ p_a, p_b, W_a)
end
# Formulation using asymmetric gradient formulation for corrections depending on local neighborhood.
diff --git a/src/schemes/fluid/shifting_techniques.jl b/src/schemes/fluid/shifting_techniques.jl
index 9e2c1f23bc..7f3dee20ce 100644
--- a/src/schemes/fluid/shifting_techniques.jl
+++ b/src/schemes/fluid/shifting_techniques.jl
@@ -58,13 +58,45 @@ end
return v_diff
end
+"""
+ FreeSurfaceTangentialShifting()
+
+Remove the interface-normal component of the particle-shifting velocity using the raw
+free-surface normal. Full shifting is retained in the fluid interior, while particles in the
+smooth interface transition are blended towards tangential-only shifting.
+
+This treatment requires a [`ColorfieldSurfaceNormal`](@ref) with
+[`SurfaceTensionMorris`](@ref) or [`SurfaceTensionMomentumMorris`](@ref), or a
+[`CorrectedCSFSurfaceNormal`](@ref) with `SurfaceTensionMorris`. It is an explicit opt-in through
+the `free_surface_treatment` keyword of [`ParticleShiftingTechnique`](@ref).
+"""
+struct FreeSurfaceTangentialShifting end
+
+@inline supports_free_surface_shifting(surface_normal_method, surface_tension) = false
+
+@inline validate_free_surface_shifting(::Nothing, surface_normal_method,
+ surface_tension) = nothing
+
+function validate_free_surface_shifting(::FreeSurfaceTangentialShifting,
+ surface_normal_method, surface_tension)
+ supports_free_surface_shifting(surface_normal_method, surface_tension) ||
+ throw(ArgumentError("`FreeSurfaceTangentialShifting` requires " *
+ "`ColorfieldSurfaceNormal` with Morris/CSS surface tension or " *
+ "`CorrectedCSFSurfaceNormal` with Morris surface tension"))
+ return nothing
+end
+
+@inline validate_free_surface_shifting(shifting, surface_normal_method,
+ surface_tension) = nothing
+
@doc raw"""
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)
+ second_continuity_equation_term=ContinuityEquationTermSun2019(),
+ momentum_equation_term=MomentumEquationTermSun2019(),
+ v_max_factor=1, sound_speed_factor=0,
+ free_surface_treatment=nothing)
Particle Shifting Technique by [Sun et al. (2017)](@cite Sun2017)
and [Sun et al. (2019)](@cite Sun2019).
@@ -133,27 +165,33 @@ We provide the following convenience constructors for common variants of the met
`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.
+- `free_surface_treatment`: Treatment applied to shifting near a free surface. The default
+ `nothing` retains the closed-system formulation. Use
+ [`FreeSurfaceTangentialShifting`](@ref) with a supported
+ Morris CSF/CSS interface to remove the interface-normal
+ shifting component.
!!! 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.**
+ The default `free_surface_treatment=nothing` is for closed systems. Free-surface use requires
+ an explicit compatible treatment such as [`FreeSurfaceTangentialShifting`](@ref).
"""
struct ParticleShiftingTechnique{integrate_shifting_velocity,
update_everystage,
modify_continuity_equation,
compute_v_max,
- ELTYPE, S, M} <: AbstractShiftingTechnique
+ ELTYPE, S, M, F} <: AbstractShiftingTechnique
v_factor :: ELTYPE
second_continuity_equation_term :: S
momentum_equation_term :: M
+ free_surface_treatment :: F
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)
+ v_max_factor=1, sound_speed_factor=0,
+ free_surface_treatment=nothing)
if !integrate_shifting_velocity && update_everystage
throw(ArgumentError("ParticleShiftingTechnique: " *
"integrate_shifting_velocity=false requires " *
@@ -190,6 +228,12 @@ struct ParticleShiftingTechnique{integrate_shifting_velocity,
"must be positive"))
end
+ if !(free_surface_treatment isa Union{Nothing,
+ FreeSurfaceTangentialShifting})
+ throw(ArgumentError("ParticleShiftingTechnique: `free_surface_treatment` " *
+ "must be `nothing` or `FreeSurfaceTangentialShifting()`"))
+ end
+
v_factor = max(v_max_factor, sound_speed_factor)
compute_v_max = v_max_factor > 0
@@ -198,12 +242,20 @@ struct ParticleShiftingTechnique{integrate_shifting_velocity,
modify_continuity_equation,
compute_v_max, typeof(v_factor),
typeof(second_continuity_equation_term),
- typeof(momentum_equation_term)}(v_factor,
+ typeof(momentum_equation_term),
+ typeof(free_surface_treatment)}(v_factor,
second_continuity_equation_term,
- momentum_equation_term)
+ momentum_equation_term,
+ free_surface_treatment)
end
end
+function validate_free_surface_shifting(shifting::ParticleShiftingTechnique,
+ surface_normal_method, surface_tension)
+ return validate_free_surface_shifting(shifting.free_surface_treatment,
+ surface_normal_method, surface_tension)
+end
+
"""
ParticleShiftingTechniqueSun2017(; kwargs...)
@@ -218,10 +270,11 @@ ParticleShiftingTechnique(integrate_shifting_velocity=false,
modify_continuity_equation=false,
second_continuity_equation_term=nothing,
momentum_equation_term=nothing,
- v_max_factor=1, sound_speed_factor=0)
+ v_max_factor=1, sound_speed_factor=0,
+ free_surface_treatment=nothing)
# output
-ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing}(1, nothing, nothing)
+ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing, Nothing}(1, nothing, nothing, nothing)
```
See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all available options.
@@ -234,13 +287,12 @@ See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all availabl
shifting_technique = ParticleShiftingTechniqueSun2017()
# output
-ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing}(1, nothing, nothing)
+ParticleShiftingTechnique{false, false, false, true, Int64, Nothing, Nothing, Nothing}(1, nothing, 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.**
+ The default `free_surface_treatment=nothing` is for closed systems. See
+ [`FreeSurfaceTangentialShifting`](@ref) for explicit free-surface use.
"""
function ParticleShiftingTechniqueSun2017(; kwargs...)
return ParticleShiftingTechnique(; integrate_shifting_velocity=false,
@@ -264,10 +316,11 @@ ParticleShiftingTechnique(integrate_shifting_velocity=true,
modify_continuity_equation=true,
second_continuity_equation_term=ContinuityEquationTermSun2019(),
momentum_equation_term=MomentumEquationTermSun2019(),
- v_max_factor=0, sound_speed_factor=0.1f0)
+ v_max_factor=0, sound_speed_factor=0.1f0,
+ free_surface_treatment=nothing)
# output
-ParticleShiftingTechnique{true, true, true, false, Float32, ContinuityEquationTermSun2019, MomentumEquationTermSun2019}(0.1f0, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019())
+ParticleShiftingTechnique{true, true, true, false, Float32, ContinuityEquationTermSun2019, MomentumEquationTermSun2019, Nothing}(0.1f0, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019(), nothing)
```
See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all available options.
@@ -287,13 +340,12 @@ See [ParticleShiftingTechnique](@ref ParticleShiftingTechnique) for all availabl
shifting_technique = ConsistentShiftingSun2019()
# output
-ParticleShiftingTechnique{true, true, true, false, Float32, ContinuityEquationTermSun2019, MomentumEquationTermSun2019}(0.1f0, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019())
+ParticleShiftingTechnique{true, true, true, false, Float32, ContinuityEquationTermSun2019, MomentumEquationTermSun2019, Nothing}(0.1f0, ContinuityEquationTermSun2019(), MomentumEquationTermSun2019(), 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.**
+ The default `free_surface_treatment=nothing` is for closed systems. See
+ [`FreeSurfaceTangentialShifting`](@ref) for explicit free-surface use.
"""
function ConsistentShiftingSun2019(; kwargs...)
return ParticleShiftingTechnique(; integrate_shifting_velocity=true,
@@ -504,7 +556,39 @@ end
end
modify_shifting_at_free_surfaces!(system, u, semi)
+ modify_shifting_with_surface_normal!(system, shifting.free_surface_treatment, semi)
+
+ return system
+end
+@inline modify_shifting_with_surface_normal!(system, treatment, semi) = system
+
+@inline function tangential_shifting_velocity(shifting_velocity, normal, activity)
+ normal_norm_squared = dot(normal, normal)
+ normal_norm_squared > eps(normal_norm_squared) || return shifting_velocity
+ isfinite(activity) || return shifting_velocity
+
+ weight = clamp(activity, zero(activity), one(activity))
+ normal_component = dot(shifting_velocity, normal) / normal_norm_squared * normal
+ return shifting_velocity - weight * normal_component
+end
+
+function modify_shifting_with_surface_normal!(system::AbstractFluidSystem,
+ ::FreeSurfaceTangentialShifting, semi)
+ delta_v_cache = system.cache.delta_v
+ @threaded semi for particle in each_integrated_particle(system)
+ activity = surface_interface_activity(system, particle)
+ if !iszero(activity)
+ # Shifting uses raw geometry even when capillary normal smoothing is enabled.
+ normal = surface_normal(system, particle)
+ shifting_velocity = extract_svector(delta_v_cache, system, particle)
+ corrected_velocity = tangential_shifting_velocity(shifting_velocity, normal,
+ activity)
+ for dimension in eachindex(corrected_velocity)
+ @inbounds delta_v_cache[dimension, particle] = corrected_velocity[dimension]
+ end
+ end
+ end
return system
end
diff --git a/src/schemes/fluid/surface_normal_sph.jl b/src/schemes/fluid/surface_normal_sph.jl
index 4db94ea763..758aff31e1 100644
--- a/src/schemes/fluid/surface_normal_sph.jl
+++ b/src/schemes/fluid/surface_normal_sph.jl
@@ -1,36 +1,688 @@
+abstract type AbstractContactAngleModel end
+
+function validate_contact_angle(contact_angle)
+ if !(contact_angle isa Real) || !isfinite(contact_angle) ||
+ !(0 <= contact_angle <= 180)
+ throw(ArgumentError("`contact_angle` must be a finite real number in [0, 180] degrees"))
+ end
+
+ return contact_angle
+end
+
+@doc raw"""
+ WettedAreaContactAngle(contact_angle)
+
+Apply Young's wall energy through a corrected wetted-area quadrature. The model is an explicit
+opt-in for [`ColorfieldSurfaceNormal`](@ref); constructing `ColorfieldSurfaceNormal()` without a
+contact model remains unchanged.
+
+The supported configuration is currently restricted to three-dimensional WCSPH or EDAC fluids
+using `ContinuityDensity`, `SurfaceTensionMomentumMorris`, and `WendlandC2Kernel{3}` with
+`h/dx=1.4`. Contact boundaries must be dummy-particle wall or rigid-body systems built with
+per-particle `surface_measure` values and `InitialCondition.normals`. Each boundary system
+represents one connected disk-like wetted patch. Angles must lie strictly inside `(0, 180)`
+degrees; at 90 degrees the wall energy and force are exactly zero.
+"""
+struct WettedAreaContactAngle{ELTYPE <: Real} <: AbstractContactAngleModel
+ contact_angle::ELTYPE
+
+ function WettedAreaContactAngle(contact_angle)
+ angle = validate_contact_angle(contact_angle)
+ 0 < angle < 180 ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires `contact_angle` in (0, 180) degrees"))
+ new{typeof(angle)}(angle)
+ end
+end
+
+@inline convert_contact_model(::Nothing, ELTYPE) = nothing
+
+@inline function convert_contact_model(contact_model::WettedAreaContactAngle, ELTYPE)
+ return WettedAreaContactAngle(convert(ELTYPE, contact_model.contact_angle))
+end
+
+function convert_contact_model(contact_model, ELTYPE)
+ throw(ArgumentError("`contact_model` must be `nothing` or `WettedAreaContactAngle`"))
+end
+
@doc raw"""
ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1, interface_threshold=0.01,
- ideal_density_threshold=0.0)
+ ideal_density_threshold=0.0, interface_taper_start=0.8,
+ support_taper_width=0.025, contact_model=nothing,
+ normal_smoothing=false)
Color field based computation of the interface normals.
# Keywords
- `boundary_contact_threshold=0.1`: If this threshold is reached the fluid is assumed to be in contact with the boundary.
- `interface_threshold=0.01`: Threshold for normals to be removed as being invalid.
-- `ideal_density_threshold=0.0`: Assume particles are inside if they are above this threshold, which is relative to the `ideal_neighbor_count`.
+- `ideal_density_threshold=0.0`: For Morris CSF/CSS, assume particles are inside when their
+ continuous kernel-support moment is above this fraction of
+ complete support. Zero disables this filter. Other models
+ retain their existing neighbor-count interpretation.
+- `interface_taper_start=0.8`: Start Morris CSF/CSS interface activation at this fraction of
+ `interface_threshold`.
+- `support_taper_width=0.025`: Width of the Morris CSF/CSS support-moment transition above
+ `ideal_density_threshold`.
+- `contact_model=nothing`: Optional contact-angle model. The supported explicit choice is
+ [`WettedAreaContactAngle`](@ref).
+- `normal_smoothing=false`: Apply one activity-weighted Shepard smoothing pass to unit
+ normals before Morris curvature, force, or CSS stress
+ evaluation. Raw geometry remains unchanged.
"""
-struct ColorfieldSurfaceNormal{ELTYPE}
+struct ColorfieldSurfaceNormal{ELTYPE, CONTACT_MODEL}
boundary_contact_threshold::ELTYPE
interface_threshold::ELTYPE
ideal_density_threshold::ELTYPE
+ interface_taper_start::ELTYPE
+ support_taper_width::ELTYPE
+ contact_model::CONTACT_MODEL
+ normal_smoothing::Bool
end
-function ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1, interface_threshold=0.01,
- ideal_density_threshold=0.0)
- return ColorfieldSurfaceNormal(boundary_contact_threshold, interface_threshold,
+@doc raw"""
+ CorrectedCSFSurfaceNormal()
+
+Interface geometry for the corrected continuous-surface-force (C-CSF) method of Vergnaud
+et al. (2022). The outward unit normal is computed from the renormalized gradient of the
+smallest eigenvalue of the first-order kernel moment. Curvature uses the corresponding
+renormalized divergence with the published thin-jet angular filter, and the surface delta
+uses the published Shepard correction.
+
+This explicit opt-in implements the single-fluid free-surface core (equations 15--25) with
+[`SurfaceTensionMorris`](@ref). Boundary-integral and contact-angle terms are not included.
+"""
+struct CorrectedCSFSurfaceNormal end
+
+@inline function supports_free_surface_shifting(::ColorfieldSurfaceNormal,
+ ::Union{SurfaceTensionMorris,
+ SurfaceTensionMomentumMorris})
+ return true
+end
+
+@inline function supports_free_surface_shifting(::CorrectedCSFSurfaceNormal,
+ ::SurfaceTensionMorris)
+ return true
+end
+
+# Interface-aware TIC needs the smooth activity provided by these free-surface models.
+@inline supports_interface_aware_tic(surface_normal_method, surface_tension) = false
+
+@inline function supports_interface_aware_tic(::ColorfieldSurfaceNormal,
+ ::Union{SurfaceTensionMorris,
+ SurfaceTensionMomentumMorris})
+ return true
+end
+
+@inline function supports_interface_aware_tic(::CorrectedCSFSurfaceNormal,
+ ::SurfaceTensionMorris)
+ return true
+end
+
+@inline validate_corrected_csf(surface_normal_method, surface_tension) = nothing
+
+function validate_corrected_csf(::CorrectedCSFSurfaceNormal, surface_tension)
+ surface_tension isa SurfaceTensionMorris ||
+ throw(ArgumentError("`CorrectedCSFSurfaceNormal` requires `SurfaceTensionMorris`"))
+ return nothing
+end
+
+function ColorfieldSurfaceNormal(boundary_contact_threshold, interface_threshold,
+ ideal_density_threshold)
+ return ColorfieldSurfaceNormal(; boundary_contact_threshold, interface_threshold,
ideal_density_threshold)
end
+function ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1, interface_threshold=0.01,
+ ideal_density_threshold=0.0, interface_taper_start=0.8,
+ support_taper_width=0.025, contact_model=nothing,
+ normal_smoothing=false)
+ if !(boundary_contact_threshold isa Real) || isnan(boundary_contact_threshold) ||
+ boundary_contact_threshold < 0
+ throw(ArgumentError("`boundary_contact_threshold` must be non-negative and not NaN"))
+ end
+ if !(interface_threshold isa Real) || !isfinite(interface_threshold) ||
+ interface_threshold < 0
+ throw(ArgumentError("`interface_threshold` must be finite and non-negative"))
+ end
+ if !(ideal_density_threshold isa Real) || !isfinite(ideal_density_threshold) ||
+ ideal_density_threshold < 0
+ throw(ArgumentError("`ideal_density_threshold` must be finite and non-negative"))
+ end
+ if !(interface_taper_start isa Real) || !isfinite(interface_taper_start) ||
+ !(0 <= interface_taper_start < 1)
+ throw(ArgumentError("`interface_taper_start` must be finite and in [0, 1)"))
+ end
+ if !(support_taper_width isa Real) || !isfinite(support_taper_width) ||
+ support_taper_width <= 0
+ throw(ArgumentError("`support_taper_width` must be finite and positive"))
+ end
+ normal_smoothing isa Bool ||
+ throw(ArgumentError("`normal_smoothing` must be `true` or `false`"))
+
+ thresholds = promote(boundary_contact_threshold, interface_threshold,
+ ideal_density_threshold)
+ ELTYPE = typeof(first(thresholds))
+ if ELTYPE <: Integer
+ thresholds = float.(thresholds)
+ ELTYPE = typeof(first(thresholds))
+ end
+
+ taper_start = convert(ELTYPE, interface_taper_start)
+ taper_width = convert(ELTYPE, support_taper_width)
+ contact_model_ = convert_contact_model(contact_model, ELTYPE)
+ return ColorfieldSurfaceNormal(thresholds..., taper_start, taper_width, contact_model_,
+ normal_smoothing)
+end
+
+@inline function cubic_smoothstep(value)
+ value <= zero(value) && return zero(value)
+ value >= one(value) && return one(value)
+ return value^2 * (3 - 2value)
+end
+
+@inline function gradient_interface_activity(normal_norm, support_radius,
+ surface_normal_method::ColorfieldSurfaceNormal)
+ threshold = surface_normal_method.interface_threshold
+ dimensionless_norm = support_radius * normal_norm
+ if iszero(threshold)
+ return iszero(dimensionless_norm) ? zero(dimensionless_norm) :
+ one(dimensionless_norm)
+ end
+
+ lower_bound = surface_normal_method.interface_taper_start * threshold
+ transition_coordinate = (dimensionless_norm - lower_bound) /
+ (threshold - lower_bound)
+ return cubic_smoothstep(transition_coordinate)
+end
+
+@inline function support_interface_activity(support_moment,
+ surface_normal_method::ColorfieldSurfaceNormal)
+ threshold = surface_normal_method.ideal_density_threshold
+ iszero(threshold) && return one(support_moment)
+
+ transition_coordinate = (support_moment - threshold) /
+ surface_normal_method.support_taper_width
+ return one(support_moment) - cubic_smoothstep(transition_coordinate)
+end
+
+@inline function surface_support_moment(system, ::SurfaceTensionMorris, particle)
+ return @inbounds system.cache.support_moment[particle]
+end
+
+@inline function surface_support_moment(system, ::SurfaceTensionMomentumMorris, particle)
+ return @inbounds system.cache.divergence_correction[particle]
+end
+
+@inline function surface_interface_activity(system, particle)
+ return surface_interface_activity(surface_tension_model(system), system, particle)
+end
+
+@inline function surface_interface_activity(::Union{SurfaceTensionMorris,
+ SurfaceTensionMomentumMorris},
+ system, particle)
+ return @inbounds system.cache.interface_activity[particle]
+end
+
+@inline function surface_interface_activity(surface_tension, system, particle)
+ normal = surface_normal(system, particle)
+ return dot(normal, normal) > eps(eltype(normal)) ? one(eltype(normal)) :
+ zero(eltype(normal))
+end
+
function create_cache_surface_normal(surface_normal_method, ELTYPE, NDIMS, nparticles)
return (;)
end
-function create_cache_surface_normal(::ColorfieldSurfaceNormal, ELTYPE, NDIMS, nparticles)
+function create_cache_surface_normal(method::ColorfieldSurfaceNormal, ELTYPE, NDIMS,
+ nparticles)
surface_normal = Array{ELTYPE, 2}(undef, NDIMS, nparticles)
neighbor_count = Array{ELTYPE, 1}(undef, nparticles)
colorfield = Array{ELTYPE, 1}(undef, nparticles)
correction_factor = Array{ELTYPE, 1}(undef, nparticles)
- return (; surface_normal, neighbor_count, colorfield, correction_factor)
+ cache = (; surface_normal, neighbor_count, colorfield, correction_factor)
+ method.normal_smoothing || return cache
+
+ smoothed_surface_normal = Array{ELTYPE, 2}(undef, NDIMS, nparticles)
+ normal_smoothing_weight = Array{ELTYPE, 1}(undef, nparticles)
+ return (; cache..., smoothed_surface_normal, normal_smoothing_weight)
+end
+
+function create_cache_surface_normal(::CorrectedCSFSurfaceNormal, ELTYPE, NDIMS, nparticles)
+ surface_normal = Array{ELTYPE, 2}(undef, NDIMS, nparticles)
+ neighbor_count = Array{ELTYPE, 1}(undef, nparticles)
+ correction_factor = Array{ELTYPE, 1}(undef, nparticles)
+ ccsf_correction_matrix = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, nparticles)
+ ccsf_minimum_eigenvalue = Array{ELTYPE, 1}(undef, nparticles)
+ ccsf_lambda_gradient = Array{ELTYPE, 2}(undef, NDIMS, nparticles)
+ ccsf_color_gradient = Array{ELTYPE, 2}(undef, NDIMS, nparticles)
+ ccsf_shepard_sum = Array{ELTYPE, 1}(undef, nparticles)
+ return (; surface_normal, neighbor_count, correction_factor,
+ ccsf_correction_matrix, ccsf_minimum_eigenvalue,
+ ccsf_lambda_gradient, ccsf_color_gradient, ccsf_shepard_sum)
+end
+
+function create_cache_surface_normal(method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ ELTYPE, NDIMS, nparticles)
+ cache = create_cache_surface_normal(ColorfieldSurfaceNormal(;
+ normal_smoothing=method.normal_smoothing),
+ ELTYPE, NDIMS, nparticles)
+ wetted_area_density_conjugate = zeros(ELTYPE, nparticles)
+ wetted_area_energy = Ref(zero(ELTYPE))
+ wetted_area_raw_area = Ref(zero(ELTYPE))
+ wetted_area = Ref(zero(ELTYPE))
+ wetted_area_normalized_edge_shift = Ref(convert(ELTYPE, NaN))
+ wetted_area_evaluations = Ref(0)
+ return (; cache..., wetted_area_density_conjugate, wetted_area_energy,
+ wetted_area_raw_area, wetted_area, wetted_area_normalized_edge_shift,
+ wetted_area_evaluations)
+end
+
+@inline wetted_area_smoothstep_derivative(value) = 6value * (1 - value)
+
+@inline function wetted_area_contact_cosine(contact_model::WettedAreaContactAngle)
+ contact_model.contact_angle == 90 && return zero(contact_model.contact_angle)
+ return cosd(contact_model.contact_angle)
+end
+
+@inline function wetted_area_coefficient(surface_tension,
+ contact_model::WettedAreaContactAngle)
+ contact_cosine = wetted_area_contact_cosine(contact_model)
+ iszero(contact_cosine) && return zero(surface_tension.surface_tension_coefficient)
+ return surface_tension.surface_tension_coefficient * contact_cosine
+end
+
+function wetted_area_halfspace_reference(::WendlandC2Kernel{3}, normalized_distance)
+ distance = clamp(normalized_distance, zero(normalized_distance),
+ convert(typeof(normalized_distance), 2))
+ distance >= 2 && return zero(distance)
+
+ # Integral of the normalized three-dimensional kernel over a spherical cap.
+ coefficients = (one(distance), zero(distance), -5one(distance) / 2,
+ 5one(distance) / 2, -15one(distance) / 16,
+ one(distance) / 8)
+ upper = convert(typeof(distance), 2)
+ integral = zero(distance)
+ for power in 0:5
+ coefficient = coefficients[power + 1]
+ integral += coefficient *
+ ((upper^(power + 3) - distance^(power + 3)) / (power + 3) -
+ distance * (upper^(power + 2) - distance^(power + 2)) /
+ (power + 2))
+ end
+ return 21integral / 8
+end
+
+function canonical_wetted_area_edge_shift(smoothing_kernel, cells_per_h, contact_angle;
+ quadrature_cells_per_h=64)
+ contact_sine = sind(contact_angle)
+ abs(contact_sine) > sqrt(eps(typeof(contact_sine))) || return zero(contact_sine)
+ contact_cotangent = cosd(contact_angle) / contact_sine
+ lattice_spacing = inv(convert(typeof(cells_per_h), quadrature_cells_per_h))
+ support = compact_support(smoothing_kernel, one(cells_per_h))
+ search_radius = ceil(Int, support / lattice_spacing)
+ boundary_distance = inv(2cells_per_h)
+ thresholds = typeof(cells_per_h)[]
+ weights = typeof(cells_per_h)[]
+
+ for z_offset in (-search_radius):search_radius,
+ x_offset in (-search_radius):search_radius
+ planar_distance2 = lattice_spacing^2 * (x_offset^2 + z_offset^2)
+ planar_distance2 < support^2 || continue
+ reduced_kernel = zero(cells_per_h)
+ for tangent_offset in (-search_radius):search_radius
+ distance = lattice_spacing *
+ sqrt(x_offset^2 + tangent_offset^2 + z_offset^2)
+ distance < support || continue
+ reduced_kernel += lattice_spacing * kernel(smoothing_kernel, distance,
+ one(cells_per_h))
+ end
+ source_z = -boundary_distance - z_offset * lattice_spacing
+ source_z > 0 || continue
+ push!(thresholds, x_offset * lattice_spacing + contact_cotangent * source_z)
+ push!(weights, lattice_spacing^2 * reduced_kernel)
+ end
+
+ order = sortperm(thresholds)
+ thresholds = thresholds[order]
+ weights = weights[order]
+ reference = sum(weights)
+ reference > eps(reference) || return zero(reference)
+ breaks = sort!(unique!([thresholds; zero(cells_per_h)]))
+ cumulative = zero(reference)
+ event = 1
+ shift = zero(reference)
+ for interval in 1:(length(breaks) - 1)
+ left = breaks[interval]
+ right = breaks[interval + 1]
+ while event <= length(thresholds) && thresholds[event] <= left
+ cumulative += weights[event]
+ event += 1
+ end
+ fraction = clamp(cumulative / reference, 0, 1)
+ step = (left + right) / 2 > 0 ? one(reference) : zero(reference)
+ shift += (right - left) * (cubic_smoothstep(fraction) - step)
+ end
+ return shift
+end
+
+@inline function wetted_area_boundary_cache(system)
+ hasproperty(system, :boundary_model) || return nothing
+ model = system.boundary_model
+ hasproperty(model, :cache) || return nothing
+ haskey(model.cache, :surface_measure) || return nothing
+ return model.cache
+end
+
+@inline wetted_area_supported_fluid(system) = false
+
+@inline function check_wetted_area_configuration!(system, surface_normal_method, systems)
+ return system
+end
+
+function check_wetted_area_configuration!(system::AbstractFluidSystem,
+ surface_normal_method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ systems)
+ ndims(system) == 3 ||
+ throw(ArgumentError("`WettedAreaContactAngle` currently supports only 3D fluids"))
+ wetted_area_supported_fluid(system) ||
+ throw(ArgumentError("`WettedAreaContactAngle` currently supports only WCSPH and EDAC fluids"))
+ density_calculator(system) isa ContinuityDensity ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires `ContinuityDensity`"))
+ system.smoothing_kernel isa WendlandC2Kernel{3} ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires `WendlandC2Kernel{3}`"))
+ system.surface_tension isa SurfaceTensionMomentumMorris ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires `SurfaceTensionMomentumMorris`"))
+ isfinite(surface_normal_method.boundary_contact_threshold) ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires a finite `boundary_contact_threshold`"))
+ system.cache.color == 1 ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires the fluid `color_value` to be 1"))
+
+ particle_spacing = system.cache.reference_particle_spacing
+ cells_per_h = initial_smoothing_length(system) / particle_spacing
+ isapprox(cells_per_h, convert(typeof(cells_per_h), 1.4);
+ rtol=100eps(typeof(cells_per_h)), atol=zero(cells_per_h)) ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires `smoothing_length / reference_particle_spacing == 1.4`"))
+
+ fluid_count = 0
+ boundary_count = 0
+ foreach_system(systems) do candidate
+ if candidate isa AbstractFluidSystem
+ fluid_count += 1
+ return
+ end
+
+ valid_boundary = (candidate isa WallBoundarySystem ||
+ candidate isa RigidBodySystem) &&
+ hasproperty(candidate, :boundary_model) &&
+ candidate.boundary_model isa BoundaryModelDummyParticles
+ valid_boundary ||
+ throw(ArgumentError("`WettedAreaContactAngle` supports only dummy-particle wall and rigid-body neighbors"))
+ candidate.cache.color == 0 ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires contact-boundary `color_value` to be 0"))
+ boundary_count += 1
+ initialize_wetted_area_boundary!(system, candidate)
+ end
+ fluid_count == 1 ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires exactly one fluid system"))
+ boundary_count > 0 ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires at least one contact boundary"))
+
+ cache = system.cache
+ if isnan(cache.wetted_area_normalized_edge_shift[])
+ cache.wetted_area_normalized_edge_shift[] = canonical_wetted_area_edge_shift(system.smoothing_kernel,
+ cells_per_h,
+ surface_normal_method.contact_model.contact_angle)
+ end
+ return system
+end
+
+function initialize_wetted_area_boundary!(fluid_system, boundary_system)
+ cache = wetted_area_boundary_cache(boundary_system)
+ isnothing(cache) &&
+ throw(ArgumentError("contact boundaries require explicit per-particle `surface_measure` values"))
+ haskey(cache, :initial_colorfield) ||
+ throw(ArgumentError("contact boundaries require a positive `reference_particle_spacing`"))
+
+ normals = boundary_system.initial_condition.normals
+ isnothing(normals) &&
+ throw(ArgumentError("contact boundaries require `InitialCondition.normals`"))
+ surface_measure = cache.surface_measure
+ contact_model = fluid_system.surface_normal_method.contact_model
+ cache.wetted_area_active[] = !iszero(wetted_area_coefficient(fluid_system.surface_tension,
+ contact_model))
+ active_particles = findall(>(zero(eltype(surface_measure))), surface_measure)
+ isempty(active_particles) &&
+ throw(ArgumentError("each contact boundary requires at least one positive `surface_measure`"))
+ validate_wetted_area_patch_connectivity(boundary_system.initial_condition,
+ surface_measure, active_particles)
+
+ particle_spacing = fluid_system.cache.reference_particle_spacing
+ first_particle = first(eachparticle(fluid_system))
+ particle_volume = fluid_system.initial_condition.mass[first_particle] /
+ fluid_system.initial_condition.density[first_particle]
+ volume_scale = particle_volume / particle_spacing^3
+ for particle in eachparticle(fluid_system)
+ volume = fluid_system.initial_condition.mass[particle] /
+ fluid_system.initial_condition.density[particle]
+ isapprox(volume / particle_spacing^3, volume_scale;
+ rtol=100eps(typeof(volume_scale)), atol=zero(volume_scale)) ||
+ throw(ArgumentError("`WettedAreaContactAngle` requires uniform reference fluid particle volumes"))
+ end
+
+ smoothing_length = initial_smoothing_length(fluid_system)
+ support = compact_support(fluid_system.smoothing_kernel, one(smoothing_length))
+ set_zero!(cache.wetted_area_flooded_reference)
+ for particle in active_particles
+ normal = extract_svector(normals, boundary_system, particle)
+ all(isfinite, normal) ||
+ throw(ArgumentError("contact-boundary normals must be finite"))
+ normalized_offset = norm(normal) / smoothing_length
+ 0 < normalized_offset < support ||
+ throw(ArgumentError("the magnitude of each active contact-boundary normal must place the physical surface inside the kernel support"))
+ reference = volume_scale *
+ wetted_area_halfspace_reference(fluid_system.smoothing_kernel,
+ normalized_offset)
+ reference > eps(reference) ||
+ throw(ArgumentError("contact-boundary flooded colorfield references must be positive"))
+ cache.wetted_area_flooded_reference[particle] = reference
+ end
+ return boundary_system
+end
+
+function validate_wetted_area_patch_connectivity(initial_condition, surface_measure,
+ active_particles)
+ length(active_particles) == 1 && return initial_condition
+ spacing = initial_condition.particle_spacing
+ area_spacing = sqrt(maximum(surface_measure))
+ length_scale = max(spacing > 0 ? spacing : zero(spacing), area_spacing)
+ length_scale > 0 ||
+ throw(ArgumentError("positive contact surface measures must define a finite patch scale"))
+ connection_radius2 = (1.75length_scale)^2
+ coordinates = initial_condition.coordinates
+ visited = falses(length(surface_measure))
+ queue = [first(active_particles)]
+ visited[first(queue)] = true
+ next_particle = 1
+ while next_particle <= length(queue)
+ particle = queue[next_particle]
+ next_particle += 1
+ for neighbor in active_particles
+ visited[neighbor] && continue
+ distance2 = zero(eltype(coordinates))
+ for dim in axes(coordinates, 1)
+ distance2 += (coordinates[dim, particle] -
+ coordinates[dim, neighbor])^2
+ end
+ distance2 <= connection_radius2 || continue
+ visited[neighbor] = true
+ push!(queue, neighbor)
+ end
+ end
+ all(visited[active_particles]) ||
+ throw(ArgumentError("each contact boundary must contain one connected wetted-area patch"))
+ return initial_condition
+end
+
+@inline function prepare_wetted_area_boundary!(system, neighbor_system,
+ surface_normal_method)
+ return system
+end
+
+function prepare_wetted_area_boundary!(system::AbstractFluidSystem, neighbor_system,
+ surface_normal_method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle})
+ boundary_cache = wetted_area_boundary_cache(neighbor_system)
+ isnothing(boundary_cache) && return system
+ (; surface_measure, wetted_area_flooded_reference, wetted_area_weight,
+ colorfield) = boundary_cache
+ set_zero!(wetted_area_weight)
+
+ raw_area = zero(eltype(system))
+ for particle in eachparticle(neighbor_system)
+ measure = surface_measure[particle]
+ iszero(measure) && continue
+ reference = wetted_area_flooded_reference[particle]
+ fraction = clamp(colorfield[particle] / reference, zero(reference), one(reference))
+ raw_area += measure * cubic_smoothstep(fraction)
+ end
+
+ pi_ = convert(eltype(system), pi)
+ raw_radius = sqrt(raw_area / pi_)
+ edge_shift = system.cache.wetted_area_normalized_edge_shift[] *
+ initial_smoothing_length(system)
+ corrected_radius = max(raw_radius - edge_shift, zero(raw_radius))
+ corrected_area = pi_ * corrected_radius^2
+ area_derivative = raw_radius > eps(raw_radius) ? corrected_radius / raw_radius :
+ zero(raw_radius)
+ system.cache.wetted_area_raw_area[] += raw_area
+ system.cache.wetted_area[] += corrected_area
+
+ coefficient = wetted_area_coefficient(surface_tension_model(system),
+ surface_normal_method.contact_model)
+ iszero(coefficient) && return system
+ for particle in eachparticle(neighbor_system)
+ measure = surface_measure[particle]
+ iszero(measure) && continue
+ reference = wetted_area_flooded_reference[particle]
+ fraction = colorfield[particle] / reference
+ 0 < fraction < 1 || continue
+ wetted_area_weight[particle] = area_derivative * measure / reference *
+ wetted_area_smoothstep_derivative(fraction)
+ end
+ return system
+end
+
+@inline function accumulate_wetted_area_density_conjugate!(system, neighbor_system,
+ surface_normal_method,
+ particle, neighbor, distance)
+ return system
+end
+
+@inline function accumulate_wetted_area_density_conjugate!(system::AbstractFluidSystem,
+ neighbor_system,
+ ::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ particle, neighbor, distance)
+ boundary_cache = wetted_area_boundary_cache(neighbor_system)
+ isnothing(boundary_cache) && return system
+ weight = @inbounds boundary_cache.wetted_area_weight[neighbor]
+ iszero(weight) && return system
+ kernel_value = smoothing_kernel(system, distance, particle)
+ @inbounds system.cache.wetted_area_density_conjugate[particle] += weight * kernel_value
+ return system
+end
+
+@inline function reset_wetted_area_contact!(system, surface_normal_method)
+ return system
+end
+
+@inline function reset_wetted_area_contact!(system,
+ ::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle})
+ set_zero!(system.cache.wetted_area_density_conjugate)
+ system.cache.wetted_area_energy[] = zero(eltype(system))
+ system.cache.wetted_area_raw_area[] = zero(eltype(system))
+ system.cache.wetted_area[] = zero(eltype(system))
+ return system
+end
+
+@inline function finalize_wetted_area_contact!(system, surface_normal_method, v)
+ return system
+end
+
+function finalize_wetted_area_contact!(system::AbstractFluidSystem,
+ surface_normal_method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ v)
+ coefficient = wetted_area_coefficient(surface_tension_model(system),
+ surface_normal_method.contact_model)
+ area = system.cache.wetted_area[]
+ system.cache.wetted_area_energy[] = iszero(coefficient) ? zero(coefficient) :
+ -coefficient * area
+ if iszero(coefficient)
+ set_zero!(system.cache.wetted_area_density_conjugate)
+ else
+ for particle in each_integrated_particle(system)
+ density = current_density(v, system, particle)
+ @inbounds system.cache.wetted_area_density_conjugate[particle] *= coefficient /
+ density^2
+ end
+ end
+ system.cache.wetted_area_evaluations[] += 1
+ return system
+end
+
+@inline function wetted_area_density_acceleration(surface_normal_method, particle_system,
+ neighbor_system, particle, neighbor,
+ rho_a, rho_b, m_b, grad_kernel)
+ return zero(grad_kernel)
+end
+
+@inline function wetted_area_density_acceleration(surface_normal_method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ particle_system::AbstractFluidSystem,
+ neighbor_system::AbstractFluidSystem,
+ particle, neighbor, rho_a, rho_b, m_b,
+ grad_kernel)
+ particle_system === neighbor_system || return zero(grad_kernel)
+ conjugate_a = @inbounds particle_system.cache.wetted_area_density_conjugate[particle]
+ conjugate_b = @inbounds neighbor_system.cache.wetted_area_density_conjugate[neighbor]
+ pair_coefficient = conjugate_a * rho_a / rho_b + conjugate_b * rho_b / rho_a
+ iszero(pair_coefficient) && return zero(grad_kernel)
+ return -m_b * pair_coefficient * grad_kernel
+end
+
+@inline function wetted_area_explicit_acceleration(surface_tension, surface_normal_method,
+ particle_system, neighbor_system,
+ particle, neighbor, m_a, rho_a,
+ grad_kernel)
+ return zero(grad_kernel)
+end
+
+@inline function wetted_area_explicit_acceleration(surface_tension,
+ surface_normal_method::ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle},
+ particle_system::AbstractFluidSystem,
+ neighbor_system, particle, neighbor,
+ m_a, rho_a, grad_kernel)
+ boundary_cache = wetted_area_boundary_cache(neighbor_system)
+ isnothing(boundary_cache) && return zero(grad_kernel)
+ weight = @inbounds boundary_cache.wetted_area_weight[neighbor]
+ iszero(weight) && return zero(grad_kernel)
+ coefficient = wetted_area_coefficient(surface_tension,
+ surface_normal_method.contact_model)
+ iszero(coefficient) && return zero(grad_kernel)
+ acceleration = coefficient / rho_a * weight * grad_kernel
+ if neighbor_system isa WallBoundarySystem
+ thread = Threads.threadid()
+ reaction_buffer = boundary_cache.wetted_area_reaction_buffer
+ for dim in eachindex(acceleration)
+ @inbounds reaction_buffer[dim, neighbor, thread] -= m_a * acceleration[dim]
+ end
+ end
+ return acceleration
end
@inline function surface_normal(particle_system::AbstractFluidSystem, particle)
@@ -38,6 +690,22 @@ end
return extract_svector(cache.surface_normal, particle_system, particle)
end
+@inline function surface_tension_normal(particle_system::AbstractFluidSystem, particle)
+ return surface_tension_normal(surface_normal_method(particle_system), particle_system,
+ particle)
+end
+
+@inline function surface_tension_normal(surface_normal_method, particle_system, particle)
+ return surface_normal(particle_system, particle)
+end
+
+@inline function surface_tension_normal(method::ColorfieldSurfaceNormal, particle_system,
+ particle)
+ method.normal_smoothing || return surface_normal(particle_system, particle)
+ return extract_svector(particle_system.cache.smoothed_surface_normal,
+ particle_system, particle)
+end
+
function calc_normal!(system, neighbor_system, u_system, v, v_neighbor_system,
u_neighbor_system, semi, surface_normal_method,
neighbor_surface_normal_method)
@@ -67,6 +735,8 @@ function calc_normal!(system::AbstractFluidSystem, neighbor_system::AbstractFlui
for i in 1:ndims(system)
cache.surface_normal[i, particle] += m_b / density_neighbor * grad_kernel[i]
end
+ accumulate_surface_support_moment!(system, surface_tension_model(system), particle,
+ m_b / density_neighbor, pos_diff, grad_kernel)
cache.neighbor_count[particle] += 1
end
@@ -74,11 +744,55 @@ function calc_normal!(system::AbstractFluidSystem, neighbor_system::AbstractFlui
return system
end
+@inline function accumulate_surface_support_moment!(system, surface_tension, particle,
+ volume, pos_diff, grad_kernel)
+ return system
+end
+
+@inline function accumulate_surface_support_moment!(system, ::SurfaceTensionMorris,
+ particle, volume, pos_diff,
+ grad_kernel)
+ value = -volume * dot(pos_diff, grad_kernel) / ndims(system)
+ @inbounds system.cache.support_moment[particle] += value
+ return system
+end
+
+@inline function accumulate_surface_support_moment!(system,
+ ::SurfaceTensionMomentumMorris,
+ particle, volume, pos_diff,
+ grad_kernel)
+ value = -volume * dot(pos_diff, grad_kernel) / ndims(system)
+ @inbounds system.cache.divergence_correction[particle] += value
+ return system
+end
+
+@inline function accumulate_boundary_surface_support_moment!(system, surface_tension,
+ neighbor_system,
+ v_neighbor_system, particle,
+ neighbor, pos_diff, distance)
+ return system
+end
+
+@inline function accumulate_boundary_surface_support_moment!(system,
+ surface_tension::Union{SurfaceTensionMorris,
+ SurfaceTensionMomentumMorris},
+ neighbor_system,
+ v_neighbor_system, particle,
+ neighbor, pos_diff, distance)
+ m_b = hydrodynamic_mass(neighbor_system, neighbor)
+ density_neighbor = current_density(v_neighbor_system, neighbor_system, neighbor)
+ grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+ accumulate_surface_support_moment!(system, surface_tension, particle,
+ m_b / density_neighbor, pos_diff, grad_kernel)
+ return system
+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_boundary_normal!(system::AbstractFluidSystem, neighbor_system, u_system, v,
- u_neighbor_system, semi, surface_normal_method)
+ v_neighbor_system, u_neighbor_system, semi,
+ surface_normal_method)
(; cache) = system
(; colorfield, initial_colorfield) = neighbor_system.boundary_model.cache
(; boundary_contact_threshold) = surface_normal_method
@@ -103,10 +817,18 @@ function calc_boundary_normal!(system::AbstractFluidSystem, neighbor_system, u_s
end
maximum_colorfield = maximum(colorfield)
+ prepare_wetted_area_boundary!(system, neighbor_system, surface_normal_method)
foreach_point_neighbor(system, neighbor_system,
system_coords, neighbor_system_coords,
semi) do particle, neighbor, pos_diff, distance
+ accumulate_boundary_surface_support_moment!(system, surface_tension_model(system),
+ neighbor_system, v_neighbor_system,
+ particle, neighbor, pos_diff, distance)
+ accumulate_wetted_area_density_conjugate!(system, neighbor_system,
+ surface_normal_method, particle,
+ neighbor, distance)
+
# We assume that we are in contact with the boundary if the color of the boundary particle
# is larger than the threshold
if colorfield[neighbor] / maximum_colorfield > boundary_contact_threshold
@@ -126,8 +848,8 @@ end
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)
- return calc_boundary_normal!(system, neighbor_system, u_system, v, u_neighbor_system,
- semi, surface_normal_method)
+ return calc_boundary_normal!(system, neighbor_system, u_system, v, v_neighbor_system,
+ u_neighbor_system, semi, surface_normal_method)
end
function remove_invalid_normals!(system::AbstractFluidSystem, surface_tension,
@@ -145,46 +867,112 @@ function remove_invalid_normals!(system::AbstractFluidSystem, surface_tension,
return system
end
-# See Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics"
function remove_invalid_normals!(system::AbstractFluidSystem,
surface_tension::Union{SurfaceTensionMorris,
SurfaceTensionMomentumMorris},
surface_normal_method::ColorfieldSurfaceNormal)
(; cache, smoothing_kernel) = system
- (; ideal_density_threshold, interface_threshold) = surface_normal_method
- (; neighbor_count) = cache
-
- smoothing_length_ = initial_smoothing_length(system)
-
- # We remove invalid normals i.e. they have a small norm (eq. 20)
- normal_condition2 = (interface_threshold /
- compact_support(smoothing_kernel, smoothing_length_))^2
+ support_radius = compact_support(smoothing_kernel, initial_smoothing_length(system))
for particle in each_integrated_particle(system)
+ cache.delta_s[particle] = zero(eltype(system))
+ cache.interface_activity[particle] = zero(eltype(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.
- if ideal_density_threshold > 0 &&
- ideal_density_threshold *
- ideal_neighbor_count(Val(ndims(system)), cache.reference_particle_spacing,
- compact_support(smoothing_kernel, smoothing_length_)) <
- neighbor_count[particle]
+ particle_surface_normal = surface_normal(system, particle)
+ norm2 = dot(particle_surface_normal, particle_surface_normal)
+ if !(norm2 > eps(norm2))
cache.surface_normal[1:ndims(system), particle] .= 0
continue
end
- particle_surface_normal = surface_normal(system, particle)
- norm2 = dot(particle_surface_normal, particle_surface_normal)
-
- # See eq. 21
- if norm2 > normal_condition2
- cache.surface_normal[1:ndims(system),
- particle] = particle_surface_normal / sqrt(norm2)
- else
+ normal_norm = sqrt(norm2)
+ gradient_activity = gradient_interface_activity(normal_norm, support_radius,
+ surface_normal_method)
+ support_moment = surface_support_moment(system, surface_tension, particle)
+ support_activity = support_interface_activity(support_moment,
+ surface_normal_method)
+ activity = gradient_activity * support_activity
+ if !(activity > zero(activity))
cache.surface_normal[1:ndims(system), particle] .= 0
+ continue
+ end
+
+ cache.interface_activity[particle] = activity
+ # A one-phase free surface samples one half of the kernel-smoothed interface.
+ cache.delta_s[particle] = 2 * normal_norm * activity
+ cache.surface_normal[1:ndims(system),
+ particle] = particle_surface_normal / normal_norm
+ end
+
+ return system
+end
+
+@inline reset_surface_interface_data!(system, surface_tension) = system
+
+@inline function reset_surface_interface_data!(system, ::SurfaceTensionMorris)
+ set_zero!(system.cache.support_moment)
+ set_zero!(system.cache.interface_activity)
+ set_zero!(system.cache.delta_s)
+ return system
+end
+
+@inline function reset_surface_interface_data!(system, ::SurfaceTensionMomentumMorris)
+ set_zero!(system.cache.divergence_correction)
+ set_zero!(system.cache.interface_activity)
+ set_zero!(system.cache.delta_s)
+ return system
+end
+
+@inline function smooth_surface_normals!(system, surface_normal_method, v, u, semi)
+ return system
+end
+
+function smooth_surface_normals!(system::AbstractFluidSystem,
+ surface_normal_method::ColorfieldSurfaceNormal,
+ v, u, semi)
+ surface_normal_method.normal_smoothing || return system
+ cache = system.cache
+ normal_sum = cache.smoothed_surface_normal
+ weight_sum = cache.normal_smoothing_weight
+ coordinates = current_coordinates(u, system)
+ set_zero!(normal_sum)
+ set_zero!(weight_sum)
+
+ @trixi_timeit timer() "smooth surface normals" begin
+ foreach_point_neighbor(system, system, coordinates, coordinates, semi;
+ points=each_integrated_particle(system)) do particle,
+ neighbor,
+ pos_diff,
+ distance
+ target_activity = surface_interface_activity(system, particle)
+ target_activity > zero(target_activity) || return
+ activity = surface_interface_activity(system, neighbor)
+ activity > zero(activity) || return
+ volume = hydrodynamic_mass(system, neighbor) /
+ current_density(v, system, neighbor)
+ weight = activity * volume * smoothing_kernel(system, distance, particle)
+ normal = surface_normal(system, neighbor)
+ for dimension in 1:ndims(system)
+ @inbounds normal_sum[dimension, particle] += weight * normal[dimension]
+ end
+ @inbounds weight_sum[particle] += weight
end
end
+ for particle in each_integrated_particle(system)
+ surface_interface_activity(system, particle) > zero(eltype(system)) || continue
+ weight = @inbounds weight_sum[particle]
+ normal = extract_svector(normal_sum, system, particle)
+ normal_norm = norm(normal)
+ raw_normal = surface_normal(system, particle)
+ use_smoothed_normal = weight > eps(weight) && normal_norm > eps(normal_norm)
+ for dimension in 1:ndims(system)
+ @inbounds normal_sum[dimension,
+ particle] = use_smoothed_normal ?
+ normal[dimension] / normal_norm :
+ raw_normal[dimension]
+ end
+ end
return system
end
@@ -200,6 +988,8 @@ function compute_surface_normal!(system::AbstractFluidSystem,
# Reset surface normal
set_zero!(cache.surface_normal)
set_zero!(cache.neighbor_count)
+ reset_surface_interface_data!(system, surface_tension)
+ reset_wetted_area_contact!(system, surface_normal_method_)
# TODO: if color values are set only different systems need to be called
@trixi_timeit timer() "compute surface normal" foreach_system(semi) do neighbor_system
@@ -210,7 +1000,133 @@ function compute_surface_normal!(system::AbstractFluidSystem,
u_neighbor_system, semi, surface_normal_method_,
surface_normal_method(neighbor_system))
end
+ finalize_wetted_area_contact!(system, surface_normal_method_, v)
remove_invalid_normals!(system, surface_tension, surface_normal_method_)
+ smooth_surface_normals!(system, surface_normal_method_, v, u, semi)
+
+ return system
+end
+
+@inline function ccsf_store_matrix!(matrix_cache, system, particle, matrix)
+ for column in 1:ndims(system), row in 1:ndims(system)
+ @inbounds matrix_cache[row, column, particle] = matrix[row, column]
+ end
+ return matrix_cache
+end
+
+@inline function ccsf_minimum_eigenvalue(matrix)
+ # `eigmin` falls back to an allocating dense eigensolver for static matrices.
+ symmetric_matrix = (matrix + transpose(matrix)) / 2
+ return minimum(eigvals(Symmetric(symmetric_matrix)))
+end
+
+@inline function ccsf_corrected_divergence(normal_difference, renormalization,
+ kernel_direction)
+ return dot(renormalization * normal_difference, kernel_direction)
+end
+
+@inline function ccsf_lambda_difference(lambda_i, lambda_j)
+ return lambda_i >= oftype(lambda_i, 0.7) ? lambda_j - lambda_i : lambda_j
+end
+
+function compute_surface_normal!(system::AbstractFluidSystem,
+ ::CorrectedCSFSurfaceNormal,
+ v, u, v_ode, u_ode, semi, t)
+ system.surface_tension isa SurfaceTensionMorris ||
+ throw(ArgumentError("`CorrectedCSFSurfaceNormal` requires `SurfaceTensionMorris`"))
+ cache = system.cache
+ matrix_cache = cache.ccsf_correction_matrix
+ lambda = cache.ccsf_minimum_eigenvalue
+ lambda_gradient = cache.ccsf_lambda_gradient
+ color_gradient = cache.ccsf_color_gradient
+ shepard_sum = cache.ccsf_shepard_sum
+ coordinates = current_coordinates(u, system)
+
+ set_zero!(cache.surface_normal)
+ set_zero!(cache.neighbor_count)
+ set_zero!(matrix_cache)
+ set_zero!(lambda)
+ set_zero!(lambda_gradient)
+ set_zero!(color_gradient)
+ set_zero!(shepard_sum)
+ set_zero!(cache.support_moment)
+
+ @trixi_timeit timer() "compute C-CSF moments" begin
+ foreach_point_neighbor(system, system, coordinates, coordinates, semi;
+ points=each_integrated_particle(system)) do particle,
+ neighbor,
+ pos_diff,
+ distance
+ m_b = hydrodynamic_mass(system, neighbor)
+ rho_b = current_density(v, system, neighbor)
+ volume_b = m_b / rho_b
+ grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+ kernel = smoothing_kernel(system, distance, particle)
+
+ moment = -volume_b * grad_kernel * permutedims(pos_diff)
+ for column in 1:ndims(system), row in 1:ndims(system)
+ @inbounds matrix_cache[row, column, particle] += moment[row, column]
+ end
+ for dimension in 1:ndims(system)
+ @inbounds color_gradient[dimension,
+ particle] += volume_b * grad_kernel[dimension]
+ end
+ @inbounds shepard_sum[particle] += volume_b * kernel
+ @inbounds cache.neighbor_count[particle] += 1
+ end
+ end
+
+ @threaded semi for particle in each_integrated_particle(system)
+ inverse_renormalization = extract_smatrix(matrix_cache, system, particle)
+ @inbounds lambda[particle] = ccsf_minimum_eigenvalue(inverse_renormalization)
+ renormalization = abs(det(inverse_renormalization)) < 1.0f-9 ?
+ one(inverse_renormalization) : inv(inverse_renormalization)
+ ccsf_store_matrix!(matrix_cache, system, particle, renormalization)
+ end
+
+ @trixi_timeit timer() "compute C-CSF normal" begin
+ foreach_point_neighbor(system, system, coordinates, coordinates, semi;
+ points=each_integrated_particle(system)) do particle,
+ neighbor,
+ pos_diff,
+ distance
+ rho_b = current_density(v, system, neighbor)
+ volume_b = hydrodynamic_mass(system, neighbor) / rho_b
+ grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+ renormalization = extract_smatrix(matrix_cache, system, particle)
+ lambda_a = @inbounds lambda[particle]
+ lambda_b = @inbounds lambda[neighbor]
+ coefficient = ccsf_lambda_difference(lambda_a, lambda_b)
+ contribution = coefficient * volume_b * renormalization * grad_kernel
+ for dimension in 1:ndims(system)
+ @inbounds lambda_gradient[dimension, particle] += contribution[dimension]
+ end
+ end
+ end
+
+ set_zero!(cache.interface_activity)
+ set_zero!(cache.delta_s)
+ for particle in each_integrated_particle(system)
+ gradient = extract_svector(lambda_gradient, system, particle)
+ gradient_norm = norm(gradient)
+ lambda_i = @inbounds lambda[particle]
+ threshold = oftype(lambda_i, 0.1) * lambda_i /
+ smoothing_length(system, particle)
+ if gradient_norm > threshold
+ normal = -gradient / gradient_norm
+ for dimension in 1:ndims(system)
+ @inbounds cache.surface_normal[dimension, particle] = normal[dimension]
+ end
+ @inbounds cache.interface_activity[particle] = one(lambda_i)
+ end
+
+ raw_gradient = extract_svector(color_gradient, system, particle)
+ shepard = @inbounds shepard_sum[particle]
+ correction = shepard > eps(shepard) ?
+ max(one(shepard), inv(2shepard)) : one(shepard)
+ @inbounds cache.delta_s[particle] = 2correction * norm(raw_gradient)
+ @inbounds cache.support_moment[particle] = lambda_i
+ end
return system
end
@@ -231,38 +1147,80 @@ function calc_curvature!(system::AbstractFluidSystem, neighbor_system::AbstractF
system_coords = current_coordinates(u_system, system)
neighbor_system_coords = current_coordinates(u_neighbor_system, neighbor_system)
- set_zero!(correction_factor)
-
foreach_point_neighbor(system, neighbor_system,
system_coords, neighbor_system_coords,
semi) do particle, neighbor, pos_diff, distance
m_b = hydrodynamic_mass(neighbor_system, neighbor)
rho_b = current_density(v_neighbor_system, neighbor_system, neighbor)
- n_a = surface_normal(system, particle)
- n_b = surface_normal(neighbor_system, neighbor)
+ n_a = surface_tension_normal(system, particle)
+ n_b = surface_tension_normal(neighbor_system, neighbor)
v_b = m_b / rho_b
+ activity_a = surface_interface_activity(system, particle)
+ activity_b = surface_interface_activity(neighbor_system, neighbor)
- # Eq. 22: we can test against `eps()` here since the surface normals that are invalid have been removed
- if dot(n_a, n_a) > eps() && dot(n_b, n_b) > eps()
+ if activity_a > zero(activity_a) && activity_b > zero(activity_b)
w = smoothing_kernel(system, distance, particle)
grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+ weighted_volume = v_b * activity_b
for i in 1:ndims(system)
- curvature[particle] += v_b * (n_b[i] - n_a[i]) * grad_kernel[i]
+ curvature[particle] += weighted_volume * (n_b[i] - n_a[i]) *
+ grad_kernel[i]
end
- # Eq. 24
- correction_factor[particle] += v_b * w
+ correction_factor[particle] += weighted_volume * w
end
end
- # Eq. 23
- for particle in each_integrated_particle(system)
- curvature[particle] /= (correction_factor[particle] + eps())
- end
+ return system
+end
+function calc_curvature!(system::AbstractFluidSystem,
+ neighbor_system::AbstractFluidSystem,
+ u_system, v, v_neighbor_system, u_neighbor_system, semi,
+ ::CorrectedCSFSurfaceNormal,
+ ::CorrectedCSFSurfaceNormal)
+ system === neighbor_system ||
+ throw(ArgumentError("`CorrectedCSFSurfaceNormal` currently supports one fluid system"))
+ cache = system.cache
+ coordinates = current_coordinates(u_system, system)
+ cosine_threshold = -inv(convert(eltype(system), ndims(system)))
+
+ foreach_point_neighbor(system, system, coordinates, coordinates, semi;
+ points=each_integrated_particle(system)) do particle, neighbor,
+ pos_diff, distance
+ n_a = surface_normal(system, particle)
+ n_b = surface_normal(system, neighbor)
+ dot(n_a, n_a) > eps(eltype(n_a)) || return
+ dot(n_b, n_b) > eps(eltype(n_b)) || return
+ dot(n_a, n_b) >= cosine_threshold || return
+
+ rho_b = current_density(v, system, neighbor)
+ volume_b = hydrodynamic_mass(system, neighbor) / rho_b
+ grad_kernel = smoothing_kernel_grad(system, pos_diff, distance, particle)
+ renormalization = extract_smatrix(cache.ccsf_correction_matrix, system, particle)
+ @inbounds cache.curvature[particle] += volume_b *
+ ccsf_corrected_divergence(n_b - n_a,
+ renormalization,
+ grad_kernel)
+ end
return system
end
+@inline function normalized_surface_curvature(curvature_numerator, denominator)
+ denominator > sqrt(eps(typeof(denominator))) || return zero(curvature_numerator)
+ return curvature_numerator / denominator
+end
+
+@inline function finalize_surface_curvature(curvature_numerator, denominator,
+ surface_normal_method)
+ return normalized_surface_curvature(curvature_numerator, denominator)
+end
+
+@inline function finalize_surface_curvature(curvature_numerator, denominator,
+ ::CorrectedCSFSurfaceNormal)
+ return curvature_numerator
+end
+
function compute_curvature!(system, surface_tension, v, u, v_ode, u_ode, semi, t)
return system
end
@@ -271,17 +1229,25 @@ function compute_curvature!(system::AbstractFluidSystem,
surface_tension::SurfaceTensionMorris,
v, u, v_ode, u_ode, semi, t)
(; cache, surface_tension) = system
+ normal_method = surface_normal_method(system)
- # Reset surface curvature
+ # Reset once so contributions from multiple fluid systems accumulate consistently.
set_zero!(cache.curvature)
+ set_zero!(cache.correction_factor)
@trixi_timeit timer() "compute surface curvature" foreach_system(semi) do neighbor_system
u_neighbor_system = wrap_u(u_ode, neighbor_system, semi)
v_neighbor_system = wrap_v(v_ode, neighbor_system, semi)
calc_curvature!(system, neighbor_system, u, v, v_neighbor_system,
- u_neighbor_system, semi, surface_normal_method(system),
+ u_neighbor_system, semi, normal_method,
surface_normal_method(neighbor_system))
end
+
+ for particle in each_integrated_particle(system)
+ denominator = cache.correction_factor[particle]
+ cache.curvature[particle] = finalize_surface_curvature(cache.curvature[particle],
+ denominator, normal_method)
+ end
return system
end
diff --git a/src/schemes/fluid/surface_tension.jl b/src/schemes/fluid/surface_tension.jl
index 5656e95e12..a335e7d5fb 100644
--- a/src/schemes/fluid/surface_tension.jl
+++ b/src/schemes/fluid/surface_tension.jl
@@ -51,6 +51,11 @@ It calculates surface tension forces based on the curvature of the fluid interfa
using particle normals and their divergence, making it suitable for simulating
phenomena like droplet formation and capillary wave dynamics.
+The one-phase color-gradient magnitude is retained as a surface delta. The local
+continuum-surface-force acceleration is evaluated once per particle as
+``-sigma * kappa * delta_s * n_hat / rho``. A smooth interface activity avoids discrete normal
+and curvature-stencil switches when using [`ColorfieldSurfaceNormal`](@ref).
+
See [`surface_tension`](@ref) for more details.
@@ -72,23 +77,36 @@ end
function create_cache_surface_tension(::SurfaceTensionMorris, ELTYPE, NDIMS, nparticles)
curvature = Array{ELTYPE, 1}(undef, nparticles)
- return (; curvature)
+ delta_s = Array{ELTYPE, 1}(undef, nparticles)
+ interface_activity = Array{ELTYPE, 1}(undef, nparticles)
+ support_moment = Array{ELTYPE, 1}(undef, nparticles)
+ return (; curvature, delta_s, interface_activity, support_moment)
end
@doc raw"""
SurfaceTensionMomentumMorris(surface_tension_coefficient=1.0)
-This model implements the momentum-conserving surface tension approach outlined by Morris
-[Morris2000](@cite). It calculates surface tension forces using the divergence of a stress
-tensor, ensuring exact conservation of linear momentum. This method is particularly
-useful for simulations where momentum conservation is critical, though it may require
-numerical adjustments at higher resolutions.
+This model implements the conservative continuum-surface-stress (CSS) approach outlined by
+Morris [Morris2000](@cite). It computes the divergence of
+``\sigma\delta_s(I - \hat{n}\otimes\hat{n})`` with the same symmetric pair operator used by
+the fluid momentum equation. This avoids an explicit curvature estimate and conserves linear
+momentum exactly for constant smoothing length.
+
+The unnormalized color-gradient magnitude is retained as the surface delta ``\delta_s`` before
+the gradient is converted to a unit normal. The stress projection is evaluated directly during
+the fluid interaction, so no per-particle stress tensor or global reduction is required. A
+symmetric scalar reproducing correction is accumulated during the normal pass and applied to the
+stress divergence. It restores first-order scaling near truncated kernel support without another
+neighbor traversal or loss of pairwise momentum conservation.
+
+Validated wetted-wall energy can be enabled explicitly with
+`ColorfieldSurfaceNormal(contact_model=WettedAreaContactAngle(theta))`; omitting the contact model
+preserves the no-wetting default.
See [`surface_tension`](@ref) for more details.
# Keywords
-- `surface_tension_coefficient=1.0`: A parameter to adjust the strength of surface tension
- forces, allowing fine-tuning to replicate physical behavior.
+- `surface_tension_coefficient=1.0`: Physical surface tension coefficient in N/m.
"""
struct SurfaceTensionMomentumMorris{ELTYPE} <: AbstractSurfaceTension
surface_tension_coefficient::ELTYPE
@@ -101,13 +119,9 @@ end
function create_cache_surface_tension(::SurfaceTensionMomentumMorris, ELTYPE, NDIMS,
nparticles)
delta_s = Array{ELTYPE, 1}(undef, nparticles)
- # Allocate stress tensor for each particle: NDIMS x NDIMS x nparticles
- stress_tensor = Array{ELTYPE, 3}(undef, NDIMS, NDIMS, nparticles)
- return (; stress_tensor, delta_s)
-end
-
-@inline function stress_tensor(particle_system::AbstractFluidSystem, particle)
- return extract_smatrix(particle_system.cache.stress_tensor, particle_system, particle)
+ interface_activity = Array{ELTYPE, 1}(undef, nparticles)
+ divergence_correction = Array{ELTYPE, 1}(undef, nparticles)
+ return (; delta_s, interface_activity, divergence_correction)
end
# Note that `floating_point_number^integer_literal` is lowered to `Base.literal_pow`.
@@ -229,71 +243,44 @@ end
particle, neighbor, pos_diff, distance,
rho_a, rho_b, grad_kernel,
surface_tension_correction)
- (; surface_tension_coefficient) = surface_tension_a
-
- # No surface tension with oneself. See `src/general/smoothing_kernels.jl` for more details.
- distance^2 < eps(initial_smoothing_length(particle_system)^2) && return dv_particle
-
- n_a = surface_normal(particle_system, particle)
- curvature_a = curvature(particle_system, particle)
-
- dv_particle[] -= surface_tension_correction * surface_tension_coefficient / rho_a *
- curvature_a * n_a
-
+ # Morris CSF is a particle-local continuum force. It is added once outside the
+ # neighbor loop by `surface_tension_acceleration`.
return dv_particle
end
-function compute_stress_tensors!(system, surface_tension, v, u, v_ode, u_ode, semi, t)
- return system
+@inline function surface_tension_acceleration(surface_tension, particle_system, particle,
+ rho_a, vector_template)
+ return zero(vector_template)
end
-# Section 6 in Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics"
-function compute_stress_tensors!(system::AbstractFluidSystem,
- ::SurfaceTensionMomentumMorris,
- v, u, v_ode, u_ode, semi, t)
- (; cache) = system
- (; delta_s, stress_tensor) = cache
-
- # Reset surface stress_tensor
- set_zero!(stress_tensor)
-
- max_delta_s = maximum(delta_s)
- NDIMS = ndims(system)
-
- @trixi_timeit timer() "compute surface stress tensor" begin
- @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()
- for i in 1:NDIMS, j in 1:NDIMS
- delta_ij = (i == j) ? 1 : 0
- stress_tensor[i, j,
- particle] = delta_s_particle *
- (delta_ij - normal[i] * normal[j]) -
- delta_ij * max_delta_s
- end
- end
- end
- end
-
- return system
-end
+@inline function surface_tension_acceleration(surface_tension::SurfaceTensionMorris,
+ particle_system, particle, rho_a,
+ vector_template)
+ delta_s = @inbounds particle_system.cache.delta_s[particle]
+ iszero(delta_s) && return zero(vector_template)
-function compute_surface_delta_function!(system, surface_tension, semi)
- return system
+ normal = surface_tension_normal(particle_system, particle)
+ curvature_a = curvature(particle_system, particle)
+ return -surface_tension.surface_tension_coefficient / rho_a * curvature_a * delta_s *
+ normal
end
-# Eq. 6 in Morris 2000 "Simulating surface tension with smoothed particle hydrodynamics"
-function compute_surface_delta_function!(system, ::SurfaceTensionMomentumMorris, semi)
- (; cache) = system
- (; delta_s) = cache
+@inline function surface_stress_times_gradient(particle_system, particle, grad_kernel)
+ delta_s = @inbounds particle_system.cache.delta_s[particle]
+ iszero(delta_s) && return zero(grad_kernel)
- set_zero!(delta_s)
+ normal = surface_tension_normal(particle_system, particle)
+ return delta_s * (grad_kernel - normal * dot(normal, grad_kernel))
+end
- @threaded semi for particle in each_integrated_particle(system)
- delta_s[particle] = norm(surface_normal(system, particle))
- end
- return system
+@inline function symmetric_surface_divergence_correction(particle_system,
+ neighbor_system,
+ particle, neighbor)
+ correction_a = @inbounds particle_system.cache.divergence_correction[particle]
+ correction_b = @inbounds neighbor_system.cache.divergence_correction[neighbor]
+ denominator = correction_a + correction_b
+ denominator > eps(denominator) || return zero(denominator)
+ return 2 / denominator
end
@inline function surface_tension_force!(dv_particle,
@@ -309,13 +296,19 @@ end
# No surface tension with oneself. See `src/general/smoothing_kernels.jl` for more details.
distance^2 < eps(initial_smoothing_length(particle_system)^2) && return dv_particle
- S_a = stress_tensor(particle_system, particle)
- S_b = stress_tensor(neighbor_system, neighbor)
-
m_b = hydrodynamic_mass(neighbor_system, neighbor)
-
- dv_particle[] += surface_tension_correction * surface_tension_coefficient * m_b *
- (S_a + S_b) / (rho_a * rho_b) * grad_kernel
+ stress_gradient_a = surface_stress_times_gradient(particle_system, particle,
+ grad_kernel)
+ stress_gradient_b = surface_stress_times_gradient(neighbor_system, neighbor,
+ grad_kernel)
+ divergence_correction = symmetric_surface_divergence_correction(particle_system,
+ neighbor_system,
+ particle, neighbor)
+
+ # This uses the same symmetric stress-divergence operator as the pressure force. The
+ # Akinci free-surface correction is deliberately not applied to a continuum stress.
+ dv_particle[] += divergence_correction * surface_tension_coefficient * m_b /
+ (rho_a * rho_b) * (stress_gradient_a + stress_gradient_b)
return dv_particle
end
diff --git a/src/schemes/fluid/viscosity.jl b/src/schemes/fluid/viscosity.jl
index b39ccb0ef4..b81d637239 100644
--- a/src/schemes/fluid/viscosity.jl
+++ b/src/schemes/fluid/viscosity.jl
@@ -285,7 +285,7 @@ end
end
@doc raw"""
- ViscosityAdamiSGS(; nu, C_S=0.1, epsilon=0.01)
+ ViscosityAdamiSGS(; nu, C_S=0.1, epsilon=0.001)
Viscosity model that extends the standard [Adami formulation](@ref ViscosityAdami)
by incorporating a subgrid-scale (SGS) eddy viscosity via a Smagorinsky-type [Smagorinsky (1963)](@cite Smagorinsky1963) closure.
@@ -325,7 +325,7 @@ This model is appropriate for turbulent flows where unresolved scales contribute
# Keywords
- `nu`: Standard kinematic viscosity.
- `C_S`: Smagorinsky constant.
-- `epsilon=0.01`: Parameter to prevent singularities
+- `epsilon=0.001`: Parameter to prevent singularities
"""
struct ViscosityAdamiSGS{ELTYPE}
nu :: ELTYPE # Kinematic viscosity [e.g., 1e-6 m²/s]
@@ -447,7 +447,7 @@ This model is appropriate for turbulent flows where unresolved scales contribute
# Keywords
- `nu`: Standard kinematic viscosity.
- `C_S`: Smagorinsky constant.
-- `epsilon=0.01`: Parameter to prevent singularities
+- `epsilon=0.001`: Parameter to prevent singularities
"""
struct ViscosityMorrisSGS{ELTYPE}
nu :: ELTYPE # Kinematic viscosity [e.g., 1e-6 m²/s]
diff --git a/src/schemes/fluid/weakly_compressible_sph/rhs.jl b/src/schemes/fluid/weakly_compressible_sph/rhs.jl
index 836063538b..d6f38abae3 100644
--- a/src/schemes/fluid/weakly_compressible_sph/rhs.jl
+++ b/src/schemes/fluid/weakly_compressible_sph/rhs.jl
@@ -11,6 +11,7 @@ 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)
+ surface_normal_method_a = surface_normal_method(particle_system)
system_coords = current_coordinates(u_particle_system, particle_system)
neighbor_system_coords = current_coordinates(u_neighbor_system, neighbor_system)
@@ -40,6 +41,11 @@ function interact!(dv, v_particle_system, u_particle_system,
# inside the closure in the `foreach_neighbor` loop.
dv_particle = Ref(zero(v_a))
drho_particle = Ref(zero(rho_a))
+ if particle_system === neighbor_system
+ dv_particle[] += surface_tension_acceleration(surface_tension_a,
+ particle_system, particle,
+ rho_a, v_a)
+ end
# Loop over all neighbors within the kernel cutoff
@inbounds foreach_neighbor(system_coords, neighbor_system_coords,
@@ -103,10 +109,23 @@ function interact!(dv, v_particle_system, u_particle_system,
rho_a, rho_b, grad_kernel,
surface_tension_correction)
+ dv_particle[] += wetted_area_density_acceleration(surface_normal_method_a,
+ particle_system,
+ neighbor_system, particle,
+ neighbor, rho_a, rho_b, m_b,
+ grad_kernel)
+
@inbounds adhesion_force!(dv_particle, surface_tension_a, particle_system,
neighbor_system,
particle, neighbor, pos_diff, distance)
+ dv_particle[] += wetted_area_explicit_acceleration(surface_tension_a,
+ surface_normal_method_a,
+ particle_system,
+ neighbor_system, particle,
+ neighbor, m_a, rho_a,
+ grad_kernel)
+
# 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!(drho_particle, density_calculator,
diff --git a/src/schemes/fluid/weakly_compressible_sph/system.jl b/src/schemes/fluid/weakly_compressible_sph/system.jl
index eea0607d7d..13a24dd71f 100644
--- a/src/schemes/fluid/weakly_compressible_sph/system.jl
+++ b/src/schemes/fluid/weakly_compressible_sph/system.jl
@@ -35,7 +35,9 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method.
By default, the correct formulation is chosen based on the
density calculator and the correction method.
To use [Tensile Instability Control](@ref tic), pass
- [`tensile_instability_control`](@ref) here.
+ [`tensile_instability_control`](@ref), or use
+ [`InterfaceAwareTensileInstabilityControl`](@ref) with a
+ supported Morris CSF/CSS free surface.
- `shifting_technique`: [Shifting technique](@ref shifting) or [transport velocity
formulation](@ref transport_velocity_formulation) to use
with this system. Default is no shifting.
@@ -133,9 +135,15 @@ function WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
if surface_tension !== nothing && surface_normal_method === nothing
surface_normal_method = ColorfieldSurfaceNormal()
end
+ validate_corrected_csf(surface_normal_method, surface_tension)
+ validate_free_surface_shifting(shifting_technique, surface_normal_method,
+ surface_tension)
+ validate_interface_aware_tic(pressure_acceleration, density_calculator,
+ state_equation, surface_normal_method,
+ surface_tension, correction)
if surface_normal_method !== nothing && reference_particle_spacing < eps()
- throw(ArgumentError("`reference_particle_spacing` must be set to a positive value when using `ColorfieldSurfaceNormal` or a surface tension model"))
+ throw(ArgumentError("`reference_particle_spacing` must be set to a positive value when using a surface-normal method or a surface tension model"))
end
pressure_acceleration = choose_pressure_acceleration_formulation(pressure_acceleration,
@@ -229,6 +237,8 @@ end
@inline Base.eltype(::WeaklyCompressibleSPHSystem{<:Any, ELTYPE}) where {ELTYPE} = ELTYPE
+@inline wetted_area_supported_fluid(::WeaklyCompressibleSPHSystem) = true
+
@inline function v_nvariables(system::WeaklyCompressibleSPHSystem)
return v_nvariables(system, system.density_calculator)
end
@@ -321,7 +331,7 @@ end
end
function update_pressure!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_ode, semi, t)
- (; density_calculator, correction, surface_normal_method, surface_tension) = system
+ (; density_calculator, correction, surface_normal_method) = system
compute_pressure!(system, v, semi)
@@ -334,7 +344,6 @@ function update_pressure!(system::WeaklyCompressibleSPHSystem, v, u, v_ode, u_od
# These are only computed when using surface tension
compute_surface_normal!(system, surface_normal_method, v, u, v_ode, u_ode, semi, t)
- compute_surface_delta_function!(system, surface_tension, semi)
return system
end
@@ -344,7 +353,6 @@ 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_shifting!(system, shifting_technique(system), v, u, v_ode, u_ode, semi)
end
diff --git a/src/schemes/structure/rigid_body/system.jl b/src/schemes/structure/rigid_body/system.jl
index 033ea9f1da..23197fe5e5 100644
--- a/src/schemes/structure/rigid_body/system.jl
+++ b/src/schemes/structure/rigid_body/system.jl
@@ -281,8 +281,9 @@ function calc_normal!(system::AbstractFluidSystem,
surface_normal_method, neighbor_surface_normal_method)
haskey(neighbor_system.boundary_model.cache, :initial_colorfield) || return system
- return calc_boundary_normal!(system, neighbor_system, u_system, v, u_neighbor_system,
- semi, surface_normal_method)
+ return calc_boundary_normal!(system, neighbor_system, u_system, v,
+ v_neighbor_system, u_neighbor_system, semi,
+ surface_normal_method)
end
@inline function adhesion_force!(dv_particle,
@@ -379,6 +380,8 @@ end
function reset_interaction_caches!(system::RigidBodySystem)
set_zero!(system.force_per_particle)
+ boundary_cache = wetted_area_boundary_cache(system)
+ isnothing(boundary_cache) || set_zero!(boundary_cache.wetted_area_reaction)
system.cache.contact_count[] = 0
system.cache.max_contact_penetration[] = zero(eltype(system))
diff --git a/src/schemes/structure/structure.jl b/src/schemes/structure/structure.jl
index dd7e8d38ea..7941f9cab8 100644
--- a/src/schemes/structure/structure.jl
+++ b/src/schemes/structure/structure.jl
@@ -88,6 +88,10 @@ function interact_structure_fluid!(dv, v_particle_system, u_particle_system,
adhesion_force!(dv_particle, surface_tension, neighbor_system, particle_system,
neighbor, particle, pos_diff, distance)
+ accumulate_wetted_area_structure_reaction!(dv_particle, particle_system,
+ neighbor_system, particle, neighbor,
+ rho_b, m_b, grad_kernel)
+
accumulate_structure_fluid_pair!(dv, dv_particle[], particle_system, particle, m_b)
drho_particle = Ref(zero(rho_a))
@@ -101,6 +105,40 @@ function interact_structure_fluid!(dv, v_particle_system, u_particle_system,
return dv
end
+@inline function accumulate_wetted_area_structure_reaction!(dv_particle, particle_system,
+ fluid_system, particle,
+ fluid_particle, fluid_density,
+ fluid_mass, grad_kernel)
+ return dv_particle
+end
+
+@inline function accumulate_wetted_area_structure_reaction!(dv_particle,
+ particle_system::RigidBodySystem,
+ fluid_system::AbstractFluidSystem,
+ particle, fluid_particle,
+ fluid_density, fluid_mass,
+ grad_kernel)
+ surface_normal_method_ = surface_normal_method(fluid_system)
+ surface_normal_method_ isa ColorfieldSurfaceNormal{<:Any,
+ <:WettedAreaContactAngle} ||
+ return dv_particle
+ boundary_cache = wetted_area_boundary_cache(particle_system)
+ isnothing(boundary_cache) && return dv_particle
+ weight = @inbounds boundary_cache.wetted_area_weight[particle]
+ iszero(weight) && return dv_particle
+ coefficient = wetted_area_coefficient(surface_tension_model(fluid_system),
+ surface_normal_method_.contact_model)
+ iszero(coefficient) && return dv_particle
+
+ reaction_acceleration = coefficient / fluid_density * weight * grad_kernel
+ dv_particle[] += reaction_acceleration
+ reaction = fluid_mass * reaction_acceleration
+ for dim in eachindex(reaction)
+ @inbounds boundary_cache.wetted_area_reaction[dim, particle] += reaction[dim]
+ end
+ return dv_particle
+end
+
@inline function continuity_equation!(drho_particle,
particle_system::AbstractStructureSystem,
neighbor_system::AbstractFluidSystem,
diff --git a/src/setups/complex_shape.jl b/src/setups/complex_shape.jl
index 6a78b412e2..d0da406f7d 100644
--- a/src/setups/complex_shape.jl
+++ b/src/setups/complex_shape.jl
@@ -52,6 +52,8 @@ function ComplexShape(geometry; particle_spacing, density,
throw(ArgumentError("`WindingNumberHormann` only supports 2D geometries"))
end
+ require_closed_geometry(geometry, "ComplexShape")
+
if grid_offset < 0.0
throw(ArgumentError("only a positive `grid_offset` is supported"))
end
@@ -91,8 +93,10 @@ of the geometry.
- `boundary_density`: Density of each boundary particle.
- `place_on_shell`: When `place_on_shell=true`, boundary particles will be placed
one particle spacing from the surface of the geometry.
- Otherwise when `place_on_shell=true` (simulating fluid particles),
+ Otherwise when `place_on_shell=false` (simulating fluid particles),
boundary particles will be placed half particle spacing away from the surface.
+ Thus, `boundary_thickness` must be at least one particle spacing
+ for `place_on_shell=true` and half a particle spacing otherwise.
# Examples
@@ -111,7 +115,7 @@ boundary_sampled = sample_boundary(signed_distance_field; boundary_density=1.0,
│ InitialCondition │
│ ════════════════ │
│ #dimensions: ……………………………………………… 2 │
-│ #particles: ………………………………………………… 889 │
+│ #particles: ………………………………………………… 677 │
│ particle spacing: ………………………………… 0.03 │
│ eltype: …………………………………………………………… Float64 │
│ coordinate eltype: ……………………………… Float64 │
@@ -133,10 +137,22 @@ function sample_boundary(signed_distance_field;
end
# Only keep the required part of the signed distance field
- distance_to_boundary = zero(particle_spacing)
- keep_indices = (distance_to_boundary .< distances .<= max_signed_distance)
+ distance_to_boundary = place_on_shell ? particle_spacing : particle_spacing / 2
+ if boundary_thickness < distance_to_boundary
+ throw(ArgumentError("`boundary_thickness` must be at least " *
+ "`particle_spacing` for `place_on_shell=true` and " *
+ "half `particle_spacing` for `place_on_shell=false`."))
+ end
+
+ keep_indices = (distance_to_boundary .<= distances .<= boundary_thickness)
+ boundary_positions = positions[keep_indices]
+
+ if isempty(boundary_positions)
+ throw(ArgumentError("No boundary particles were sampled. Increase " *
+ "`boundary_thickness` or generate a denser `SignedDistanceField`."))
+ end
- boundary_coordinates = stack(positions[keep_indices])
+ boundary_coordinates = stack(boundary_positions)
return InitialCondition(; coordinates=boundary_coordinates, density=boundary_density,
particle_spacing)
end
diff --git a/src/setups/extrude_geometry.jl b/src/setups/extrude_geometry.jl
index 460583c38e..43455678bb 100644
--- a/src/setups/extrude_geometry.jl
+++ b/src/setups/extrude_geometry.jl
@@ -87,6 +87,14 @@ shape = extrude_geometry(shape; direction, particle_spacing=0.1, n_extrude=4, de
function extrude_geometry(geometry; particle_spacing=-1, direction, n_extrude::Integer,
velocity=zeros(length(direction)), place_on_shell=false,
mass=nothing, density=nothing, pressure=0.0)
+ if all(iszero, direction)
+ throw(ArgumentError("`direction` needs to be non-zero"))
+ end
+
+ if n_extrude < 1
+ throw(ArgumentError("`n_extrude` needs to be positive"))
+ end
+
direction_ = normalize(direction)
NDIMS = length(direction_)
@@ -105,6 +113,11 @@ function extrude_geometry(geometry; particle_spacing=-1, direction, n_extrude::I
face_coords = sample_plane(geometry, particle_spacing; place_on_shell)
+ if size(face_coords, 1) != NDIMS
+ throw(ArgumentError("`direction` must be of length $(size(face_coords, 1)) " *
+ "for the sampled geometry"))
+ end
+
coords = (face_coords .+ i * particle_spacing * direction_ for i in 0:(n_extrude - 1))
# In this context, `stack` is faster than `hcat(coords...)`
@@ -235,6 +248,10 @@ end
function shift_plane_corners(plane_points::NTuple{2}, direction, particle_spacing,
place_on_shell)
+ if length(direction) != 2
+ throw(ArgumentError("`direction` must be 2D when extruding 2D points"))
+ end
+
# With `place_on_shell`, particles need to be AT the min coordinates and not half a particle
# spacing away from it.
(place_on_shell) && (return plane_points)
@@ -254,6 +271,10 @@ end
function shift_plane_corners(plane_points::NTuple{3}, direction, particle_spacing,
place_on_shell)
+ if length(direction) != 3
+ throw(ArgumentError("`direction` must be 3D when extruding 3D points"))
+ end
+
# With `place_on_shell`, particles need to be AT the min coordinates and not half a particle
# spacing away from it.
(place_on_shell) && (return plane_points)
diff --git a/src/setups/rectangular_shape.jl b/src/setups/rectangular_shape.jl
index c9043a35e6..a1daff091d 100644
--- a/src/setups/rectangular_shape.jl
+++ b/src/setups/rectangular_shape.jl
@@ -92,7 +92,7 @@ function RectangularShape(particle_spacing, n_particles_per_dimension, min_coord
throw(ArgumentError("`min_coordinates` must be of length $NDIMS for a $(NDIMS)D problem"))
end
- if density !== nothing && any(density .< eps())
+ if density !== nothing && !(density isa Function) && any(density .< eps())
throw(ArgumentError("`density` needs to be positive and larger than $(eps())"))
end
@@ -105,34 +105,50 @@ function RectangularShape(particle_spacing, n_particles_per_dimension, min_coord
place_on_shell, loop_order)
if !isnothing(coordinates_perturbation)
- seed!(1)
amplitude = coordinates_perturbation * particle_spacing
- coordinates .+= rand((-amplitude):(particle_spacing * 1e-3):(amplitude),
+ coordinates .+= rand(MersenneTwister(1),
+ (-amplitude):(particle_spacing * 1e-3):(amplitude),
NDIMS, n_particles)
end
# Allow zero acceleration with state equation, but interpret `nothing` acceleration
# with state equation as a likely mistake.
if acceleration isa AbstractVector || acceleration isa Tuple
+ if length(acceleration) != NDIMS
+ throw(ArgumentError("`acceleration` must be of length $NDIMS for a $(NDIMS)D problem"))
+ end
+
if pressure != 0.0
throw(ArgumentError("`pressure` cannot be used together with `acceleration` " *
"and `state_equation` (hydrostatic pressure gradient)"))
end
if state_equation === nothing
- density_fun = pressure -> density
+ if density === nothing
+ throw(ArgumentError("`density` must be specified when using " *
+ "`acceleration` without `state_equation`"))
+ end
else
if density !== nothing
throw(ArgumentError("`density` cannot be used together with `acceleration` " *
"and `state_equation` (hydrostatic pressure gradient)"))
end
- density_fun = pressure -> inverse_state_equation(state_equation, pressure)
end
# Initialize hydrostatic pressure
pressure = Vector{ELTYPE}(undef, n_particles)
- initialize_pressure!(pressure, particle_spacing, acceleration,
- density_fun, n_particles_per_dimension, loop_order)
+ if state_equation === nothing && density isa Function
+ initialize_pressure_with_coordinate_density!(pressure, particle_spacing,
+ acceleration, density,
+ coordinates,
+ n_particles_per_dimension,
+ loop_order)
+ else
+ density_fun = state_equation === nothing ? (pressure -> density) :
+ (pressure -> inverse_state_equation(state_equation, pressure))
+ initialize_pressure!(pressure, particle_spacing, acceleration,
+ density_fun, n_particles_per_dimension, loop_order)
+ end
if state_equation !== nothing
# Weakly compressible case: get density from inverse state equation
@@ -223,15 +239,23 @@ function rectangular_shape_coords(particle_spacing, n_particles_per_dimension,
return coordinates
end
-function initialize_pressure!(pressure, particle_spacing, acceleration, density_fun,
- n_particles_per_dimension, loop_order)
+function acceleration_dimension(acceleration)
if count(a -> abs(a) > eps(), acceleration) > 1
throw(ArgumentError("hydrostatic pressure calculation is not supported with " *
"diagonal acceleration"))
end
+ return findfirst(a -> abs(a) > eps(), acceleration)
+end
+
+function initialize_pressure!(pressure, particle_spacing, acceleration, density_fun,
+ n_particles_per_dimension, loop_order)
# Dimension in which the acceleration is acting
- accel_dim = findfirst(a -> abs(a) > eps(), acceleration)
+ accel_dim = acceleration_dimension(acceleration)
+ if accel_dim === nothing
+ fill!(pressure, zero(eltype(pressure)))
+ return pressure
+ end
# Compute 1D pressure gradient with explicit Euler method
factor = particle_spacing * abs(acceleration[accel_dim])
@@ -265,3 +289,72 @@ function initialize_pressure!(pressure, particle_spacing, acceleration, density_
pressure[particle] = pressure_1d[index_in_accel_dim]
end
end
+
+function particle_indices_by_cartesian_index(n_particles_per_dimension, loop_order)
+ NDIMS = length(n_particles_per_dimension)
+ particle_indices = Array{Int}(undef, n_particles_per_dimension)
+ cartesian_indices = CartesianIndices(n_particles_per_dimension)
+ permutation = loop_permutation(loop_order, Val(NDIMS))
+ permuted_indices = permutedims(cartesian_indices, permutation)
+
+ for particle in eachindex(permuted_indices)
+ particle_indices[permuted_indices[particle]] = particle
+ end
+
+ return particle_indices
+end
+
+# This is needed for `density = coords -> ...`. The pressure-dependent path above can reuse
+# one 1D pressure profile for every column. Coordinate-dependent density may vary between
+# columns, so each gravity-aligned column needs its own explicit Euler integration.
+function initialize_pressure_with_coordinate_density!(pressure, particle_spacing,
+ acceleration, density_fun,
+ coordinates,
+ n_particles_per_dimension,
+ loop_order)
+ # Dimension in which the acceleration is acting
+ accel_dim = acceleration_dimension(acceleration)
+ if accel_dim === nothing
+ fill!(pressure, zero(eltype(pressure)))
+ return pressure
+ end
+
+ NDIMS = length(n_particles_per_dimension)
+ factor = particle_spacing * abs(acceleration[accel_dim])
+ particle_indices = particle_indices_by_cartesian_index(n_particles_per_dimension,
+ loop_order)
+
+ accel_indices = if sign(acceleration[accel_dim]) < 0
+ n_particles_per_dimension[accel_dim]:-1:1
+ else
+ 1:n_particles_per_dimension[accel_dim]
+ end
+ surface_index = first(accel_indices)
+ column_starts = ntuple(dim -> dim == accel_dim ? (surface_index:surface_index) :
+ axes(particle_indices, dim), Val(NDIMS))
+
+ for column_start in CartesianIndices(column_starts)
+ pressure_prev = zero(eltype(pressure))
+ density_prev = zero(eltype(pressure))
+ for (i, accel_index) in enumerate(accel_indices)
+ index = ntuple(dim -> dim == accel_dim ? accel_index : column_start[dim],
+ Val(NDIMS))
+ particle = particle_indices[index...]
+ coords = SVector{NDIMS, eltype(coordinates)}(ntuple(dim -> coordinates[dim,
+ particle],
+ Val(NDIMS)))
+ density = density_fun(coords)
+
+ if i == 1
+ pressure[particle] = 0.5factor * density
+ else
+ pressure[particle] = pressure_prev + factor * density_prev
+ end
+
+ pressure_prev = pressure[particle]
+ density_prev = density
+ end
+ end
+
+ return pressure
+end
diff --git a/src/setups/rectangular_tank.jl b/src/setups/rectangular_tank.jl
index 40abd3a9ca..155dece92f 100644
--- a/src/setups/rectangular_tank.jl
+++ b/src/setups/rectangular_tank.jl
@@ -117,10 +117,27 @@ struct RectangularTank{NDIMS, NDIMSt2, ELTYPE <: Real, F, B}
throw(ArgumentError("`fluid_density` needs to be positive and larger than $(eps())."))
end
+ if any(<(0), fluid_size_)
+ throw(ArgumentError("`fluid_size` dimensions need to be non-negative"))
+ end
+
+ if !(n_layers isa Integer) || n_layers < 1
+ throw(ArgumentError("`n_layers` needs to be a positive integer"))
+ end
+ n_layers = Int(n_layers)
+
+ if spacing_ratio < eps()
+ throw(ArgumentError("`spacing_ratio` needs to be positive and larger than $(eps())."))
+ end
+
if length(tank_size) != NDIMS
throw(ArgumentError("`tank_size` must be of length $NDIMS for a $(NDIMS)D problem"))
end
+ if any(<(0), tank_size_)
+ throw(ArgumentError("`tank_size` dimensions need to be non-negative"))
+ end
+
# Fluid particle data
n_particles_per_dim,
fluid_size_ = fluid_particles_per_dimension(fluid_size_, particle_spacing)
@@ -168,7 +185,7 @@ struct RectangularTank{NDIMS, NDIMSt2, ELTYPE <: Real, F, B}
# Move the tank corner in the negative coordinate directions to the desired position
boundary.coordinates .+= min_coordinates
- if norm(fluid_size) > eps()
+ if all(>(0), n_particles_per_dim)
if state_equation !== nothing
# Use hydrostatic pressure gradient and calculate density from inverse state
# equation, so don't pass fluid density.
@@ -480,14 +497,20 @@ function check_tank_overlap(fluid_size::NTuple{2}, tank_size, particle_spacing,
fluid_size_x, fluid_size_y = fluid_size
if tank_size[1] < fluid_size[1] - 1e-5 * particle_spacing
- n_particles_x -= 1
+ n_particles_x = max(0,
+ floor(Int,
+ (tank_size[1] + 1e-5 * particle_spacing) /
+ particle_spacing))
fluid_size_x = n_particles_x * particle_spacing
@info "The fluid was overlapping.\n New fluid length in x-direction is set to $fluid_size_x."
end
if tank_size[2] < fluid_size[2] - 1e-5 * particle_spacing
- n_particles_y -= 1
+ n_particles_y = max(0,
+ floor(Int,
+ (tank_size[2] + 1e-5 * particle_spacing) /
+ particle_spacing))
fluid_size_y = n_particles_y * particle_spacing
@info "The fluid was overlapping.\n New fluid length in y-direction is set to $fluid_size_y."
@@ -502,21 +525,30 @@ function check_tank_overlap(fluid_size::NTuple{3}, tank_size, particle_spacing,
fluid_size_x, fluid_size_y, fluid_size_z = fluid_size
if tank_size[1] < fluid_size[1] - 1e-5 * particle_spacing
- n_particles_x -= 1
+ n_particles_x = max(0,
+ floor(Int,
+ (tank_size[1] + 1e-5 * particle_spacing) /
+ particle_spacing))
fluid_size_x = n_particles_x * particle_spacing
@info "The fluid was overlapping.\n New fluid length in x-direction is set to $fluid_size_x."
end
if tank_size[2] < fluid_size[2] - 1e-5 * particle_spacing
- n_particles_y -= 1
+ n_particles_y = max(0,
+ floor(Int,
+ (tank_size[2] + 1e-5 * particle_spacing) /
+ particle_spacing))
fluid_size_y = n_particles_y * particle_spacing
@info "The fluid was overlapping.\n New fluid length in y-direction is set to $fluid_size_y."
end
if tank_size[3] < fluid_size[3] - 1e-5 * particle_spacing
- n_particles_z -= 1
+ n_particles_z = max(0,
+ floor(Int,
+ (tank_size[3] + 1e-5 * particle_spacing) /
+ particle_spacing))
fluid_size_z = n_particles_z * particle_spacing
@info "The fluid was overlapping.\n New fluid length in z-direction is set to $fluid_size_z."
diff --git a/src/setups/sphere_shape.jl b/src/setups/sphere_shape.jl
index fea261e127..b547897888 100644
--- a/src/setups/sphere_shape.jl
+++ b/src/setups/sphere_shape.jl
@@ -36,7 +36,8 @@ coordinate directions as `cutout_min` and `cutout_max`.
- `cutout_min`: Corner in negative coordinate directions of a cuboid that is to be
cut out of the sphere.
- `cutout_max`: Corner in positive coordinate directions of a cuboid that is to be
- cut out of the sphere.
+ cut out of the sphere. If the cutout has zero volume, no particles
+ are removed.
- `place_on_shell = false`: 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
@@ -114,9 +115,22 @@ function SphereShape(particle_spacing, radius, center_position, density;
cutout_min_ = collect(cutout_min)
cutout_max_ = collect(cutout_max)
+ # A zero-volume cutout means no cutout. This keeps the 2D zero default valid for
+ # 3D shapes while still validating dimensionality once a real cutout is requested.
+ has_cutout = length(cutout_min_) != length(cutout_max_) ||
+ norm(cutout_max_ - cutout_min_) > eps()
+
+ if has_cutout && (length(cutout_min_) != NDIMS || length(cutout_max_) != NDIMS)
+ throw(ArgumentError("`cutout_min` and `cutout_max` must be of length $NDIMS " *
+ "for a $(NDIMS)D problem"))
+ end
+
+ if has_cutout && any(cutout_min_ .> cutout_max_)
+ throw(ArgumentError("`cutout_min` must be smaller than or equal to `cutout_max`"))
+ end
+
# Remove particles in cutout
# TODO This should consider the particle radius as well
- has_cutout = norm(cutout_max_ - cutout_min_) > eps()
function in_cutout(particle)
return has_cutout &&
all(cutout_min_ .<= view(coordinates, :, particle) .<= cutout_max_)
diff --git a/test/general/custom_quantities.jl b/test/general/custom_quantities.jl
index 9181269a6b..b5d9f63830 100644
--- a/test/general/custom_quantities.jl
+++ b/test/general/custom_quantities.jl
@@ -121,4 +121,104 @@
@test isnan(avg_density(boundary_system, dv_ode, du_ode, v_ode, u_ode, semi, t))
end
end
+
+ @testset "Structure kinetic energy" begin
+ struct EnergyStructureMock{IC, M} <: TrixiParticles.AbstractStructureSystem{2}
+ initial_condition::IC
+ mass::M
+ end
+
+ Base.eltype(::EnergyStructureMock) = Float64
+ TrixiParticles.compact_support(::EnergyStructureMock, neighbor) = 1.0
+ function TrixiParticles.write_u0!(u0, system::EnergyStructureMock)
+ u0 .= system.initial_condition.coordinates
+ return u0
+ end
+ function TrixiParticles.write_v0!(v0, system::EnergyStructureMock)
+ v0 .= system.initial_condition.velocity
+ return v0
+ end
+
+ coordinates = [0.0 1.0 2.0
+ 0.0 0.0 0.0]
+ velocity = [1.0 2.0 3.0
+ 4.0 5.0 6.0]
+ mass = [1.0, 2.0, 3.0]
+ ic = InitialCondition(; coordinates, velocity, mass, density=ones(3))
+ system = EnergyStructureMock(ic, mass)
+ semi = Semidiscretization(system; neighborhood_search=nothing)
+ ode = semidiscretize(semi, (0.0, 1.0))
+ v_ode, u_ode = ode.u0.x
+ dv_ode, du_ode = similar(v_ode), similar(u_ode)
+
+ expected = sum(axes(velocity, 2)) do particle
+ return mass[particle] * dot(velocity[:, particle], velocity[:, particle]) / 2
+ end
+
+ @test kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == expected
+ end
+
+ @testset "Active particle reductions" begin
+ struct ActiveFluidQuantityMock{IC, M, P, B} <: TrixiParticles.AbstractFluidSystem{2}
+ initial_condition::IC
+ mass::M
+ pressure::P
+ buffer::B
+ surface_tension::Nothing
+ surface_normal_method::Nothing
+ end
+
+ Base.eltype(::ActiveFluidQuantityMock) = Float64
+ TrixiParticles.v_nvariables(::ActiveFluidQuantityMock) = 3
+ TrixiParticles.buffer(system::ActiveFluidQuantityMock) = system.buffer
+ TrixiParticles.compact_support(::ActiveFluidQuantityMock, neighbor) = 1.0
+ function TrixiParticles.current_velocity(v, ::ActiveFluidQuantityMock)
+ return view(v, 1:2, :)
+ end
+ function TrixiParticles.current_density(v, ::ContinuityDensity,
+ ::ActiveFluidQuantityMock)
+ return view(v, 3, :)
+ end
+ function TrixiParticles.current_density(v, system::ActiveFluidQuantityMock)
+ return TrixiParticles.current_density(v, ContinuityDensity(), system)
+ end
+ function TrixiParticles.current_pressure(v, system::ActiveFluidQuantityMock)
+ return system.pressure
+ end
+ function TrixiParticles.write_u0!(u0, system::ActiveFluidQuantityMock)
+ u0 .= system.initial_condition.coordinates
+ return u0
+ end
+ function TrixiParticles.write_v0!(v0, system::ActiveFluidQuantityMock)
+ v0[1:2, :] .= system.initial_condition.velocity
+ v0[3, :] .= system.initial_condition.density
+ return v0
+ end
+
+ coordinates = [0.0 1.0 2.0
+ 0.0 0.0 0.0]
+ velocity = [1.0 10.0 3.0
+ 2.0 20.0 4.0]
+ mass = [1.0, 2.0, 4.0]
+ density = [10.0, 50.0, 30.0]
+ pressure = [100.0, 500.0, 300.0]
+ ic = InitialCondition(; coordinates, velocity, mass, density, pressure)
+ buffer = TrixiParticles.SystemBuffer(nparticles(ic), 0)
+ buffer.active_particle[2] = false
+ TrixiParticles.update_system_buffer!(buffer)
+ system = ActiveFluidQuantityMock(ic, mass, pressure, buffer, nothing, nothing)
+ semi = Semidiscretization(system; neighborhood_search=nothing)
+ ode = semidiscretize(semi, (0.0, 1.0))
+ v_ode, u_ode = ode.u0.x
+ dv_ode, du_ode = similar(v_ode), similar(u_ode)
+
+ @test total_mass(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 5.0
+ @test max_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 300.0
+ @test min_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 100.0
+ @test avg_pressure(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 200.0
+ @test max_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 30.0
+ @test min_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 10.0
+ @test avg_density(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 20.0
+ @test kinetic_energy(system, dv_ode, du_ode, v_ode, u_ode, semi, t) == 52.5
+ end
end
diff --git a/test/general/semidiscretization.jl b/test/general/semidiscretization.jl
index 4b466ad66a..d8cd080a1a 100644
--- a/test/general/semidiscretization.jl
+++ b/test/general/semidiscretization.jl
@@ -141,6 +141,29 @@
@test_throws ArgumentError(error_str) Semidiscretization(fluid_system,
boundary_system)
end
+
+ @testset verbose=true "Fluid Surface Tension Consistency" begin
+ struct FluidSurfaceMock <: TrixiParticles.AbstractFluidSystem{2}
+ surface_tension::Any
+ surface_normal_method::Any
+ end
+
+ system_with_surface = FluidSurfaceMock(SurfaceTensionMorris(),
+ ColorfieldSurfaceNormal())
+ system_with_normal = FluidSurfaceMock(nothing, ColorfieldSurfaceNormal())
+ system_without_surface = FluidSurfaceMock(nothing, nothing)
+
+ error_str = "either none or all fluid systems in a simulation need " *
+ "to use a surface tension model or a surface normal method."
+ @test_throws ArgumentError(error_str) TrixiParticles.check_configuration(system_with_surface,
+ (system_with_surface,
+ system_without_surface),
+ nothing)
+ @test_nowarn TrixiParticles.check_configuration(system_with_surface,
+ (system_with_surface,
+ system_with_normal),
+ nothing)
+ end
end
@testset verbose=true "`show`" begin
diff --git a/test/preprocessing/geometries/geometries.jl b/test/preprocessing/geometries/geometries.jl
index 4f428d266c..e01099dbe0 100644
--- a/test/preprocessing/geometries/geometries.jl
+++ b/test/preprocessing/geometries/geometries.jl
@@ -54,12 +54,98 @@
end
end
+ @testset verbose=true "Open Polygon Closure" begin
+ open_square = [1.0 2.0 2.0 1.0;
+ 1.0 1.0 2.0 2.0]
+
+ geometry = TrixiParticles.Polygon(open_square)
+
+ @test TrixiParticles.nfaces(geometry) == 4
+ @test first(geometry.vertices) == last(geometry.vertices)
+ @test TrixiParticles.volume(geometry) ≈ 1.0
+
+ mktempdir() do dir
+ filename = joinpath(dir, "open_square.asc")
+ open(filename, "w") do io
+ println(io, "# ASCII")
+ for vertex in eachcol(open_square)
+ println(io, vertex[1], " ", vertex[2])
+ end
+ end
+
+ geometry_from_file = load_geometry(filename)
+
+ @test TrixiParticles.nfaces(geometry_from_file) == 4
+ @test first(geometry_from_file.vertices) == last(geometry_from_file.vertices)
+ @test TrixiParticles.volume(geometry_from_file) ≈ 1.0
+ end
+ end
+
+ @testset verbose=true "Closed Geometry Detection" begin
+ open_square = [1.0 2.0 2.0 1.0;
+ 1.0 1.0 2.0 2.0]
+
+ closed_polygon = TrixiParticles.Polygon(open_square)
+ open_polygon = TrixiParticles.Polygon(open_square; close_curve=false)
+ partial_polygon = delete_faces(closed_polygon, 2)
+ rebuilt_closed_polygon = delete_faces(closed_polygon, Int[])
+
+ @test TrixiParticles.is_closed_geometry(closed_polygon)
+ @test TrixiParticles.is_closed_geometry(rebuilt_closed_polygon)
+ @test !TrixiParticles.is_closed_geometry(open_polygon)
+ @test !TrixiParticles.is_closed_geometry(partial_polygon)
+
+ shape = RectangularShape(0.5, (2, 2), (1.0, 1.0), density=1.0)
+ @test_throws ArgumentError intersect(shape, open_polygon)
+ @test_throws ArgumentError setdiff(shape, open_polygon)
+
+ file = pkgdir(TrixiParticles, "test", "preprocessing", "data")
+ planar_geometry = load_geometry(joinpath(file, "inflow_geometry.stl"))
+ closed_mesh = extrude_geometry(planar_geometry, 0.8)
+ open_mesh = extrude_geometry(planar_geometry, 0.8; omit_top_face=true)
+
+ @test !TrixiParticles.is_closed_geometry(planar_geometry)
+ @test TrixiParticles.is_closed_geometry(closed_mesh)
+ @test !TrixiParticles.is_closed_geometry(open_mesh)
+ end
+
+ @testset verbose=true "`delete_faces` Rebuilds Derived Data" begin
+ triangle = [0.0 1.0 0.5 0.0;
+ 0.0 0.0 0.7 0.0]
+
+ edge_only = TrixiParticles.delete_faces(TrixiParticles.Polygon(triangle), [1, 2])
+
+ @test TrixiParticles.nfaces(edge_only) == 1
+ @test length(edge_only.vertices) == 2
+ @test length(edge_only.vertex_normals) == 1
+ @test edge_only.min_corner == min.(edge_only.edge_vertices[1]...)
+ @test edge_only.max_corner == max.(edge_only.edge_vertices[1]...)
+ @test edge_only.vertex_normals[1] == (edge_only.edge_normals[1],
+ edge_only.edge_normals[1])
+
+ A = SVector(0.0, 0.0, 0.0)
+ B = SVector(1.0, 0.0, 0.0)
+ C = SVector(0.0, 1.0, 0.0)
+ D = SVector(1.0, 1.0, 0.0)
+ face_vertices = [(A, B, C), (B, D, C)]
+ face_normals = [SVector(0.0, 0.0, 1.0), SVector(0.0, 0.0, 1.0)]
+ mesh = TrixiParticles.TriangleMesh(face_vertices, face_normals, [A, B, C, D])
+
+ mesh = TrixiParticles.delete_faces(mesh, 1)
+
+ @test TrixiParticles.nfaces(mesh) == 1
+ @test length(mesh.vertices) == 3
+ @test length(mesh.edge_normals) == 3
+ @test mesh.face_vertices == [face_vertices[2]]
+ end
+
@testset verbose=true "Real World Data" begin
data_dir = pkgdir(TrixiParticles, "examples", "preprocessing", "data")
validation_dir = pkgdir(TrixiParticles, "test", "preprocessing", "data")
@testset verbose=true "2D" begin
files = ["hexagon", "circle", "inverted_open_curve"]
+ close_curves = [true, true, false]
n_edges = [6, 63, 240]
volumes = [2.5980750000000006, 3.1363805763454, 2.6153740535469048]
@@ -74,7 +160,8 @@
points = vcat((data.var"Points:0")', (data.var"Points:1")')
- geometry = load_geometry(joinpath(data_dir, files[i] * ".asc"))
+ geometry = load_geometry(joinpath(data_dir, files[i] * ".asc");
+ close_curve=close_curves[i])
@test TrixiParticles.nfaces(geometry) == n_edges[i]
@@ -157,6 +244,19 @@
end
end
+ @testset verbose=true "Degenerate Triangle Normals" begin
+ vertex = SVector(0.0, 0.0, 0.0)
+ normal = SVector(0.0, 0.0, 0.0)
+
+ geometry = TrixiParticles.TriangleMesh([(vertex, vertex, vertex)],
+ [normal], [vertex, vertex, vertex])
+
+ @test all(iszero, geometry.vertex_normals)
+ @test all(iszero, geometry.edge_normals)
+ @test all(all(isfinite, normal) for normal in geometry.vertex_normals)
+ @test all(all(isfinite, normal) for normal in geometry.edge_normals)
+ end
+
@testset verbose=true "Union" begin
# Build a single geometry by uniting multiple STL patches (cuboid.stl contains separate solids).
# The union should produce a closed volume.
@@ -202,47 +302,22 @@
omit_bottom_face=true)
winding_number_factor = 0.2
- @testset verbose=true "Omit Top Face" begin
- expected_min_corner = [-0.036399998962879196; 0.24624998748302457; -0.5233639197487431;;]
- expected_max_corner = [0.38360000103712083; 1.1462499874830245; -0.07336391974874301;;]
-
- ic_1 = ComplexShape(geometry_extruded_1; particle_spacing=0.03, density=1.0,
- point_in_geometry_algorithm=WindingNumberJacobson(;
- geometry=geometry_extruded_1,
- winding_number_factor))
-
- @test nparticles(ic_1) == 2994
- @test isapprox(maximum(ic_1.coordinates, dims=2), expected_max_corner)
- @test isapprox(minimum(ic_1.coordinates, dims=2), expected_min_corner)
- end
- @testset verbose=true "Omit Bottom Face" begin
- expected_min_corner = [-0.0663999989628792; 0.1562499874830246; -0.49336391974874305;;]
- expected_max_corner = [0.38360000103712083; 1.0562499874830245; -0.07336391974874301;;]
-
- ic_2 = ComplexShape(geometry_extruded_2; particle_spacing=0.03, density=1.0,
- point_in_geometry_algorithm=WindingNumberJacobson(;
- geometry=geometry_extruded_2,
- winding_number_factor))
-
- @test nparticles(ic_2) == 2988
- @test isapprox(maximum(ic_2.coordinates, dims=2), expected_max_corner)
- @test isapprox(minimum(ic_2.coordinates, dims=2), expected_min_corner)
- end
-
- @testset verbose=true "Omit Both" begin
- expected_min_corner = [-0.0663999989628792; 0.1562499874830246; -0.5233639197487431;;]
- expected_max_corner = [0.38360000103712083; 1.1462499874830245; -0.07336391974874301;;]
-
- ic_3 = ComplexShape(geometry_extruded_3; particle_spacing=0.03, density=1.0,
- point_in_geometry_algorithm=WindingNumberJacobson(;
- geometry=geometry_extruded_3,
- winding_number_factor))
-
- @test nparticles(ic_3) == 3258
- @test isapprox(maximum(ic_3.coordinates, dims=2), expected_max_corner)
- @test isapprox(minimum(ic_3.coordinates, dims=2), expected_min_corner)
- end
+ @test_throws ArgumentError ComplexShape(geometry_extruded_1;
+ particle_spacing=0.03, density=1.0,
+ point_in_geometry_algorithm=WindingNumberJacobson(;
+ geometry=geometry_extruded_1,
+ winding_number_factor))
+ @test_throws ArgumentError ComplexShape(geometry_extruded_2;
+ particle_spacing=0.03, density=1.0,
+ point_in_geometry_algorithm=WindingNumberJacobson(;
+ geometry=geometry_extruded_2,
+ winding_number_factor))
+ @test_throws ArgumentError ComplexShape(geometry_extruded_3;
+ particle_spacing=0.03, density=1.0,
+ point_in_geometry_algorithm=WindingNumberJacobson(;
+ geometry=geometry_extruded_3,
+ winding_number_factor))
end
end
diff --git a/test/preprocessing/packing/nhs_faces.jl b/test/preprocessing/packing/nhs_faces.jl
index 292522a91b..a9b038f2ed 100644
--- a/test/preprocessing/packing/nhs_faces.jl
+++ b/test/preprocessing/packing/nhs_faces.jl
@@ -4,7 +4,7 @@
0.0 0.0 0.7 0.0]
# Only use the third edge of the triangle, i.e. the edge from [0.1, 0.0] to [0.0, 0.0]
- edge_aligned = deleteat!(TrixiParticles.Polygon(triangle), [1, 2])
+ edge_aligned = TrixiParticles.delete_faces(TrixiParticles.Polygon(triangle), [1, 2])
edge_id = 1 # Only one edge in `Polygon`
cell_sizes = [1.0 + sqrt(eps()), 0.1]
@@ -27,7 +27,8 @@
end
# Only use the first edge of the triangle, i.e. the edge from [0.0, 0.0] to [0.5, 0.7]
- edge_arbitrary = deleteat!(TrixiParticles.Polygon(triangle), [2, 3])
+ edge_arbitrary = TrixiParticles.delete_faces(TrixiParticles.Polygon(triangle),
+ [2, 3])
edge_id = 1 # Only one edge in `Polygon`
expected_ncells_bbox = [(1, 1), (6, 7)]
diff --git a/test/preprocessing/packing/signed_distance.jl b/test/preprocessing/packing/signed_distance.jl
index cb02753b26..d49ae57fcb 100644
--- a/test/preprocessing/packing/signed_distance.jl
+++ b/test/preprocessing/packing/signed_distance.jl
@@ -44,6 +44,16 @@
@test repr("text/plain", signed_distance_field) == show_box
end
+ @testset verbose=true "Open Geometry Validation" begin
+ open_square = [0.0 1.0 1.0 0.0;
+ 0.0 0.0 1.0 1.0]
+ geometry = TrixiParticles.Polygon(open_square; close_curve=false)
+
+ @test SignedDistanceField(geometry, 0.1) isa SignedDistanceField
+ @test_throws ArgumentError SignedDistanceField(geometry, 0.1;
+ use_for_boundary_packing=true)
+ end
+
@testset verbose=true "Real World Data" begin
data_dir = pkgdir(TrixiParticles, "examples", "preprocessing", "data")
validation_dir = pkgdir(TrixiParticles, "test", "preprocessing", "data")
@@ -135,4 +145,15 @@
end
end
end
+
+ @testset verbose=true "Point Matrix Input" begin
+ data_dir = pkgdir(TrixiParticles, "examples", "preprocessing", "data")
+ geometry = load_geometry(joinpath(data_dir, "hexagon.asc"))
+
+ point = first(geometry.vertices)
+ signed_distance_field = SignedDistanceField(geometry, 0.1; points=hcat(point))
+
+ @test signed_distance_field.positions == [point]
+ @test signed_distance_field.distances == [0.0]
+ end
end
diff --git a/test/preprocessing/point_in_poly/winding_number_jacobson.jl b/test/preprocessing/point_in_poly/winding_number_jacobson.jl
index ee2119a62d..3622b48f52 100644
--- a/test/preprocessing/point_in_poly/winding_number_jacobson.jl
+++ b/test/preprocessing/point_in_poly/winding_number_jacobson.jl
@@ -3,6 +3,11 @@
data_dir = pkgdir(TrixiParticles, "examples", "preprocessing", "data")
geometry = load_geometry(joinpath(data_dir, "circle.asc"))
+ winding = WindingNumberJacobson()
+
+ show_compact = "WindingNumberJacobson{NaiveWinding}()"
+ @test repr(winding) == show_compact
+
winding = WindingNumberJacobson(; hierarchical_winding=false)
show_compact = "WindingNumberJacobson{NaiveWinding}()"
@@ -30,4 +35,33 @@
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"""
@test repr("text/plain", winding) == show_box
end
+
+ @testset verbose=true "Point Matrix Input" begin
+ geometry = TrixiParticles.Polygon([0.0 1.0 1.0 0.0 0.0;
+ 0.0 0.0 1.0 1.0 0.0])
+ point_storage = [0.5 0.0 1.5;
+ 0.5 0.0 1.5]
+ points = @view point_storage[:, 1:2:3]
+
+ expected = Bool[true, false]
+
+ inpoly_jacobson, _ = WindingNumberJacobson()(geometry, points)
+ inpoly_hormann, _ = WindingNumberHormann()(geometry, points)
+
+ @test inpoly_jacobson == expected
+ @test inpoly_hormann == expected
+ end
+
+ @testset verbose=true "Open Geometry Validation" begin
+ open_square = [0.0 1.0 1.0 0.0;
+ 0.0 0.0 1.0 1.0]
+ geometry = TrixiParticles.Polygon(open_square; close_curve=false)
+ points = [SVector(0.5, 0.5)]
+
+ jacobson = WindingNumberJacobson(; hierarchical_winding=false)
+ hormann = WindingNumberHormann()
+
+ @test jacobson(geometry, points)[1] isa Vector{Bool}
+ @test hormann(geometry, points)[1] isa Vector{Bool}
+ end
end
diff --git a/test/schemes/boundary/dummy_particles/dummy_particles.jl b/test/schemes/boundary/dummy_particles/dummy_particles.jl
index 8317bd144a..91831b6ddf 100644
--- a/test/schemes/boundary/dummy_particles/dummy_particles.jl
+++ b/test/schemes/boundary/dummy_particles/dummy_particles.jl
@@ -10,6 +10,51 @@
@test repr(boundary_model) == expected_repr
end
+ @testset "Surface quadrature" begin
+ densities = Float32[1000, 1000, 1000]
+ masses = Float32[1, 1, 1]
+ smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+ smoothing_length = 0.1f0
+ surface_measure = [0.1, 0.0, 0.2]
+ expected_measure = Float32[0.1, 0.0, 0.2]
+
+ boundary_model = BoundaryModelDummyParticles(densities, masses,
+ SummationDensity(),
+ smoothing_kernel,
+ smoothing_length;
+ surface_measure)
+ @test haskey(boundary_model.cache, :surface_measure)
+ @test boundary_model.cache.surface_measure == expected_measure
+ @test eltype(boundary_model.cache.surface_measure) == Float32
+
+ # Setup data is copied so later changes to the input do not alter the model.
+ surface_measure[1] = 1.0
+ @test boundary_model.cache.surface_measure == expected_measure
+
+ adapted_model = TrixiParticles.Adapt.adapt(Array, boundary_model)
+ @test adapted_model.cache.surface_measure == expected_measure
+
+ default_model = BoundaryModelDummyParticles(densities, masses,
+ SummationDensity(),
+ smoothing_kernel,
+ smoothing_length)
+ @test !haskey(default_model.cache, :surface_measure)
+
+ make_model(measure) = BoundaryModelDummyParticles(densities, masses,
+ SummationDensity(),
+ smoothing_kernel,
+ smoothing_length;
+ surface_measure=measure)
+ @test make_model(zeros(3)).cache.surface_measure == zeros(Float32, 3)
+ @test_throws ArgumentError make_model(1.0)
+ @test_throws ArgumentError make_model(zeros(2))
+ @test_throws ArgumentError make_model([-1.0, 0.0, 0.0])
+ @test_throws ArgumentError make_model([NaN, 0.0, 0.0])
+ @test_throws ArgumentError make_model([Inf, 0.0, 0.0])
+ @test_throws ArgumentError make_model([1.0e100, 0.0, 0.0])
+ @test_throws ArgumentError make_model(Any["invalid", 0.0, 0.0])
+ end
+
@testset "Pressure clipping" begin
state_equation = StateEquationCole(sound_speed=10.0,
reference_density=1000.0,
@@ -458,8 +503,8 @@
(width_reference, height_reference),
(width_reference, height_reference),
density; acceleration=[0.0, -9.81],
- state_equation, n_layers=0,
- faces=(true, true, true, false))
+ state_equation,
+ faces=(false, false, false, false))
# Because it is a pain to deal with the linear indices of the pressure arrays,
# we convert the matrices to Cartesian indices based on the coordinates.
diff --git a/test/schemes/boundary/open_boundary/boundary_zone.jl b/test/schemes/boundary/open_boundary/boundary_zone.jl
index 8542fd554d..869f565f87 100644
--- a/test/schemes/boundary/open_boundary/boundary_zone.jl
+++ b/test/schemes/boundary/open_boundary/boundary_zone.jl
@@ -256,6 +256,24 @@
end
end
+ @testset verbose=true "Boundary Zone 3D Float32 Tolerance" begin
+ edge1 = Float32[0.6208666, 0.6295315, 0.46713477]
+ edge2 = Float32[-0.48528308, 0.7766439, -0.4016525]
+ boundary_face = (zeros(Float32, 3), edge1, edge2)
+ face_normal = normalize(cross(edge1, edge2))
+
+ # This is orthogonal to Float32 precision, but not to a Float64-based tolerance.
+ @test abs(dot(edge1, edge2)) > sqrt(eps()) * norm(edge1) * norm(edge2)
+ @test abs(dot(edge1, edge2)) <= sqrt(eps(Float32)) * norm(edge1) * norm(edge2)
+
+ boundary_zone = BoundaryZone(; boundary_face, particle_spacing=0.5f0,
+ face_normal, density=1.0f0,
+ open_boundary_layers=1, boundary_type=InFlow(),
+ sample_points=nothing)
+
+ @test size(boundary_zone.initial_condition.coordinates, 2) > 0
+ end
+
@testset verbose=true "Particle In Boundary Zone 2D" begin
face_vertices = [[-0.2, -0.5], [0.3, 0.6]]
face_size = face_vertices[2] - face_vertices[1]
@@ -367,6 +385,26 @@
open_boundary_layers=2,
boundary_type=OutFlow())
+ non_orthogonal_face = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]]
+ flow_direction = [0.0, 0.0, 1.0]
+
+ error_str = "the vectors `AB` and `AC` must be orthogonal"
+
+ @test_throws ArgumentError(error_str) BoundaryZone(;
+ boundary_face=non_orthogonal_face,
+ particle_spacing=0.1,
+ face_normal=flow_direction,
+ density=1.0,
+ open_boundary_layers=2,
+ boundary_type=InFlow())
+ @test_throws ArgumentError(error_str) BoundaryZone(;
+ boundary_face=non_orthogonal_face,
+ particle_spacing=0.1,
+ face_normal=(-flow_direction),
+ density=1.0,
+ open_boundary_layers=2,
+ boundary_type=OutFlow())
+
rectangular_face = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
flow_direction = [0.0, 1.0, 0.0]
diff --git a/test/schemes/boundary/open_boundary/characteristic_variables.jl b/test/schemes/boundary/open_boundary/characteristic_variables.jl
index 9d2e9fa03e..7e21b0efc4 100644
--- a/test/schemes/boundary/open_boundary/characteristic_variables.jl
+++ b/test/schemes/boundary/open_boundary/characteristic_variables.jl
@@ -18,6 +18,72 @@
# Add small offset to avoid "ArgumentError: density must be positive and larger than `eps()`"
reference_density = (pos, t) -> 1000.0 * (t + sqrt(eps()))
+ @testset "Reject bidirectional flow" begin
+ initial_condition = rectangular_patch(particle_spacing, (2, 2))
+ fluid_system = WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation=nothing)
+
+ bidirectional = BoundaryZone(; boundary_face=([0.0, 0.0], [0.0, 1.0]),
+ face_normal=[1.0, 0.0], open_boundary_layers,
+ density, particle_spacing)
+ boundary_system = OpenBoundarySystem(bidirectional; fluid_system, buffer_size=0,
+ boundary_model=BoundaryModelCharacteristicsLastiwka())
+
+ error_str = "`BoundaryModelCharacteristicsLastiwka` needs a directed boundary zone. " *
+ "Please specify `InFlow()` or `OutFlow()` instead of `BidirectionalFlow()`."
+ @test_throws ArgumentError(error_str) Semidiscretization(fluid_system,
+ boundary_system)
+ end
+
+ @testset "Fallback is zone-local" begin
+ face_vertices = ([0.0, 0.0], [0.0, 0.5])
+ face_vertices_far = ([10.0, 0.0], [10.0, 0.5])
+ flow_direction = SVector(1.0, 0.0)
+
+ inflow = BoundaryZone(; boundary_face=face_vertices, face_normal=flow_direction,
+ open_boundary_layers, boundary_type=InFlow(),
+ reference_velocity, reference_pressure, reference_density,
+ density, particle_spacing)
+ inflow_far = BoundaryZone(; boundary_face=face_vertices_far,
+ face_normal=flow_direction,
+ open_boundary_layers, boundary_type=InFlow(),
+ reference_velocity, reference_pressure, reference_density,
+ density, particle_spacing)
+ fluid = extrude_geometry(face_vertices; particle_spacing, n_extrude=4,
+ density, pressure, direction=flow_direction)
+ fluid_system = EntropicallyDampedSPHSystem(fluid; smoothing_kernel,
+ smoothing_length,
+ sound_speed,
+ buffer_size=0,
+ density_calculator=ContinuityDensity())
+ boundary_system = OpenBoundarySystem(inflow, inflow_far;
+ fluid_system, buffer_size=0,
+ boundary_model=BoundaryModelCharacteristicsLastiwka())
+ semi = Semidiscretization(fluid_system, boundary_system)
+ ode = semidiscretize(semi, (0.0, 5.0))
+
+ v0_ode, u0_ode = ode.u0.x
+ v = TrixiParticles.wrap_v(v0_ode, boundary_system, semi)
+ u = TrixiParticles.wrap_u(u0_ode, boundary_system, semi)
+
+ TrixiParticles.evaluate_characteristics!(boundary_system,
+ v, u, v0_ode, u0_ode, semi, 2.0)
+ TrixiParticles.evaluate_characteristics!(boundary_system,
+ v, u, v0_ode, u0_ode, semi, 3.0)
+
+ zone_1_particles = findall(==(1), boundary_system.boundary_zone_indices)
+ zone_2_particles = findall(==(2), boundary_system.boundary_zone_indices)
+
+ @test any(!isapprox(characteristic, 0.0)
+ for characteristic in boundary_system.cache.characteristics[:,
+ zone_1_particles])
+ @test all(isapprox(characteristic, 0.0)
+ for characteristic in boundary_system.cache.characteristics[:,
+ zone_2_particles])
+ end
+
# 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]]
@@ -144,7 +210,7 @@
initial_condition = rectangular_patch(particle_spacing, ntuple(_ -> 2, n_dims))
boundary_face = n_dims == 2 ? ([0.0, 0.0], [0.0, 1.0]) :
- ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 1.0])
+ ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
face_normal = n_dims == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]
inflow = BoundaryZone(; boundary_face, boundary_type=InFlow(), face_normal,
open_boundary_layers=10, density=1.0, particle_spacing)
diff --git a/test/schemes/boundary/open_boundary/dynamical_pressure.jl b/test/schemes/boundary/open_boundary/dynamical_pressure.jl
index bd325eb690..dbe80f7406 100644
--- a/test/schemes/boundary/open_boundary/dynamical_pressure.jl
+++ b/test/schemes/boundary/open_boundary/dynamical_pressure.jl
@@ -123,7 +123,7 @@
initial_condition = rectangular_patch(particle_spacing, ntuple(_ -> 2, n_dims))
boundary_face = n_dims == 2 ? ([0.0, 0.0], [0.0, 1.0]) :
- ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 1.0])
+ ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
face_normal = n_dims == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]
inflow = BoundaryZone(; boundary_face, boundary_type=InFlow(), face_normal,
open_boundary_layers=10, density=1.0, particle_spacing)
diff --git a/test/schemes/boundary/open_boundary/mirroring.jl b/test/schemes/boundary/open_boundary/mirroring.jl
index d9e41ee0a2..5bd9a5e5d7 100644
--- a/test/schemes/boundary/open_boundary/mirroring.jl
+++ b/test/schemes/boundary/open_boundary/mirroring.jl
@@ -548,7 +548,7 @@
initial_condition = rectangular_patch(particle_spacing, ntuple(_ -> 2, n_dims))
boundary_face = n_dims == 2 ? ([0.0, 0.0], [0.0, 1.0]) :
- ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 1.0])
+ ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
face_normal = n_dims == 2 ? [1.0, 0.0] : [1.0, 0.0, 0.0]
inflow = BoundaryZone(; boundary_face, boundary_type=InFlow(), face_normal,
open_boundary_layers=10, density=1.0, particle_spacing)
diff --git a/test/schemes/fluid/pressure_acceleration.jl b/test/schemes/fluid/pressure_acceleration.jl
index 80de3d9a66..7c01e9e3d0 100644
--- a/test/schemes/fluid/pressure_acceleration.jl
+++ b/test/schemes/fluid/pressure_acceleration.jl
@@ -65,4 +65,202 @@
correction_dict_2[correction_name])
end
end
+
+ @testset verbose=true "Interface-Aware Tensile Instability Control" begin
+ control = InterfaceAwareTensileInstabilityControl()
+ @test control.strength == 1.0
+ @test InterfaceAwareTensileInstabilityControl(; strength=0.25).strength == 0.25
+ for strength in (0, -1, 1.1, Inf, NaN, "invalid")
+ @test_throws ArgumentError InterfaceAwareTensileInstabilityControl(; strength)
+ end
+
+ m_a = m_b = rho_a = rho_b = 1.0
+ p_a = -2.0
+ p_b = 0.5
+ grad_kernel = SVector(1.0, -0.5)
+ standard = TrixiParticles.pressure_acceleration_continuity_density(m_a, m_b,
+ rho_a, rho_b,
+ p_a, p_b,
+ grad_kernel)
+ controlled = tensile_instability_control(m_a, m_b, rho_a, rho_b,
+ p_a, p_b, grad_kernel)
+ interface_aware(activity_a, activity_b,
+ strength=1.0) = TrixiParticles.interface_aware_tensile_acceleration(m_a,
+ m_b,
+ rho_a,
+ rho_b,
+ p_a,
+ p_b,
+ grad_kernel,
+ activity_a,
+ activity_b,
+ strength)
+
+ @test interface_aware(0.0, 0.0) == controlled
+ @test interface_aware(1.0, 0.0) == standard
+ @test interface_aware(0.0, 1.0) == standard
+ @test interface_aware(0.5, 0.0) == (standard + controlled) / 2
+ @test interface_aware(0.0, 0.5) == (standard + controlled) / 2
+ @test interface_aware(0.0, 0.0, 0.25) ==
+ standard + 0.25 * (controlled - standard)
+ @test interface_aware(-1.0, -0.5) == controlled
+ @test interface_aware(2.0, 0.0) == standard
+ @test interface_aware(NaN, 0.0) == standard
+ @test interface_aware(Inf, 0.0) == standard
+
+ colorfield = ColorfieldSurfaceNormal(; ideal_density_threshold=0.95)
+ css = SurfaceTensionMomentumMorris(; surface_tension_coefficient=1.0)
+ morris = SurfaceTensionMorris(; surface_tension_coefficient=1.0)
+ ccsf = CorrectedCSFSurfaceNormal()
+ @test TrixiParticles.supports_interface_aware_tic(colorfield, css)
+ @test TrixiParticles.supports_interface_aware_tic(colorfield, morris)
+ @test TrixiParticles.supports_interface_aware_tic(ccsf, morris)
+ @test !TrixiParticles.supports_interface_aware_tic(ccsf, css)
+ @test !TrixiParticles.supports_interface_aware_tic(nothing, morris)
+
+ state_equation = StateEquationCole(; sound_speed=10.0,
+ reference_density=1000.0,
+ exponent=7,
+ clip_negative_pressure=false)
+ clipped_state_equation = StateEquationCole(; sound_speed=10.0,
+ reference_density=1000.0,
+ exponent=7,
+ clip_negative_pressure=true)
+ validate(density_calculator, equation, normal_method, surface_tension,
+ correction=nothing) = TrixiParticles.validate_interface_aware_tic(control,
+ density_calculator,
+ equation,
+ normal_method,
+ surface_tension,
+ correction)
+ @test_nowarn validate(ContinuityDensity(), state_equation, colorfield, css)
+ @test_nowarn validate(ContinuityDensity(), state_equation, colorfield, css,
+ AkinciFreeSurfaceCorrection(1000.0))
+ @test_throws ArgumentError validate(SummationDensity(), state_equation,
+ colorfield, css)
+ @test_throws ArgumentError validate(ContinuityDensity(), clipped_state_equation,
+ colorfield, css)
+ @test_throws ArgumentError validate(ContinuityDensity(), state_equation,
+ colorfield, css, KernelCorrection())
+ @test_throws ArgumentError validate(ContinuityDensity(), state_equation,
+ nothing, css)
+
+ @test TrixiParticles.choose_pressure_acceleration_formulation(control,
+ ContinuityDensity(),
+ 2, Float64,
+ nothing) === control
+ @test_throws ArgumentError TrixiParticles.choose_pressure_acceleration_formulation(control,
+ SummationDensity(),
+ 2,
+ Float64,
+ nothing)
+ @test_throws ArgumentError TrixiParticles.choose_pressure_acceleration_formulation(control,
+ ContinuityDensity(),
+ 2,
+ Float64,
+ GradientCorrection())
+
+ particle_spacing = 0.1
+ initial_condition = RectangularShape(particle_spacing, (3, 3), (0.0, 0.0);
+ density=1000.0)
+ smoothing_kernel = WendlandC2Kernel{2}()
+ smoothing_length = 1.4particle_spacing
+ system = WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation,
+ pressure_acceleration=control,
+ surface_tension=css,
+ surface_normal_method=colorfield,
+ reference_particle_spacing=particle_spacing)
+ @test system.pressure_acceleration_formulation === control
+ system_data = Dict{String, Any}()
+ @test_nowarn TrixiParticles.add_system_data!(system_data, system)
+ @test system_data["pressure_acceleration_formulation"] ==
+ :InterfaceAwareTensileInstabilityControl
+ @test system_data["interface_aware_tic_strength"] == 1.0
+
+ system.cache.interface_activity .= 0.0
+ system.cache.interface_activity[2] = 0.5
+ actual = TrixiParticles.pressure_acceleration(system, system, 1, 2,
+ m_a, m_b, p_a, p_b,
+ rho_a, rho_b,
+ SVector(0.1, 0.0), 0.1,
+ grad_kernel, nothing)
+ @test actual == interface_aware(0.0, 0.5)
+
+ boundary_result = TrixiParticles.evaluate_pressure_acceleration(control, system,
+ nothing, 1, 1,
+ m_a, m_b,
+ rho_a, rho_b,
+ p_a, p_b,
+ grad_kernel)
+ @test boundary_result == standard
+
+ neighbor_without_interface = WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation)
+ unsupported_fluid_result = TrixiParticles.evaluate_pressure_acceleration(control,
+ system,
+ neighbor_without_interface,
+ 1, 1,
+ m_a, m_b,
+ rho_a,
+ rho_b,
+ p_a, p_b,
+ grad_kernel)
+ @test unsupported_fluid_result == standard
+
+ matrix_pressure_a = TrixiParticles.SMatrix{2, 2}(1.0, 0.0, 0.0, 2.0)
+ matrix_pressure_b = TrixiParticles.SMatrix{2, 2}(0.5, 0.0, 0.0, 1.0)
+ matrix_result = TrixiParticles.evaluate_pressure_acceleration(control, system,
+ system, 1, 2,
+ m_a, m_b,
+ rho_a, rho_b,
+ matrix_pressure_a,
+ matrix_pressure_b,
+ grad_kernel)
+ @test matrix_result ==
+ TrixiParticles.pressure_acceleration_continuity_density(m_a, m_b,
+ rho_a, rho_b,
+ matrix_pressure_a,
+ matrix_pressure_b,
+ grad_kernel)
+
+ @test_throws ArgumentError WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation,
+ pressure_acceleration=control)
+
+ edac = EntropicallyDampedSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length, sound_speed=10.0,
+ density_calculator=ContinuityDensity(),
+ pressure_acceleration=control,
+ surface_tension=css,
+ surface_normal_method=colorfield,
+ reference_particle_spacing=particle_spacing)
+ @test edac.pressure_acceleration_formulation === control
+ @test_throws ArgumentError EntropicallyDampedSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length,
+ sound_speed=10.0,
+ pressure_acceleration=control,
+ surface_tension=css,
+ surface_normal_method=colorfield,
+ reference_particle_spacing=particle_spacing)
+
+ ccsf_system = WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation,
+ pressure_acceleration=control,
+ surface_tension=morris,
+ surface_normal_method=ccsf,
+ reference_particle_spacing=particle_spacing)
+ @test ccsf_system.pressure_acceleration_formulation === control
+ end
end
diff --git a/test/schemes/fluid/shifting_techniques.jl b/test/schemes/fluid/shifting_techniques.jl
index 20ef2372fa..3c31cd1ffc 100644
--- a/test/schemes/fluid/shifting_techniques.jl
+++ b/test/schemes/fluid/shifting_techniques.jl
@@ -7,6 +7,81 @@
@test_nowarn ConsistentShiftingSun2019()
pst = @test_nowarn ConsistentShiftingSun2019(sound_speed_factor=0.2)
@test pst.v_factor == 0.2
+ @test isnothing(pst.free_surface_treatment)
+
+ treatment = FreeSurfaceTangentialShifting()
+ pst = @test_nowarn ConsistentShiftingSun2019(; free_surface_treatment=treatment)
+ @test pst.free_surface_treatment === treatment
+ callback_pst = @test_nowarn ParticleShiftingTechniqueSun2017(;
+ free_surface_treatment=treatment)
+ @test callback_pst.free_surface_treatment === treatment
+ @test_throws ArgumentError ParticleShiftingTechnique(free_surface_treatment=:invalid)
+
+ css = SurfaceTensionMomentumMorris(; surface_tension_coefficient=1.0)
+ morris = SurfaceTensionMorris(; surface_tension_coefficient=1.0)
+ @test_throws ArgumentError TrixiParticles.validate_free_surface_shifting(pst,
+ nothing,
+ css)
+ @test_throws ArgumentError TrixiParticles.validate_free_surface_shifting(pst,
+ ColorfieldSurfaceNormal(),
+ nothing)
+ @test_throws ArgumentError TrixiParticles.validate_free_surface_shifting(pst,
+ ColorfieldSurfaceNormal(),
+ SurfaceTensionAkinci())
+ @test_throws ArgumentError TrixiParticles.validate_free_surface_shifting(pst,
+ CorrectedCSFSurfaceNormal(),
+ css)
+ @test_nowarn TrixiParticles.validate_free_surface_shifting(pst,
+ ColorfieldSurfaceNormal(),
+ css)
+ @test_nowarn TrixiParticles.validate_free_surface_shifting(pst,
+ ColorfieldSurfaceNormal(),
+ morris)
+ @test_nowarn TrixiParticles.validate_free_surface_shifting(pst,
+ CorrectedCSFSurfaceNormal(),
+ morris)
+
+ system_data = Dict{String, Any}()
+ TrixiParticles.add_system_data!(system_data, pst)
+ @test system_data["shifting_technique"]["free_surface_treatment"] ==
+ "FreeSurfaceTangentialShifting"
+ default_data = Dict{String, Any}()
+ TrixiParticles.add_system_data!(default_data, ConsistentShiftingSun2019())
+ @test isnothing(default_data["shifting_technique"]["free_surface_treatment"])
+
+ particle_spacing = 0.1
+ initial_condition = RectangularShape(particle_spacing, (2, 2), (0.0, 0.0);
+ density=1.0)
+ smoothing_kernel = WendlandC2Kernel{2}()
+ normal_method = ColorfieldSurfaceNormal()
+ @test_throws ArgumentError WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length=1.4particle_spacing,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density=1.0,
+ exponent=7),
+ surface_normal_method=normal_method,
+ shifting_technique=pst,
+ reference_particle_spacing=particle_spacing)
+ @test_throws ArgumentError EntropicallyDampedSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length=1.4particle_spacing,
+ sound_speed=10.0,
+ density_calculator=ContinuityDensity(),
+ surface_normal_method=normal_method,
+ shifting_technique=pst,
+ reference_particle_spacing=particle_spacing)
+ @test_nowarn EntropicallyDampedSPHSystem(initial_condition;
+ smoothing_kernel,
+ smoothing_length=1.4particle_spacing,
+ sound_speed=10.0,
+ density_calculator=ContinuityDensity(),
+ surface_tension=css,
+ surface_normal_method=normal_method,
+ shifting_technique=pst,
+ reference_particle_spacing=particle_spacing)
# Can't use both `v_max_factor` and `sound_speed_factor`
@test_throws ArgumentError ParticleShiftingTechnique(v_max_factor=1.0,
@@ -28,4 +103,129 @@
modify_continuity_equation=false,
second_continuity_equation_term=ContinuityEquationTermSun2019())
end
+
+ @testset "Tangential free-surface projection" begin
+ shifting_velocity = SVector(3.0, 4.0)
+ normal = SVector(1.0, 0.0)
+
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity, normal,
+ 0.0) == shifting_velocity
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity, normal,
+ 0.5) ≈ SVector(1.5, 4.0)
+ tangential = TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ normal, 1.0)
+ @test tangential ≈ SVector(0.0, 4.0)
+ @test dot(tangential, normal) ≈ 0.0
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ 2normal, 1.0) ≈ tangential
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ normal, -1.0) == shifting_velocity
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ normal, 2.0) ≈ tangential
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ zero(normal), 1.0) ==
+ shifting_velocity
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ SVector(NaN, 0.0), 1.0) ==
+ shifting_velocity
+ @test TrixiParticles.tangential_shifting_velocity(shifting_velocity, normal,
+ NaN) == shifting_velocity
+
+ particle_spacing = 0.1
+ initial_condition = RectangularShape(particle_spacing, (2, 2), (0.0, 0.0);
+ density=1.0)
+ surface_tension = SurfaceTensionMomentumMorris(;
+ surface_tension_coefficient=1.0)
+ surface_normal_method = ColorfieldSurfaceNormal(; normal_smoothing=true)
+ treatment = FreeSurfaceTangentialShifting()
+ shifting_technique = ConsistentShiftingSun2019(;
+ free_surface_treatment=treatment)
+ system = WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel=WendlandC2Kernel{2}(),
+ smoothing_length=1.4particle_spacing,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density=1.0,
+ exponent=7),
+ surface_tension, surface_normal_method,
+ shifting_technique,
+ reference_particle_spacing=particle_spacing)
+ system.cache.delta_v .= reshape([3.0, 4.0], 2, 1)
+ system.cache.surface_normal .= reshape([1.0, 0.0], 2, 1)
+ system.cache.smoothed_surface_normal .= reshape([0.0, 1.0], 2, 1)
+ system.cache.interface_activity .= [1.0, 0.5, 0.0, 1.0]
+ system.cache.surface_normal[:, 4] .= 0
+
+ TrixiParticles.modify_shifting_with_surface_normal!(system, treatment,
+ DummySemidiscretization())
+ @test system.cache.delta_v[:, 1] ≈ [0.0, 4.0]
+ @test system.cache.delta_v[:, 2] ≈ [1.5, 4.0]
+ @test system.cache.delta_v[:, 3] ≈ [3.0, 4.0]
+ @test system.cache.delta_v[:, 4] ≈ [3.0, 4.0]
+
+ system.cache.delta_v .= reshape([3.0, 4.0], 2, 1)
+ TrixiParticles.modify_shifting_with_surface_normal!(system, nothing,
+ DummySemidiscretization())
+ @test all(particle -> system.cache.delta_v[:, particle] ≈ [3.0, 4.0],
+ eachparticle(system))
+ end
+
+ @testset "Integrated tangential shifting" begin
+ function shifting_system(free_surface_treatment)
+ particle_spacing = 0.1
+ initial_condition = RectangularShape(particle_spacing, (7, 7), (0.0, 0.0);
+ density=1000.0)
+ surface_tension = SurfaceTensionMomentumMorris(;
+ surface_tension_coefficient=0.072)
+ surface_normal_method = ColorfieldSurfaceNormal(;
+ ideal_density_threshold=0.9,
+ normal_smoothing=true)
+ shifting_technique = ConsistentShiftingSun2019(;
+ free_surface_treatment)
+ system = WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel=WendlandC2Kernel{2}(),
+ smoothing_length=1.4particle_spacing,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density=1000.0,
+ exponent=7),
+ surface_tension, surface_normal_method,
+ shifting_technique,
+ reference_particle_spacing=particle_spacing)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ TrixiParticles.update_systems_and_nhs(ode.u0.x..., semi, 0.0)
+ return system
+ end
+
+ untreated = shifting_system(nothing)
+ treated = shifting_system(FreeSurfaceTangentialShifting())
+ @test treated.cache.surface_normal ≈ untreated.cache.surface_normal
+ @test treated.cache.interface_activity ≈ untreated.cache.interface_activity
+
+ expected = similar(untreated.cache.delta_v)
+ for particle in eachparticle(treated)
+ shifting_velocity = TrixiParticles.extract_svector(untreated.cache.delta_v,
+ untreated, particle)
+ normal = TrixiParticles.surface_normal(treated, particle)
+ activity = treated.cache.interface_activity[particle]
+ expected[:,
+ particle] = TrixiParticles.tangential_shifting_velocity(shifting_velocity,
+ normal,
+ activity)
+ end
+ @test treated.cache.delta_v ≈ expected
+ @test maximum(abs, treated.cache.delta_v - untreated.cache.delta_v) > 1.0e-8
+
+ surface = findall(==(1), treated.cache.interface_activity)
+ @test !isempty(surface)
+ @test maximum(surface) do particle
+ normal = TrixiParticles.surface_normal(treated, particle)
+ shifting_velocity = TrixiParticles.extract_svector(treated.cache.delta_v,
+ treated, particle)
+ abs(dot(shifting_velocity, normal))
+ end < 1.0e-12
+ end
end
diff --git a/test/schemes/fluid/surface_normal_sph.jl b/test/schemes/fluid/surface_normal_sph.jl
index 5eb8a81704..7d152b3a24 100644
--- a/test/schemes/fluid/surface_normal_sph.jl
+++ b/test/schemes/fluid/surface_normal_sph.jl
@@ -113,9 +113,6 @@ function compute_and_test_surface_values(system, semi, ode; NDIMS=2)
TrixiParticles.compute_surface_normal!(system, system.surface_normal_method, v, u,
v0_ode, u0_ode, semi, 0.0)
- TrixiParticles.remove_invalid_normals!(system, system.surface_tension,
- system.surface_normal_method)
-
# After computation, check that surface normals have been computed and are not NaN or Inf
@test all(isfinite, system.cache.surface_normal)
@test all(isfinite, system.cache.neighbor_count)
@@ -143,6 +140,253 @@ function compute_curvature!(system, semi, ode)
v, u, v0_ode, u0_ode, semi, 0.0)
end
+@testset "Corrected C-CSF interface geometry" begin
+ particle_spacing = 0.05
+ radius = 0.5
+ reference_density = 1000.0
+ smoothing_kernel = WendlandC2Kernel{2}()
+ smoothing_length = 1.4particle_spacing
+ fluid = SphereShape(particle_spacing, radius, (0.0, 0.0), reference_density;
+ sphere_type=RoundSphere())
+ state_equation = StateEquationCole(; sound_speed=10.0, reference_density,
+ exponent=7)
+ surface_tension = SurfaceTensionMorris(; surface_tension_coefficient=1.0)
+ system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation, surface_tension,
+ surface_normal_method=CorrectedCSFSurfaceNormal(),
+ reference_particle_spacing=particle_spacing)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ TrixiParticles.update_systems_and_nhs(ode.u0.x..., semi, 0.0)
+
+ cache = system.cache
+ active = findall(>(0), cache.interface_activity)
+ @test !isempty(active)
+ @test all(isfinite, cache.ccsf_minimum_eigenvalue)
+ @test 0.4 < minimum(cache.ccsf_minimum_eigenvalue) < 0.6
+ @test maximum(cache.ccsf_minimum_eigenvalue) > 0.99
+ @test all(isfinite, cache.surface_normal)
+ @test all(isfinite, cache.curvature)
+ @test all(>=(0), cache.delta_s)
+ @test all(active) do particle
+ dot(TrixiParticles.surface_normal(system, particle),
+ fluid.coordinates[:, particle]) > 0
+ end
+
+ weighted_curvature = sum(cache.curvature[active] .* cache.delta_s[active]) /
+ sum(cache.delta_s[active])
+ @test isapprox(weighted_curvature, inv(radius); rtol=0.15)
+
+ system_data = Dict{String, Any}()
+ TrixiParticles.add_system_data!(system_data, system.surface_normal_method)
+ @test system_data["surface_normal_method"]["model"] ==
+ "CorrectedCSFSurfaceNormal"
+
+ @test_throws ArgumentError WeaklyCompressibleSPHSystem(fluid; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation,
+ surface_tension=SurfaceTensionMomentumMorris(),
+ surface_normal_method=CorrectedCSFSurfaceNormal(),
+ reference_particle_spacing=particle_spacing)
+end
+
+@testset "Corrected C-CSF 3D curvature" begin
+ particle_spacing = 0.05
+ radius = 0.5
+ reference_density = 1000.0
+ smoothing_kernel = WendlandC2Kernel{3}()
+ smoothing_length = 1.4particle_spacing
+ fluid = SphereShape(particle_spacing, radius, (0.0, 0.0, 0.0), reference_density;
+ sphere_type=RoundSphere())
+ system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density,
+ exponent=7),
+ surface_tension=SurfaceTensionMorris(;
+ surface_tension_coefficient=1.0),
+ surface_normal_method=CorrectedCSFSurfaceNormal(),
+ reference_particle_spacing=particle_spacing)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ TrixiParticles.update_systems_and_nhs(ode.u0.x..., semi, 0.0)
+
+ cache = system.cache
+ active = findall(>(0), cache.interface_activity)
+ @test !isempty(active)
+ @test all(active) do particle
+ dot(TrixiParticles.surface_normal(system, particle),
+ fluid.coordinates[:, particle]) > 0
+ end
+ @test minimum(cache.curvature[active]) > 0
+
+ weighted_curvature = sum(cache.curvature[active] .* cache.delta_s[active]) /
+ sum(cache.delta_s[active])
+ @test isapprox(weighted_curvature, 2 / radius; rtol=0.15)
+end
+
+@testset "Shepard-smoothed CSS normals" begin
+ particle_spacing = 0.1
+ reference_density = 1000.0
+ fluid = SphereShape(particle_spacing, 0.5, (0.0, 0.0, 0.0), reference_density;
+ sphere_type=RoundSphere())
+ smoothing_kernel = WendlandC2Kernel{3}()
+ smoothing_length = 1.4particle_spacing
+ state_equation = StateEquationCole(; sound_speed=10.0, reference_density,
+ exponent=7)
+ surface_tension = SurfaceTensionMomentumMorris(; surface_tension_coefficient=1.0)
+ normal_method = ColorfieldSurfaceNormal(; ideal_density_threshold=0.95,
+ normal_smoothing=true)
+ system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation, surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=particle_spacing)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ TrixiParticles.update_systems_and_nhs(v_ode, u_ode, semi, 0.0)
+
+ active = findall(>(0), system.cache.interface_activity)
+ @test !isempty(active)
+ @test haskey(system.cache, :smoothed_surface_normal)
+ @test haskey(system.cache, :normal_smoothing_weight)
+ @test all(isfinite, system.cache.smoothed_surface_normal)
+ @test all(isfinite, system.cache.normal_smoothing_weight)
+ @test all(active) do particle
+ isapprox(norm(TrixiParticles.surface_tension_normal(system, particle)), 1;
+ atol=1.0e-12)
+ end
+
+ raw_system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation, surface_tension,
+ surface_normal_method=ColorfieldSurfaceNormal(;
+ ideal_density_threshold=0.95),
+ reference_particle_spacing=particle_spacing)
+ raw_semi = Semidiscretization(raw_system)
+ raw_ode = semidiscretize(raw_semi, (0.0, 0.01))
+ TrixiParticles.update_systems_and_nhs(raw_ode.u0.x..., raw_semi, 0.0)
+
+ # Smoothing changes only the capillary direction, not raw geometry or activity.
+ @test !haskey(raw_system.cache, :smoothed_surface_normal)
+ @test !haskey(raw_system.cache, :normal_smoothing_weight)
+ @test system.cache.surface_normal ≈ raw_system.cache.surface_normal
+ @test system.cache.interface_activity ≈ raw_system.cache.interface_activity
+ @test system.cache.delta_s ≈ raw_system.cache.delta_s
+ differences = [norm(TrixiParticles.surface_tension_normal(system, particle) -
+ TrixiParticles.surface_normal(system, particle))
+ for particle in active]
+ candidate = active[argmax(differences)]
+ @test maximum(differences) > 1.0e-4
+
+ inactive = setdiff(eachparticle(system), active)
+ @test all(particle -> iszero(TrixiParticles.surface_tension_normal(system, particle)),
+ inactive)
+
+ grad_kernel = SVector(0.3, -0.4, 0.2)
+ normal = TrixiParticles.surface_tension_normal(system, candidate)
+ delta_s = system.cache.delta_s[candidate]
+ expected_stress_gradient = delta_s *
+ (grad_kernel - normal * dot(normal, grad_kernel))
+ @test TrixiParticles.surface_stress_times_gradient(system, candidate, grad_kernel) ≈
+ expected_stress_gradient
+
+ vtk = Dict{String, Any}()
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.write2vtk!(vtk, v, u, 0.0, system)
+ end
+ expected_stress = delta_s *
+ (Matrix{Float64}(I, 3, 3) - normal * transpose(normal))
+ @test vtk["surf_normal"][candidate] ≈ TrixiParticles.surface_normal(system, candidate)
+ @test vtk["surface_tension_normal"][candidate] ≈ normal
+ @test vtk["surface_stress_tensor"][:, :, candidate] ≈ expected_stress
+
+ system.cache.surface_normal .= reshape([1.0, 0.0, 0.0], 3, 1)
+ system.cache.interface_activity .= 1
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.smooth_surface_normals!(system, normal_method, v, u, semi)
+ end
+ @test all(particle -> TrixiParticles.surface_tension_normal(system, particle) ==
+ SVector(1.0, 0.0, 0.0), eachparticle(system))
+
+ system.cache.surface_normal .= 0
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.smooth_surface_normals!(system, normal_method, v, u, semi)
+ end
+ @test all(iszero, system.cache.smoothed_surface_normal)
+ @test all(isfinite, system.cache.smoothed_surface_normal)
+end
+
+@testset "CSS flat-pool geometry" begin
+ particle_spacing = 0.1
+ reference_density = 1000.0
+ smoothing_kernel = WendlandC2Kernel{2}()
+ smoothing_length = 1.4particle_spacing
+ state_equation = StateEquationCole(; sound_speed=10.0, reference_density,
+ exponent=1)
+ fluid = RectangularShape(particle_spacing, (9, 6), (0.0, 0.0);
+ density=reference_density)
+ normal_method = ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1,
+ interface_threshold=0.01,
+ ideal_density_threshold=0.9)
+ surface_tension = SurfaceTensionMomentumMorris(; surface_tension_coefficient=0.072)
+ fluid_system = WeaklyCompressibleSPHSystem(fluid; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation, surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=particle_spacing)
+
+ # The top wall row continues the fluid lattice one spacing below the bottom fluid row.
+ wall = RectangularShape(particle_spacing, (9, 3), (0.0, -0.3);
+ density=reference_density)
+ boundary_model = BoundaryModelDummyParticles(wall; fluid_system,
+ boundary_density_calculator=AdamiPressureExtrapolation())
+ boundary_system = WallBoundarySystem(wall, boundary_model)
+ semi = Semidiscretization(fluid_system, boundary_system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ TrixiParticles.update_systems_and_nhs(v_ode, u_ode, semi, 0.0)
+
+ coordinates = fluid.coordinates
+ particle_at(position) = findfirst(particle -> coordinates[:, particle] == position,
+ axes(coordinates, 2))
+ bottom_center = particle_at([0.45, 0.05])
+ interior_center = particle_at([0.45, 0.25])
+ top_center = particle_at([0.45, 0.55])
+ centerline_particles = [bottom_center, interior_center, top_center]
+
+ acceleration = GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, fluid_system, semi)
+ u = TrixiParticles.wrap_u(u_ode, fluid_system, semi)
+ v_boundary = TrixiParticles.wrap_v(v_ode, boundary_system, semi)
+ u_boundary = TrixiParticles.wrap_u(u_ode, boundary_system, semi)
+ dv = zeros(eltype(v), size(v))
+ TrixiParticles.interact!(dv, v, u, v, u, fluid_system, fluid_system, semi)
+ TrixiParticles.interact!(dv, v, u, v_boundary, u_boundary, fluid_system,
+ boundary_system, semi)
+ Array(dv[1:2, :])
+ end
+
+ # Wall particles complete the support moment without carrying capillary stress.
+ @test fluid_system.cache.divergence_correction[bottom_center] >= 0.9
+ @test fluid_system.cache.interface_activity[bottom_center] == 0
+ @test fluid_system.cache.delta_s[bottom_center] == 0
+ @test fluid_system.cache.delta_s[top_center] > 0
+ @test iszero(fluid_system.cache.delta_s[interior_center])
+ @test maximum(abs, acceleration[:, centerline_particles]) < 1.0e-12
+end
+
@testset verbose=true "Rigid Dummy Boundary Matches Wall Boundary" begin
NDIMS = 2
particle_spacing = 0.2
@@ -163,7 +407,8 @@ end
NDIMS, smoothing_length, smoothing_kernel,
surface_normal_method=ColorfieldSurfaceNormal(interface_threshold=0.1,
ideal_density_threshold=0.9),
- wall=true, walldistance=2.0, boundary_system_type=:wall)
+ wall=true, walldistance=particle_spacing,
+ boundary_system_type=:wall)
rigid_system, rigid_boundary, rigid_semi,
rigid_ode = create_fluid_system(coordinates, velocity, mass, density, particle_spacing,
@@ -171,11 +416,19 @@ end
NDIMS, smoothing_length, smoothing_kernel,
surface_normal_method=ColorfieldSurfaceNormal(interface_threshold=0.1,
ideal_density_threshold=0.9),
- wall=true, walldistance=2.0,
+ wall=true, walldistance=particle_spacing,
boundary_system_type=:rigid)
+ free_system, _, free_semi,
+ free_ode = create_fluid_system(coordinates, velocity, mass, density, particle_spacing,
+ SurfaceTensionMorris(surface_tension_coefficient=0.072);
+ NDIMS, smoothing_length, smoothing_kernel,
+ surface_normal_method=ColorfieldSurfaceNormal(interface_threshold=0.1,
+ ideal_density_threshold=0.9))
+
compute_and_test_surface_values(wall_system, wall_semi, wall_ode; NDIMS)
compute_and_test_surface_values(rigid_system, rigid_semi, rigid_ode; NDIMS)
+ compute_and_test_surface_values(free_system, free_semi, free_ode; NDIMS)
@test isapprox(rigid_boundary.boundary_model.cache.initial_colorfield,
wall_boundary.boundary_model.cache.initial_colorfield,
@@ -186,6 +439,14 @@ end
@test isapprox(rigid_system.cache.neighbor_count,
wall_system.cache.neighbor_count,
rtol=sqrt(eps()), atol=sqrt(eps()))
+ @test all(isfinite, wall_system.cache.support_moment)
+ @test maximum(abs, wall_system.cache.support_moment) > 0
+ @test isapprox(rigid_system.cache.support_moment,
+ wall_system.cache.support_moment,
+ rtol=sqrt(eps()), atol=sqrt(eps()))
+ @test maximum(abs,
+ wall_system.cache.support_moment - free_system.cache.support_moment) >
+ sqrt(eps())
end
@testset verbose=true "CSS/CSF: Sphere Surface Normals" begin
diff --git a/test/schemes/fluid/surface_tension.jl b/test/schemes/fluid/surface_tension.jl
index 7fe8abbd97..df45f855ab 100644
--- a/test/schemes/fluid/surface_tension.jl
+++ b/test/schemes/fluid/surface_tension.jl
@@ -1,5 +1,462 @@
@testset verbose=true "Surface Tension" begin
+ function build_wetted_area_setup(; solver=:wcsph, angle=60.0,
+ contact=true, ELTYPE=Float64,
+ smoothing_kernel=WendlandC2Kernel{3}(),
+ smoothing_length_ratio=1.4,
+ density_calculator=ContinuityDensity(),
+ surface_tension_model=:momentum,
+ provide_surface_measure=true,
+ provide_normals=true,
+ surface_measure_mode=:connected,
+ boundary_kind=:wall,
+ prescribed_motion=nothing,
+ rotation=nothing,
+ fluid_color=1, boundary_color=0)
+ particle_spacing = ELTYPE(0.1)
+ smoothing_length = ELTYPE(smoothing_length_ratio) * particle_spacing
+ reference_density = ELTYPE(1000)
+ fluid_raw = RectangularShape(particle_spacing, (4, 4, 3),
+ (zero(ELTYPE), zero(ELTYPE), zero(ELTYPE));
+ density=reference_density)
+ transform = isnothing(rotation) ? Matrix{ELTYPE}(I, 3, 3) : ELTYPE.(rotation)
+ fluid = InitialCondition(; coordinates=transform * fluid_raw.coordinates,
+ velocity=transform * fluid_raw.velocity,
+ mass=fluid_raw.mass, density=fluid_raw.density,
+ pressure=fluid_raw.pressure,
+ particle_spacing)
+ state_equation = StateEquationCole(; sound_speed=ELTYPE(10), reference_density,
+ exponent=1)
+ contact_model = contact ? WettedAreaContactAngle(ELTYPE(angle)) : nothing
+ normal_method = ColorfieldSurfaceNormal(; boundary_contact_threshold=zero(ELTYPE),
+ interface_threshold=ELTYPE(0.01),
+ ideal_density_threshold=ELTYPE(0.95),
+ contact_model)
+ surface_tension = surface_tension_model == :momentum ?
+ SurfaceTensionMomentumMorris(;
+ surface_tension_coefficient=ELTYPE(0.072)) :
+ SurfaceTensionMorris(;
+ surface_tension_coefficient=ELTYPE(0.072))
+ fluid_system = if solver == :wcsph
+ WeaklyCompressibleSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ density_calculator, state_equation, surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=particle_spacing,
+ color_value=fluid_color)
+ else
+ EntropicallyDampedSPHSystem(fluid; smoothing_kernel, smoothing_length,
+ sound_speed=ELTYPE(10), density_calculator,
+ surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=particle_spacing,
+ color_value=fluid_color)
+ end
+
+ boundary_raw = RectangularShape(particle_spacing, (4, 4, 3),
+ (zero(ELTYPE), zero(ELTYPE),
+ -3particle_spacing);
+ density=reference_density)
+ exposed_height = maximum(boundary_raw.coordinates[3, :])
+ exposed = isapprox.(boundary_raw.coordinates[3, :], exposed_height;
+ atol=eps(ELTYPE))
+ normals = zeros(ELTYPE, size(boundary_raw.coordinates))
+ normals[3, exposed] .= -particle_spacing / 2
+ surface_measure = zeros(ELTYPE, nparticles(boundary_raw))
+ if surface_measure_mode == :connected
+ surface_measure[exposed] .= particle_spacing^2
+ elseif surface_measure_mode == :disconnected
+ exposed_particles = findall(exposed)
+ surface_measure[first(exposed_particles)] = particle_spacing^2
+ surface_measure[last(exposed_particles)] = particle_spacing^2
+ end
+ boundary = InitialCondition(;
+ coordinates=transform * boundary_raw.coordinates,
+ velocity=transform * boundary_raw.velocity,
+ mass=boundary_raw.mass, density=boundary_raw.density,
+ pressure=boundary_raw.pressure, particle_spacing,
+ normals=provide_normals ? transform * normals : nothing)
+ boundary_model = if provide_surface_measure
+ BoundaryModelDummyParticles(boundary; fluid_system,
+ surface_measure=surface_measure)
+ else
+ BoundaryModelDummyParticles(boundary; fluid_system)
+ end
+ boundary_system = if boundary_kind == :wall
+ WallBoundarySystem(boundary, boundary_model; prescribed_motion,
+ color_value=boundary_color)
+ else
+ RigidBodySystem(boundary; boundary_model, color_value=boundary_color)
+ end
+ semi = Semidiscretization(fluid_system, boundary_system)
+ ode = semidiscretize(semi, (zero(ELTYPE), ELTYPE(0.01)))
+ return (; fluid_system, boundary_system, semi, ode, surface_measure,
+ particle_spacing)
+ end
+
+ function wetted_area_kick(setup; time=zero(eltype(setup.fluid_system)))
+ v_ode, u_ode = setup.ode.u0.x
+ dv_ode = zero(v_ode)
+ TrixiParticles.kick!(dv_ode, v_ode, u_ode, setup.ode.p, time)
+ fluid_dv = TrixiParticles.wrap_v(dv_ode, setup.fluid_system, setup.semi)
+ return Array(fluid_dv[1:3, :]), dv_ode
+ end
+
+ @testset "wetted-area constructors and configuration" begin
+ normal_method = ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1f0,
+ interface_threshold=0.01f0,
+ ideal_density_threshold=0.95f0,
+ contact_model=WettedAreaContactAngle(60.0f0))
+ @test normal_method.contact_model isa WettedAreaContactAngle{Float32}
+ @test normal_method.contact_model.contact_angle === 60.0f0
+ @test isnothing(ColorfieldSurfaceNormal().contact_model)
+ @test isnothing(ColorfieldSurfaceNormal(0.1, 0.01, 0.0).contact_model)
+
+ system_data = Dict{String, Any}()
+ TrixiParticles.add_system_data!(system_data, normal_method)
+ @test system_data["surface_normal_method"]["contact_model"] ==
+ "WettedAreaContactAngle"
+ @test system_data["surface_normal_method"]["contact_angle"] === 60.0f0
+
+ for angle in (-1, 181, NaN, Inf, 1im, "invalid", 0, 180)
+ @test_throws ArgumentError WettedAreaContactAngle(angle)
+ end
+ @test_throws ArgumentError ColorfieldSurfaceNormal(contact_model=:invalid)
+
+ setup32 = build_wetted_area_setup(; ELTYPE=Float32)
+ fluid32 = setup32.fluid_system
+ boundary_cache32 = setup32.boundary_system.boundary_model.cache
+ @test fluid32.surface_normal_method.contact_model isa
+ WettedAreaContactAngle{Float32}
+ @test eltype(fluid32.cache.wetted_area_density_conjugate) == Float32
+ @test eltype(boundary_cache32.surface_measure) == Float32
+ @test all(>=(0), boundary_cache32.surface_measure)
+ @test all(>(0),
+ boundary_cache32.wetted_area_flooded_reference[setup32.surface_measure .> 0])
+ @test isfinite(fluid32.cache.wetted_area_normalized_edge_shift[])
+
+ boundary = setup32.boundary_system.initial_condition
+ second_model = BoundaryModelDummyParticles(boundary;
+ fluid_system=setup32.fluid_system,
+ surface_measure=setup32.surface_measure)
+ second_boundary = WallBoundarySystem(boundary, second_model)
+ multiple_semi = Semidiscretization(setup32.fluid_system,
+ setup32.boundary_system, second_boundary)
+ multiple_ode = semidiscretize(multiple_semi, (0.0f0, 0.01f0))
+ multiple_dv = zero(multiple_ode.u0.x[1])
+ TrixiParticles.kick!(multiple_dv, multiple_ode.u0.x...,
+ multiple_ode.p, 0.0f0)
+ @test sum(abs, boundary_cache32.wetted_area_weight) > 0
+ @test sum(abs, second_model.cache.wetted_area_weight) > 0
+ @test setup32.fluid_system.cache.wetted_area[] > 0
+
+ @test_throws ArgumentError build_wetted_area_setup(;
+ provide_surface_measure=false)
+ @test_throws ArgumentError build_wetted_area_setup(; provide_normals=false)
+ @test_throws ArgumentError build_wetted_area_setup(;
+ surface_measure_mode=:disconnected)
+ @test_throws ArgumentError build_wetted_area_setup(;
+ surface_measure_mode=:empty)
+ @test_throws ArgumentError build_wetted_area_setup(;
+ smoothing_length_ratio=1.5)
+ @test_throws ArgumentError build_wetted_area_setup(;
+ smoothing_kernel=SchoenbergCubicSplineKernel{3}())
+ @test_throws ArgumentError build_wetted_area_setup(;
+ density_calculator=SummationDensity())
+ @test_throws ArgumentError build_wetted_area_setup(;
+ surface_tension_model=:csf)
+ @test_throws ArgumentError build_wetted_area_setup(; fluid_color=2)
+ @test_throws ArgumentError build_wetted_area_setup(; boundary_color=1)
+
+ no_contact = build_wetted_area_setup(; contact=false,
+ provide_surface_measure=false)
+ @test !haskey(no_contact.boundary_system.boundary_model.cache,
+ :wetted_area_weight)
+ @test !haskey(no_contact.fluid_system.cache, :wetted_area_density_conjugate)
+ end
+
+ @testset "smooth interface activity" begin
+ method = ColorfieldSurfaceNormal(; boundary_contact_threshold=1,
+ interface_threshold=0.1f0,
+ ideal_density_threshold=0.9,
+ interface_taper_start=0.8,
+ support_taper_width=0.05)
+ @test method isa ColorfieldSurfaceNormal{Float64}
+ @test method.interface_taper_start === 0.8
+ @test method.support_taper_width === 0.05
+ @test !method.normal_smoothing
+ @test ColorfieldSurfaceNormal(1, 1, 0) isa ColorfieldSurfaceNormal{Float64}
+ @test ColorfieldSurfaceNormal(; boundary_contact_threshold=0.1f0,
+ interface_threshold=0.01f0,
+ ideal_density_threshold=0.0f0,
+ interface_taper_start=0.8f0,
+ support_taper_width=0.025f0) isa
+ ColorfieldSurfaceNormal{Float32}
+
+ for ELTYPE in (Float32, Float64)
+ @test TrixiParticles.cubic_smoothstep(ELTYPE(-1)) === ELTYPE(0)
+ @test TrixiParticles.cubic_smoothstep(ELTYPE(0)) === ELTYPE(0)
+ @test TrixiParticles.cubic_smoothstep(ELTYPE(0.5)) === ELTYPE(0.5)
+ @test TrixiParticles.cubic_smoothstep(ELTYPE(1)) === ELTYPE(1)
+ @test TrixiParticles.cubic_smoothstep(ELTYPE(2)) === ELTYPE(1)
+
+ method_ = ColorfieldSurfaceNormal(; boundary_contact_threshold=ELTYPE(0.1),
+ interface_threshold=ELTYPE(0.1),
+ ideal_density_threshold=ELTYPE(0.9),
+ interface_taper_start=ELTYPE(0.8),
+ support_taper_width=ELTYPE(0.05))
+ @test TrixiParticles.gradient_interface_activity(ELTYPE(0.08), one(ELTYPE),
+ method_) === ELTYPE(0)
+ @test TrixiParticles.gradient_interface_activity(ELTYPE(0.09), one(ELTYPE),
+ method_) ≈ ELTYPE(0.5)
+ @test TrixiParticles.gradient_interface_activity(ELTYPE(0.1), one(ELTYPE),
+ method_) === ELTYPE(1)
+ @test TrixiParticles.support_interface_activity(ELTYPE(0.9), method_) ===
+ ELTYPE(1)
+ @test TrixiParticles.support_interface_activity(ELTYPE(0.925), method_) ≈
+ ELTYPE(0.5)
+ @test TrixiParticles.support_interface_activity(ELTYPE(0.95), method_) ===
+ ELTYPE(0)
+
+ step = sqrt(eps(ELTYPE))
+ derivative_at_zero = TrixiParticles.cubic_smoothstep(step) / step
+ derivative_at_one = (one(ELTYPE) -
+ TrixiParticles.cubic_smoothstep(one(ELTYPE) - step)) / step
+ @test abs(derivative_at_zero) < 4step
+ @test abs(derivative_at_one) < 4step
+ end
+
+ disabled = ColorfieldSurfaceNormal(; ideal_density_threshold=0.0)
+ @test TrixiParticles.support_interface_activity(10.0, disabled) == 1.0
+ @test TrixiParticles.normalized_surface_curvature(1.0, 0.0) == 0.0
+ @test TrixiParticles.normalized_surface_curvature(1.0, eps()) == 0.0
+ @test TrixiParticles.normalized_surface_curvature(2.0, 0.5) == 4.0
+
+ for threshold in (-1, NaN, Inf)
+ @test_throws ArgumentError ColorfieldSurfaceNormal(interface_threshold=threshold)
+ @test_throws ArgumentError ColorfieldSurfaceNormal(ideal_density_threshold=threshold)
+ end
+ for taper_start in (-0.1, 1.0, NaN, Inf)
+ @test_throws ArgumentError ColorfieldSurfaceNormal(;
+ interface_taper_start=taper_start)
+ end
+ for taper_width in (0.0, -0.1, NaN, Inf)
+ @test_throws ArgumentError ColorfieldSurfaceNormal(;
+ support_taper_width=taper_width)
+ end
+ for normal_smoothing in (0, 1, nothing)
+ @test_throws ArgumentError ColorfieldSurfaceNormal(; normal_smoothing)
+ end
+ @test ColorfieldSurfaceNormal(; normal_smoothing=true).normal_smoothing
+
+ system_data = Dict{String, Any}()
+ TrixiParticles.add_system_data!(system_data, method)
+ @test system_data["surface_normal_method"]["interface_threshold"] ≈ 0.1
+ @test system_data["surface_normal_method"]["interface_taper_start"] === 0.8
+ @test system_data["surface_normal_method"]["support_taper_width"] === 0.05
+ @test system_data["surface_normal_method"]["normal_smoothing"] === false
+ end
+
+ @testset "wetted-area energy and production RHS" begin
+ active = build_wetted_area_setup(; angle=60.0)
+ neutral = build_wetted_area_setup(; angle=90.0)
+ no_contact = build_wetted_area_setup(; contact=false)
+ active_acceleration, = wetted_area_kick(active)
+ neutral_acceleration, = wetted_area_kick(neutral)
+ no_contact_acceleration, = wetted_area_kick(no_contact)
+ contact_acceleration = active_acceleration - neutral_acceleration
+
+ @test neutral_acceleration == no_contact_acceleration
+ @test neutral.fluid_system.cache.wetted_area_energy[] == 0
+ @test all(iszero, neutral.fluid_system.cache.wetted_area_density_conjugate)
+ @test all(iszero,
+ neutral.boundary_system.boundary_model.cache.wetted_area_weight)
+ @test all(iszero,
+ neutral.boundary_system.boundary_model.cache.wetted_area_reaction)
+ @test active.fluid_system.cache.wetted_area_energy[] < 0
+ @test norm(contact_acceleration) > 0
+
+ fluid_force = contact_acceleration * active.fluid_system.mass
+ wall_reaction_cache = active.boundary_system.boundary_model.cache.wetted_area_reaction
+ wall_reaction = vec(sum(wall_reaction_cache; dims=2))
+ force_scale = sum(particle -> norm(active.fluid_system.mass[particle] *
+ contact_acceleration[:, particle]),
+ eachparticle(active.fluid_system)) +
+ sum(particle -> norm(view(wall_reaction_cache, :, particle)),
+ eachparticle(active.boundary_system))
+ @test norm(fluid_force + wall_reaction) / force_scale < 1.0e-12
+
+ active_edac = build_wetted_area_setup(; solver=:edac, angle=60.0)
+ neutral_edac = build_wetted_area_setup(; solver=:edac, angle=90.0)
+ active_edac_acceleration, = wetted_area_kick(active_edac)
+ neutral_edac_acceleration, = wetted_area_kick(neutral_edac)
+ edac_contact_acceleration = active_edac_acceleration -
+ neutral_edac_acceleration
+ edac_force = edac_contact_acceleration * active_edac.fluid_system.mass
+ edac_reaction = vec(sum(active_edac.boundary_system.boundary_model.cache.wetted_area_reaction;
+ dims=2))
+ @test norm(edac_contact_acceleration) > 0
+ @test norm(edac_force + edac_reaction) <
+ 1.0e-12 * (norm(edac_force) + norm(edac_reaction))
+
+ active_rigid = build_wetted_area_setup(; boundary_kind=:rigid, angle=60.0)
+ neutral_rigid = build_wetted_area_setup(; boundary_kind=:rigid, angle=90.0)
+ wetted_area_kick(active_rigid)
+ wetted_area_kick(neutral_rigid)
+ rigid_reaction = active_rigid.boundary_system.boundary_model.cache.wetted_area_reaction
+ rigid_contact_force = active_rigid.boundary_system.force_per_particle -
+ neutral_rigid.boundary_system.force_per_particle
+ @test rigid_contact_force ≈ rigid_reaction rtol = 2eps()
+ @test active_rigid.boundary_system.resultant_force[] -
+ neutral_rigid.boundary_system.resultant_force[] ≈
+ vec(sum(rigid_reaction; dims=2)) rtol = 2eps()
+ expected_torque = zero(active_rigid.boundary_system.resultant_torque[])
+ for particle in eachparticle(active_rigid.boundary_system)
+ relative_position = TrixiParticles.extract_svector(active_rigid.boundary_system.relative_coordinates,
+ active_rigid.boundary_system,
+ particle)
+ reaction = TrixiParticles.extract_svector(rigid_reaction,
+ active_rigid.boundary_system,
+ particle)
+ expected_torque += cross(relative_position, reaction)
+ end
+ @test active_rigid.boundary_system.resultant_torque[] -
+ neutral_rigid.boundary_system.resultant_torque[] ≈ expected_torque atol = 1.0e-12
+
+ rotation = [0.0 0.0 1.0; 0.0 1.0 0.0; -1.0 0.0 0.0]
+ rotated_active = build_wetted_area_setup(; angle=60.0, rotation)
+ rotated_neutral = build_wetted_area_setup(; angle=90.0, rotation)
+ rotated_active_acceleration, = wetted_area_kick(rotated_active)
+ rotated_neutral_acceleration, = wetted_area_kick(rotated_neutral)
+ rotated_contact_acceleration = rotated_active_acceleration -
+ rotated_neutral_acceleration
+ @test rotated_contact_acceleration≈rotation*contact_acceleration rtol=2.0e-12 atol=2.0e-12
+
+ moving_motion() = PrescribedMotion((position,
+ time) -> begin
+ cosine = cos(time)
+ sine = sin(time)
+ SVector(cosine * position[1] +
+ sine * position[3], position[2],
+ -sine * position[1] +
+ cosine * position[3])
+ end,
+ time -> true)
+ moving_active = build_wetted_area_setup(; angle=60.0,
+ prescribed_motion=moving_motion())
+ moving_neutral = build_wetted_area_setup(; angle=90.0,
+ prescribed_motion=moving_motion())
+ moving_active_acceleration, = wetted_area_kick(moving_active; time=0.02)
+ moving_neutral_acceleration, = wetted_area_kick(moving_neutral; time=0.02)
+ moving_contact_acceleration = moving_active_acceleration -
+ moving_neutral_acceleration
+ moving_force = moving_contact_acceleration * moving_active.fluid_system.mass
+ moving_reaction = vec(sum(moving_active.boundary_system.boundary_model.cache.wetted_area_reaction;
+ dims=2))
+ @test norm(moving_contact_acceleration) > 0
+ @test norm(moving_force + moving_reaction) <
+ 1.0e-12 * (norm(moving_force) + norm(moving_reaction))
+ end
+
+ @testset "wetted-area variational derivative" begin
+ active = build_wetted_area_setup(; angle=60.0)
+ neutral = build_wetted_area_setup(; angle=90.0)
+ active_acceleration, = wetted_area_kick(active)
+ neutral_acceleration, = wetted_area_kick(neutral)
+ contact_acceleration = active_acceleration - neutral_acceleration
+
+ v_ode, u_ode = active.ode.u0.x
+ v = TrixiParticles.wrap_v(v_ode, active.fluid_system, active.semi)
+ u = TrixiParticles.wrap_u(u_ode, active.fluid_system, active.semi)
+ u_boundary = TrixiParticles.wrap_u(u_ode, active.boundary_system, active.semi)
+ coordinates = Array(TrixiParticles.current_coordinates(u, active.fluid_system))
+ boundary_coordinates = Array(TrixiParticles.current_coordinates(u_boundary,
+ active.boundary_system))
+ density = collect(TrixiParticles.current_density(v, active.fluid_system))
+ displacement = similar(coordinates)
+ displacement_scale = max(maximum(abs, coordinates), active.particle_spacing)
+ for particle in eachparticle(active.fluid_system)
+ displacement[1, particle] = -coordinates[1, particle] / displacement_scale
+ displacement[2, particle] = -coordinates[2, particle] / displacement_scale
+ displacement[3, particle] = 2coordinates[3, particle] / displacement_scale
+ end
+ density_rate = zeros(eltype(active.fluid_system), nparticles(active.fluid_system))
+ TrixiParticles.foreach_point_neighbor(active.fluid_system, active.fluid_system,
+ coordinates, coordinates, active.semi;
+ points=eachparticle(active.fluid_system),
+ parallelization_backend=SerialBackend()) do particle,
+ neighbor,
+ pos_diff,
+ distance
+ gradient = TrixiParticles.smoothing_kernel_grad(active.fluid_system,
+ pos_diff, distance, particle)
+ mass_b = TrixiParticles.hydrodynamic_mass(active.fluid_system, neighbor)
+ density_rate[particle] += density[particle] / density[neighbor] * mass_b *
+ dot(displacement[:, particle] -
+ displacement[:, neighbor], gradient)
+ end
+ fluid_boundary_pairs = Tuple{Int, Int}[]
+ TrixiParticles.foreach_point_neighbor(active.fluid_system,
+ active.boundary_system,
+ coordinates, boundary_coordinates,
+ active.semi;
+ points=eachparticle(active.fluid_system),
+ parallelization_backend=SerialBackend()) do particle,
+ neighbor,
+ pos_diff,
+ distance
+ push!(fluid_boundary_pairs, (particle, neighbor))
+ end
+
+ function perturbed_wetted_area_energy(epsilon)
+ boundary_cache = active.boundary_system.boundary_model.cache
+ colorfield = copy(boundary_cache.initial_colorfield)
+ for (particle, neighbor) in fluid_boundary_pairs
+ distance2 = zero(eltype(active.fluid_system))
+ for dim in 1:3
+ difference = coordinates[dim, particle] +
+ epsilon * displacement[dim, particle] -
+ boundary_coordinates[dim, neighbor]
+ distance2 += difference^2
+ end
+ perturbed_density = density[particle] + epsilon * density_rate[particle]
+ colorfield[neighbor] += active.fluid_system.mass[particle] /
+ perturbed_density *
+ TrixiParticles.smoothing_kernel(active.fluid_system,
+ sqrt(distance2),
+ particle)
+ end
+ raw_area = zero(eltype(active.fluid_system))
+ for particle in eachparticle(active.boundary_system)
+ measure = boundary_cache.surface_measure[particle]
+ iszero(measure) && continue
+ reference = boundary_cache.wetted_area_flooded_reference[particle]
+ fraction = clamp(colorfield[particle] / reference, 0, 1)
+ raw_area += measure * TrixiParticles.cubic_smoothstep(fraction)
+ end
+ raw_radius = sqrt(raw_area / pi)
+ edge_shift = active.fluid_system.cache.wetted_area_normalized_edge_shift[] *
+ TrixiParticles.initial_smoothing_length(active.fluid_system)
+ corrected_radius = max(raw_radius - edge_shift, zero(raw_radius))
+ coefficient = TrixiParticles.wetted_area_coefficient(active.fluid_system.surface_tension,
+ active.fluid_system.surface_normal_method.contact_model)
+ return -coefficient * pi * corrected_radius^2
+ end
+
+ epsilon = 1.0e-5active.particle_spacing
+ finite_difference = (perturbed_wetted_area_energy(epsilon) -
+ perturbed_wetted_area_energy(-epsilon)) / (2epsilon)
+ analytic_derivative = zero(finite_difference)
+ for particle in eachparticle(active.fluid_system)
+ analytic_derivative -= active.fluid_system.mass[particle] *
+ dot(contact_acceleration[:, particle],
+ displacement[:, particle])
+ end
+ derivative_scale = max(abs(finite_difference), abs(analytic_derivative))
+ @test abs(finite_difference - analytic_derivative) / derivative_scale < 1.0e-5
+ @test perturbed_wetted_area_energy(0.0) ≈
+ active.fluid_system.cache.wetted_area_energy[] rtol = 5eps()
+ end
+
@testset verbose=true "`cohesion_force_akinci`" begin
surface_tension = SurfaceTensionAkinci(surface_tension_coefficient=1.0)
support_radius = 1.0
@@ -90,96 +547,359 @@
@test isapprox(zero[2], 0.0, atol=6e-15)
end
- @testset "compute_stress_tensors! (MomentumMorris)" begin
- # 1. Define Minimal Initial Condition with 2 Particles in 2D
- coords = [0.0 1.0;
- 0.0 0.0]
- velocity = zeros(2, 2)
- mass = ones(2)
- density = ones(2)
-
- ic = InitialCondition(; coordinates=coords, velocity, mass, density,
- particle_spacing=1.0)
-
- # 2. Define Density Calculator, State Equation, and Kernel
- density_calc = SummationDensity()
- eq_state = StateEquationCole(sound_speed=10.0,
- reference_density=1.0,
- exponent=1)
- kernel = WendlandC2Kernel{2}()
- smoothing_length = 0.5
-
- # 3. Create the WeaklyCompressibleSPHSystem with Surface Tension
- system = WeaklyCompressibleSPHSystem(ic; smoothing_kernel=kernel,
- smoothing_length,
- density_calculator=density_calc,
- state_equation=eq_state,
- surface_tension=SurfaceTensionMomentumMorris(surface_tension_coefficient=1.0),
- surface_normal_method=ColorfieldSurfaceNormal(interface_threshold=0.1,
- ideal_density_threshold=0.9),
- reference_particle_spacing=1.0,)
-
- # 4. Verify Cache Contains Necessary Fields
+ @testset "Morris CSF local force" begin
+ function build_morris_system(solver, particle_count; normal_smoothing=false)
+ coordinates = zeros(2, particle_count)
+ coordinates[1, :] .= range(0.0; step=0.25, length=particle_count)
+ initial_condition = InitialCondition(; coordinates,
+ velocity=zeros(2, particle_count),
+ mass=ones(particle_count),
+ density=ones(particle_count),
+ particle_spacing=0.25)
+ smoothing_kernel = WendlandC2Kernel{2}()
+ surface_tension = SurfaceTensionMorris(; surface_tension_coefficient=0.7)
+ normal_method = ColorfieldSurfaceNormal(; interface_threshold=0.1,
+ normal_smoothing)
+ if solver == :wcsph
+ return WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length=0.5,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density=1.0,
+ exponent=1),
+ surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=0.25)
+ end
+ return EntropicallyDampedSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length=0.5, sound_speed=10.0,
+ density_calculator=ContinuityDensity(),
+ surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=0.25)
+ end
+
+ function morris_rhs_effect(system)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ TrixiParticles.update_systems_and_nhs(v_ode, u_ode, semi, 0.0)
+ system.cache.surface_normal[1, :] .= 1.0
+ system.cache.surface_normal[2, :] .= 0.0
+ system.cache.curvature .= 3.0
+ system.cache.delta_s .= 2.0
+ system.cache.interface_activity .= 1.0
+
+ return GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ rho_a = TrixiParticles.current_density(v, system, 1)
+ expected = TrixiParticles.surface_tension_acceleration(system.surface_tension,
+ system, 1, rho_a,
+ SVector(0.0, 0.0))
+ with_surface_tension = zeros(eltype(v), size(v))
+ TrixiParticles.interact!(with_surface_tension, v, u, v, u,
+ system, system, semi)
+ system.cache.delta_s .= 0
+ without_surface_tension = zeros(eltype(v), size(v))
+ TrixiParticles.interact!(without_surface_tension, v, u, v, u,
+ system, system, semi)
+ return (with_surface_tension - without_surface_tension)[1:2, :],
+ expected
+ end
+ end
+
+ effects = []
+ for solver in (:wcsph, :edac), particle_count in (2, 4)
+ effect,
+ expected = morris_rhs_effect(build_morris_system(solver, particle_count))
+ @test all(particle -> effect[:, particle] ≈ expected, axes(effect, 2))
+ push!(effects, effect[:, 1])
+ end
+ @test all(effect -> effect ≈ first(effects), effects)
+
+ system = build_morris_system(:wcsph, 2)
+ system.cache.surface_normal .= [2.0 1.0; 0.0 1.0]
+ system.cache.support_moment .= 0
+ TrixiParticles.remove_invalid_normals!(system, system.surface_tension,
+ system.surface_normal_method)
+ @test system.cache.delta_s ≈ [4.0, 2sqrt(2)]
+ @test system.cache.interface_activity == [1.0, 1.0]
+ @test system.cache.surface_normal[:, 1] ≈ [1.0, 0.0]
+ @test system.cache.surface_normal[:, 2] ≈ [1 / sqrt(2), 1 / sqrt(2)]
+ system.cache.surface_normal[:, 1] .= [NaN, 0.0]
+ TrixiParticles.remove_invalid_normals!(system, system.surface_tension,
+ system.surface_normal_method)
+ @test iszero(system.cache.surface_normal[:, 1])
+ @test iszero(system.cache.delta_s[1])
+ @test iszero(system.cache.interface_activity[1])
+
+ system.cache.surface_normal[1, :] .= 1.0
+ system.cache.surface_normal[2, :] .= 0.0
+ system.cache.curvature .= 3.0
+ system.cache.delta_s .= 2.0
+ acceleration = TrixiParticles.surface_tension_acceleration(system.surface_tension,
+ system, 1, 1.0,
+ SVector(0.0, 0.0))
+ @test acceleration ≈ SVector(-4.2, 0.0)
+
+ smoothed_system = build_morris_system(:wcsph, 2; normal_smoothing=true)
+ smoothed_system.cache.surface_normal .= [1.0 1.0; 0.0 0.0]
+ smoothed_system.cache.smoothed_surface_normal .= [0.0 0.0; 1.0 1.0]
+ smoothed_system.cache.curvature .= 3.0
+ smoothed_system.cache.delta_s .= 2.0
+ smoothed_acceleration = TrixiParticles.surface_tension_acceleration(smoothed_system.surface_tension,
+ smoothed_system,
+ 1, 1.0,
+ SVector(0.0,
+ 0.0))
+ @test smoothed_acceleration ≈ SVector(0.0, -4.2)
+ @test TrixiParticles.surface_normal(smoothed_system, 1) == SVector(1.0, 0.0)
+ system.cache.curvature[1] /= 2
+ system.cache.delta_s[1] /= 2
+ scaled_acceleration = TrixiParticles.surface_tension_acceleration(system.surface_tension,
+ system, 1, 1.0,
+ SVector(0.0,
+ 0.0))
+ @test scaled_acceleration ≈ acceleration / 4
+
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ TrixiParticles.update_systems_and_nhs(v_ode, u_ode, semi, 0.0)
+ system.cache.surface_normal[1, :] .= 1.0
+ system.cache.surface_normal[2, :] .= 0.0
+ system.cache.curvature .= 3.0
+ system.cache.delta_s .= 2.0
+ system.cache.interface_activity .= 1.0
+ vtk = Dict{String, Any}()
+ expected_vtk_acceleration = GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ rho_a = TrixiParticles.current_density(v, system, 1)
+ velocity = TrixiParticles.current_velocity(v, system, 1)
+ expected = TrixiParticles.surface_tension_acceleration(system.surface_tension,
+ system, 1, rho_a,
+ velocity)
+ TrixiParticles.write2vtk!(vtk, v, u, 0.0, system)
+ expected
+ end
+ @test vtk["surface_tension"][:, 1] ≈ expected_vtk_acceleration
+ @test vtk["surface_delta"] == system.cache.delta_s
+ @test vtk["interface_activity"] == system.cache.interface_activity
+ @test vtk["surface_support_moment"] == system.cache.support_moment
+ @test vtk["surface_tension_normal"][1] == SVector(1.0, 0.0)
+ system.cache.surface_normal .= [1.0 0.0; 0.0 1.0]
+
+ function curvature_with_neighbor_activity(activity)
+ system.cache.interface_activity .= [1.0, activity]
+ fill!(system.cache.curvature, 0)
+ fill!(system.cache.correction_factor, 0)
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.calc_curvature!(system, system, u, v, v, u, semi,
+ system.surface_normal_method,
+ system.surface_normal_method)
+ end
+ denominator = system.cache.correction_factor[1]
+ return TrixiParticles.normalized_surface_curvature(system.cache.curvature[1],
+ denominator)
+ end
+
+ curvature_zero = curvature_with_neighbor_activity(0.0)
+ curvature_small = curvature_with_neighbor_activity(1.0e-6)
+ curvature_full = curvature_with_neighbor_activity(1.0)
+ @test iszero(curvature_zero)
+ @test abs(curvature_small) < 1.0e-4 * abs(curvature_full)
+ @test isfinite(curvature_full)
+
+ curvature_numerator = copy(system.cache.curvature)
+ correction_factor = copy(system.cache.correction_factor)
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.calc_curvature!(system, system, u, v, v, u, semi,
+ system.surface_normal_method,
+ system.surface_normal_method)
+ end
+ @test system.cache.curvature ≈ 2curvature_numerator
+ @test system.cache.correction_factor ≈ 2correction_factor
+ end
+
+ @testset "balanced continuum surface stress" begin
+ initial_condition = InitialCondition(; coordinates=[0.0 0.75; 0.0 0.0],
+ velocity=zeros(2, 2), mass=[2.0, 3.0],
+ density=ones(2), particle_spacing=0.5)
+ surface_tension = SurfaceTensionMomentumMorris(;
+ surface_tension_coefficient=0.7)
+ normal_method = ColorfieldSurfaceNormal(; interface_threshold=0.1)
+ system = WeaklyCompressibleSPHSystem(initial_condition;
+ smoothing_kernel=WendlandC2Kernel{2}(),
+ smoothing_length=0.5,
+ density_calculator=SummationDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=10.0,
+ reference_density=1.0,
+ exponent=1),
+ surface_tension,
+ surface_normal_method=normal_method,
+ reference_particle_spacing=0.5)
+
@test haskey(system.cache, :delta_s)
+ @test haskey(system.cache, :interface_activity)
+ @test haskey(system.cache, :divergence_correction)
@test haskey(system.cache, :surface_normal)
- @test haskey(system.cache, :stress_tensor)
-
- # 5. Manually Populate `delta_s` and `surface_normal`
- system.cache.delta_s .= [1.0, 2.0]
- system.cache.surface_normal .= hcat([1.0, 0.0], [1 / sqrt(2), 1 / sqrt(2)])
- system.cache.stress_tensor .= zeros(2, 2, 2) # Reset to zero before computation
-
- # 6. Call `compute_stress_tensors!` with `SurfaceTensionMomentumMorris`
- TrixiParticles.compute_stress_tensors!(system,
- SurfaceTensionMomentumMorris(),
- nothing, nothing, # v, u (not needed for stress computation)
- nothing, nothing, # v_ode, u_ode (not needed)
- SerialBackend(), # semi (only passed to `@threaded`)
- 0.0)
-
- # 7. Define Reference Stress Tensors by Hand
- #
- # Reference calculations based on the formula:
- # σ_ij(a) = δs_a (δ_ij - n_i n_j) - δ_ij max(δs)
- #
- # For Particle 1:
- # δs = 1.0
- # n = (1.0, 0.0)
- # max(δs) = 2.0
- # σ_11 = 1*(1 - 1^2) - 1*2 = -2
- # σ_12 = 1*(0 - 1*0) - 0*2 = 0
- # σ_21 = 1*(0 - 1*0) - 0*2 = 0
- # σ_22 = 1*(1 - 0^2) - 1*2 = 1 - 2 = -1
- #
- # Resulting Stress Tensor for Particle 1:
- # [-2.0 0.0
- # 0.0 -1.0]
- #
- # For Particle 2:
- # δs = 2.0
- # n = (1/√2, 1/√2)
- # max(δs) = 2.0
- # σ_11 = 2*(1 - (1/√2)^2) - 1*2 = 2*(1 - 0.5) - 2 = 1 - 2 = -1
- # σ_12 = 2*(0 - (1/√2)^2) - 0*2 = 2*(0 - 0.5) = -1
- # σ_21 = 2*(0 - (1/√2)^2) - 0*2 = -1
- # σ_22 = 2*(1 - (1/√2)^2) - 1*2 = 2*(1 - 0.5) - 2 = 1 - 2 = -1
- #
- # Resulting Stress Tensor for Particle 2:
- # [-1.0 -1.0
- # -1.0 -1.0]
-
- ref_particle_1 = [-2.0 0.0;
- 0.0 -1.0]
- ref_particle_2 = [-1.0 -1.0;
- -1.0 -1.0]
-
- # 8. Retrieve Computed Stress Tensor
- computed = system.cache.stress_tensor
-
- # 9. Perform Assertions
- @test all(isfinite, computed)
-
- @test isapprox(computed[:, :, 1], ref_particle_1; atol=1e-14)
- @test isapprox(computed[:, :, 2], ref_particle_2; atol=1e-14)
+ @test !haskey(system.cache, :stress_tensor)
+
+ # Capture the one-phase surface delta before normalizing the color gradient.
+ system.cache.surface_normal .= [2.0 1.0; 0.0 1.0]
+ system.cache.divergence_correction .= 0
+ TrixiParticles.remove_invalid_normals!(system, surface_tension, normal_method)
+ @test system.cache.delta_s ≈ [4.0, 2sqrt(2)]
+ @test system.cache.interface_activity == [1.0, 1.0]
+ @test system.cache.surface_normal[:, 1] ≈ [1.0, 0.0]
+ @test system.cache.surface_normal[:, 2] ≈ [1 / sqrt(2), 1 / sqrt(2)]
+
+ grad_kernel = SVector(0.3, -0.4)
+ stress_gradient_1 = 4.0 .* (grad_kernel - SVector(1.0, 0.0) * 0.3)
+ normal_2 = SVector(1 / sqrt(2), 1 / sqrt(2))
+ stress_gradient_2 = 2sqrt(2) .* (grad_kernel -
+ normal_2 * dot(normal_2, grad_kernel))
+ @test TrixiParticles.surface_stress_times_gradient(system, 1, grad_kernel) ≈
+ stress_gradient_1
+ @test TrixiParticles.surface_stress_times_gradient(system, 2, grad_kernel) ≈
+ stress_gradient_2
+
+ rho_a = 2.0
+ rho_b = 3.0
+ system.cache.divergence_correction .= [0.5, 1.0]
+ divergence_correction = 2 / (0.5 + 1.0)
+ pos_diff = SVector(-0.75, 0.0)
+ distance = norm(pos_diff)
+ dv_a = Ref(zero(pos_diff))
+ TrixiParticles.surface_tension_force!(dv_a, surface_tension, surface_tension,
+ system, system, 1, 2, pos_diff, distance,
+ rho_a, rho_b, grad_kernel, 4.0)
+ expected = 3divergence_correction * surface_tension.surface_tension_coefficient /
+ (rho_a * rho_b) * (stress_gradient_1 + stress_gradient_2)
+ @test dv_a[] ≈ expected
+
+ # The symmetric stress divergence conserves pairwise momentum and deliberately
+ # ignores the Akinci-specific correction factor passed above.
+ dv_b = Ref(zero(pos_diff))
+ TrixiParticles.surface_tension_force!(dv_b, surface_tension, surface_tension,
+ system, system, 2, 1, -pos_diff, distance,
+ rho_b, rho_a, -grad_kernel, 4.0)
+ @test 2dv_a[] ≈ -3dv_b[]
+
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ vtk = Dict{String, Any}()
+ GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ TrixiParticles.write2vtk!(vtk, v, u, 0.0, system)
+ end
+ @test vtk["surface_delta"] == system.cache.delta_s
+ @test vtk["interface_activity"] == system.cache.interface_activity
+ @test vtk["surface_tension_normal"] == [TrixiParticles.surface_normal(system, 1),
+ TrixiParticles.surface_normal(system, 2)]
+ @test vtk["surface_divergence_correction"] == [0.5, 1.0]
+ @test size(vtk["surface_stress_tensor"]) == (2, 2, 2)
+ @test vtk["surface_stress_tensor"][:, :, 1] ≈ [0.0 0.0; 0.0 4.0]
+ @test all(isfinite, vtk["surface_stress_tensor"])
+
+ system.cache.divergence_correction .= 0
+ unsupported_force = Ref(zero(pos_diff))
+ TrixiParticles.surface_tension_force!(unsupported_force, surface_tension,
+ surface_tension, system, system, 1, 2,
+ pos_diff, distance, rho_a, rho_b,
+ grad_kernel, 1.0)
+ @test iszero(unsupported_force[])
+
+ filtered_method = ColorfieldSurfaceNormal(; interface_threshold=0.1,
+ ideal_density_threshold=0.9,
+ support_taper_width=0.05)
+ system.cache.surface_normal .= 0
+ system.cache.surface_normal[1, 1] = 0.2
+ system.cache.divergence_correction .= [0.925, 1.0]
+ TrixiParticles.remove_invalid_normals!(system, surface_tension, filtered_method)
+ @test system.cache.interface_activity[1] ≈ 0.5
+ @test system.cache.delta_s[1] ≈ 0.2
+ @test system.cache.surface_normal[:, 1] == [1.0, 0.0]
+ end
+
+ @testset "CSS static Laplace balance" begin
+ reference_density = 1000.0
+ target_particles = 375
+ drop_volume = 1.0e-6
+ particle_spacing = cbrt(drop_volume / target_particles)
+ radius = cbrt(3drop_volume / (4pi))
+ initial_condition = SphereShape(particle_spacing, radius + particle_spacing / 2,
+ (0.0, 0.0, 0.0), reference_density;
+ sphere_type=VoxelSphere())
+ smoothing_kernel = WendlandC2Kernel{3}()
+ smoothing_length = 1.4particle_spacing
+
+ function initial_acceleration(system)
+ semi = Semidiscretization(system)
+ ode = semidiscretize(semi, (0.0, 0.01))
+ v_ode, u_ode = ode.u0.x
+ TrixiParticles.update_systems_and_nhs(v_ode, u_ode, semi, 0.0)
+ return GC.@preserve v_ode u_ode begin
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+ dv = zeros(eltype(v), size(v))
+ TrixiParticles.interact!(dv, v, u, v, u, system, system, semi)
+ Array(dv[1:3, :])
+ end
+ end
+
+ coefficient = 1.0
+ css = SurfaceTensionMomentumMorris(; surface_tension_coefficient=coefficient)
+ css_system = WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed=100.0,
+ reference_density,
+ exponent=1),
+ surface_tension=css,
+ surface_normal_method=ColorfieldSurfaceNormal(;
+ boundary_contact_threshold=Inf,
+ interface_threshold=0.01,
+ ideal_density_threshold=0.95),
+ reference_particle_spacing=particle_spacing)
+ css_acceleration = initial_acceleration(css_system)
+
+ pressure_basis = 1.0
+ sound_speed = 100.0
+ pressure_reference_density = reference_density - pressure_basis / sound_speed^2
+ pressure_system = WeaklyCompressibleSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length,
+ density_calculator=ContinuityDensity(),
+ state_equation=StateEquationCole(;
+ sound_speed,
+ reference_density=pressure_reference_density,
+ exponent=1))
+ pressure_acceleration = initial_acceleration(pressure_system) / pressure_basis
+
+ interface = findall(>(0), css_system.cache.delta_s)
+ capillary = vec(css_acceleration[:, interface])
+ unit_pressure = vec(pressure_acceleration[:, interface])
+ pressure_jump = -dot(capillary, unit_pressure) / dot(unit_pressure, unit_pressure)
+ volume = sum(css_system.mass) / reference_density
+ equivalent_radius = cbrt(3volume / (4pi))
+ inferred_surface_tension = pressure_jump * equivalent_radius / 2
+ total_force = vec(sum(css_acceleration .* reshape(css_system.mass, 1, :);
+ dims=2))
+
+ @test inferred_surface_tension ≈ coefficient rtol = 0.05
+ @test norm(total_force) < 1.0e-12
+ @test all(isfinite, css_system.cache.divergence_correction)
+ @test minimum(css_system.cache.divergence_correction) > 0
end
end
diff --git a/test/setups/complex_shape.jl b/test/setups/complex_shape.jl
index 2917661225..ca6b879cb1 100644
--- a/test/setups/complex_shape.jl
+++ b/test/setups/complex_shape.jl
@@ -2,6 +2,44 @@
data_dir = pkgdir(TrixiParticles, "examples", "preprocessing", "data")
validation_dir = pkgdir(TrixiParticles, "test", "preprocessing", "data")
+ @testset verbose=true "Sample Boundary" begin
+ particle_spacing = 0.1
+ positions = [
+ SVector(0.0, 0.0),
+ SVector(0.1, 0.0),
+ SVector(0.2, 0.0),
+ SVector(0.3, 0.0),
+ SVector(0.4, 0.0)
+ ]
+ distances = [0.01, 0.05, 0.1, 0.2, 0.3]
+
+ signed_distance_field = (; positions, distances, particle_spacing,
+ boundary_packing=true, max_signed_distance=0.3)
+
+ boundary = sample_boundary(signed_distance_field; boundary_density=1.0,
+ boundary_thickness=0.2, place_on_shell=false)
+ @test boundary.coordinates ≈ stack(positions[2:4])
+
+ boundary = sample_boundary(signed_distance_field; boundary_density=1.0,
+ boundary_thickness=0.2, place_on_shell=true)
+ @test boundary.coordinates ≈ stack(positions[3:4])
+
+ @test_throws ArgumentError sample_boundary(signed_distance_field;
+ boundary_density=1.0,
+ boundary_thickness=0.04,
+ place_on_shell=false)
+
+ too_thin_sdf = (; positions, distances, particle_spacing,
+ boundary_packing=true, max_signed_distance=0.1)
+ @test_throws ArgumentError sample_boundary(too_thin_sdf; boundary_density=1.0,
+ boundary_thickness=0.2)
+
+ not_boundary_sdf = (; positions, distances, particle_spacing,
+ boundary_packing=false, max_signed_distance=0.3)
+ @test_throws ArgumentError sample_boundary(not_boundary_sdf; boundary_density=1.0,
+ boundary_thickness=0.2)
+ end
+
@testset verbose=true "2D" begin
@testset verbose=true "Shifted Rectangle" begin
algorithms = [
@@ -41,7 +79,7 @@
end
@testset verbose=true "Real World Data" begin
- files = ["hexagon", "circle", "inverted_open_curve"]
+ files = ["hexagon", "circle"]
algorithms = [
WindingNumberHormann(),
WindingNumberJacobson(; hierarchical_winding=false)
@@ -72,7 +110,8 @@
# See https://docs.julialang.org/en/v1/base/base/#var%22name%22
coords = vcat((data.var"Points:0")', (data.var"Points:1")')
- geometry = load_geometry(joinpath(data_dir, files[j] * ".asc"))
+ geometry = load_geometry(joinpath(data_dir, files[j] * ".asc");
+ close_curve=true)
shape_sampled = ComplexShape(geometry; particle_spacing=0.05,
density=1.0, point_in_geometry_algorithm)
@@ -82,6 +121,15 @@
end
end
+ @testset verbose=true "Open Geometry Validation" begin
+ open_square = [0.0 1.0 1.0 0.0;
+ 0.0 0.0 1.0 1.0]
+ geometry = TrixiParticles.Polygon(open_square; close_curve=false)
+
+ @test_throws ArgumentError ComplexShape(geometry; particle_spacing=0.1,
+ density=1.0)
+ end
+
@testset verbose=true "Intersect of Overlapping Shapes and Geometries" begin
shape = RectangularShape(0.1, (10, 10), (0.0, 0.0), density=1.0)
geometry = load_geometry(joinpath(data_dir, "circle.asc"))
diff --git a/test/setups/extrude_geometry.jl b/test/setups/extrude_geometry.jl
index 4146ab6dd6..b61b14549b 100644
--- a/test/setups/extrude_geometry.jl
+++ b/test/setups/extrude_geometry.jl
@@ -47,6 +47,28 @@
@test shape.coordinates ≈ expected_coords
end
+
+ @testset verbose=true "Errors" begin
+ point1 = [0.0, 0.0]
+ point2 = [0.0, 1.0]
+
+ @test_throws ArgumentError extrude_geometry((point1, point2);
+ direction=[0.0, 0.0],
+ particle_spacing=0.1,
+ n_extrude=1, density=1.0)
+ @test_nowarn extrude_geometry((point1, point2);
+ direction=[1e-20, 0.0],
+ particle_spacing=0.1,
+ n_extrude=1, density=1.0)
+ @test_throws ArgumentError extrude_geometry((point1, point2);
+ direction=[1.0, 0.0],
+ particle_spacing=0.1,
+ n_extrude=0, density=1.0)
+ @test_throws ArgumentError extrude_geometry((point1, point2);
+ direction=[0.0, 0.0, 1.0],
+ particle_spacing=0.1,
+ n_extrude=1, density=1.0)
+ end
end
# 3D
diff --git a/test/setups/rectangular_shape.jl b/test/setups/rectangular_shape.jl
index 4e2419487f..aae2e8a290 100644
--- a/test/setups/rectangular_shape.jl
+++ b/test/setups/rectangular_shape.jl
@@ -48,6 +48,33 @@
@test shape.coordinates == expected_coords[i]
end
end
+
+ @testset "Function Density" begin
+ shape = RectangularShape(0.1, (2, 1), (0.0, 0.0),
+ density=coords -> 1000.0 + coords[1])
+
+ @test shape.density ≈ [1000.05, 1000.15]
+ end
+
+ @testset "Coordinates Perturbation Does Not Reset Random State" begin
+ Random.seed!(42)
+ first_random_number = rand()
+ next_random_number = rand()
+
+ Random.seed!(42)
+ @test rand() == first_random_number
+
+ RectangularShape(0.1, (2, 2), (0.0, 0.0), density=1.0,
+ coordinates_perturbation=0.1)
+
+ @test rand() == next_random_number
+ end
+
+ @testset "Errors" begin
+ @test_throws ArgumentError RectangularShape(0.1, (2, 2), (0.0, 0.0),
+ density=1000.0,
+ acceleration=(0.0, -9.81, 0.0))
+ end
end
# Only show all of these nested testsets in case of errors
@@ -123,6 +150,26 @@
@test shape.pressure ≈ 4.71 * 1000.0 * vec(reverse(pressure'))
end
end
+
+ @testset "Function Density" begin
+ density_function = coords -> 1000.0 + 100coords[1] + 10coords[2]
+ shape = RectangularShape(particle_spacing, (2, 3), (0.0, 0.0),
+ density=density_function,
+ acceleration=(0.0, -1.0))
+
+ @test shape.density ≈ [1005.5, 1015.5, 1006.5, 1016.5, 1007.5, 1017.5]
+ @test shape.pressure ≈ [251.775, 254.275, 151.125, 152.625, 50.375,
+ 50.875]
+ @test shape.mass ≈ particle_spacing^2 * shape.density
+ end
+
+ @testset "Zero Acceleration" begin
+ shape = RectangularShape(particle_spacing, (2, 5), (0.0, 0.0),
+ density=1000.0, acceleration=(0.0, 0.0))
+
+ @test shape.pressure == zeros(10)
+ @test shape.density == 1000 * ones(10)
+ end
end
# Use `@trixi_testset` to isolate the mock functions in a separate namespace
@@ -186,6 +233,16 @@
shape.pressure)
@test shape.mass == particle_spacing^2 * shape.density
end
+
+ @testset "Zero Acceleration" begin
+ shape = RectangularShape(particle_spacing, (2, 5), (0.0, 0.0);
+ acceleration=(0.0, 0.0), state_equation)
+
+ @test shape.pressure == zeros(10)
+ @test shape.density ==
+ TrixiParticles.inverse_state_equation.(Ref(state_equation),
+ shape.pressure)
+ end
end
end
diff --git a/test/setups/rectangular_tank.jl b/test/setups/rectangular_tank.jl
index 1a1fc88508..33775a5873 100644
--- a/test/setups/rectangular_tank.jl
+++ b/test/setups/rectangular_tank.jl
@@ -119,6 +119,43 @@
(water_width, water_height, 0.5),
(tank_width, tank_height),
water_density, spacing_ratio=3)
+
+ error = ArgumentError("`fluid_size` dimensions need to be non-negative")
+ @test_throws error RectangularTank(particle_spacing,
+ (-water_width, water_height),
+ (tank_width, tank_height),
+ water_density)
+
+ error = ArgumentError("`tank_size` dimensions need to be non-negative")
+ @test_throws error RectangularTank(particle_spacing,
+ (water_width, water_height),
+ (-tank_width, tank_height),
+ water_density)
+
+ @test_throws ArgumentError RectangularTank(particle_spacing,
+ (water_width, water_height),
+ (tank_width, tank_height),
+ water_density, spacing_ratio=0)
+
+ @test_throws ArgumentError RectangularTank(particle_spacing,
+ (water_width, water_height),
+ (tank_width, tank_height),
+ water_density, n_layers=0)
+
+ @test_throws ArgumentError RectangularTank(particle_spacing,
+ (water_width, water_height),
+ (tank_width, tank_height),
+ water_density, n_layers=1.5)
+
+ tank = RectangularTank(0.1, (1.0, 1.0), (0.3, 0.3), water_density)
+ @test tank.n_particles_per_dimension == (3, 3)
+ @test all(tank.fluid_size .≈ (0.3, 0.3))
+
+ tank = RectangularTank(0.1, (1.0, 1.0), (0.05, 0.3), water_density;
+ acceleration=(1.0, 0.0))
+ @test isempty(tank.fluid.coordinates)
+ @test tank.n_particles_per_dimension == (0, 3)
+ @test all(tank.fluid_size .≈ (0.0, 0.3))
end
end
diff --git a/test/setups/sphere_shape.jl b/test/setups/sphere_shape.jl
index c8b87d76db..42fc3e7b70 100644
--- a/test/setups/sphere_shape.jl
+++ b/test/setups/sphere_shape.jl
@@ -79,6 +79,18 @@
end
end
+ @testset verbose=true "Errors" begin
+ @test_throws ArgumentError SphereShape(0.1, 0.5, (0.0, 0.0), 1000.0;
+ cutout_min=(0.2, 0.0),
+ cutout_max=(0.1, 0.1))
+ @test_throws ArgumentError SphereShape(0.1, 0.5, (0.0, 0.0, 0.0),
+ 1000.0; cutout_min=(0.0, 0.0),
+ cutout_max=(0.1, 0.1))
+ @test_nowarn SphereShape(0.1, 0.5, (0.0, 0.0, 0.0),
+ 1000.0; cutout_min=(0.0, 0.0),
+ cutout_max=(0.0, 0.0))
+ end
+
@testset verbose=true "SphereShape 3D" begin
shape_names = [
"1-particle VoxelSphere",
diff --git a/test/systems/boundary_system.jl b/test/systems/boundary_system.jl
index 8c55dfa029..7b99a99f70 100644
--- a/test/systems/boundary_system.jl
+++ b/test/systems/boundary_system.jl
@@ -28,6 +28,58 @@
end
end
+ @testset verbose=true "High-level Dummy-Particle Builder" begin
+ boundary_coordinates = [1.0 2.0
+ 1.0 2.0]
+ fluid_coordinates = [0.0 0.5
+ 0.0 0.0]
+
+ boundary_ic = InitialCondition(; coordinates=boundary_coordinates, mass, density)
+ fluid_ic = InitialCondition(; coordinates=fluid_coordinates, mass, density)
+
+ smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+ smoothing_length = 0.8
+ state_equation = StateEquationCole(; sound_speed=15.0, reference_density=1000.0,
+ exponent=1)
+ viscosity = ViscosityAdami(nu=1e-6)
+ surface_measure = [0.1, 0.0]
+
+ fluid_system = WeaklyCompressibleSPHSystem(fluid_ic;
+ density_calculator=ContinuityDensity(),
+ state_equation, smoothing_kernel,
+ smoothing_length,
+ correction=KernelCorrection(),
+ reference_particle_spacing=0.1)
+
+ boundary_model = BoundaryModelDummyParticles(boundary_ic;
+ fluid_system=fluid_system,
+ viscosity=viscosity,
+ surface_measure)
+ system = WallBoundarySystem(boundary_ic, boundary_model,
+ adhesion_coefficient=0.3,
+ color_value=2)
+
+ @test system isa WallBoundarySystem
+ @test system.boundary_model isa BoundaryModelDummyParticles
+ @test system.boundary_model.hydrodynamic_mass == boundary_ic.mass
+ @test system.boundary_model.density_calculator isa AdamiPressureExtrapolation
+ @test system.boundary_model.smoothing_kernel === smoothing_kernel
+ @test system.boundary_model.smoothing_length == smoothing_length
+ @test system.boundary_model.viscosity == viscosity
+ @test system.boundary_model.state_equation == state_equation
+ @test system.boundary_model.correction isa KernelCorrection
+ @test system.boundary_model.cache.reference_particle_spacing == 0.1
+ @test system.boundary_model.cache.surface_measure == surface_measure
+ @test system.adhesion_coefficient == 0.3
+ @test system.cache.color == 2
+
+ edac_system = EntropicallyDampedSPHSystem(fluid_ic; smoothing_kernel,
+ smoothing_length, sound_speed=15.0)
+ edac_boundary_model = BoundaryModelDummyParticles(boundary_ic;
+ fluid_system=edac_system)
+ @test edac_boundary_model.state_equation === nothing
+ end
+
@testset verbose=true "Moving Boundaries" begin
@testset "$(i+1)D" for i in 1:2
NDIMS = i + 1
diff --git a/test/systems/edac_system.jl b/test/systems/edac_system.jl
index ce04cd6774..33316af9ae 100644
--- a/test/systems/edac_system.jl
+++ b/test/systems/edac_system.jl
@@ -213,6 +213,57 @@
@test v0 == vcat(velocity, [0.8, 1.0]')
end
+ @trixi_testset "Correction cache updates" begin
+ coordinates = [0.0 0.1 0.0
+ 0.0 0.0 0.1]
+ velocity = zeros(2, 3)
+ mass = ones(3)
+ density = fill(1000.0, 3)
+ pressure = zeros(3)
+ initial_condition = InitialCondition(; coordinates, velocity, mass, density,
+ pressure)
+ smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+ smoothing_length = 0.5
+
+ corrections = (ShepardKernelCorrection(), KernelCorrection(), GradientCorrection(),
+ MixedKernelGradientCorrection())
+
+ @testset "$(typeof(correction))" for correction in corrections
+ system = EntropicallyDampedSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length, sound_speed=10.0,
+ correction, pressure_acceleration=nothing)
+ semi = Semidiscretization(system)
+
+ TrixiParticles.initialize_neighborhood_searches!(semi)
+
+ u_ode = vec(coordinates)
+ v0 = zeros(TrixiParticles.v_nvariables(system),
+ TrixiParticles.n_integrated_particles(system))
+ TrixiParticles.write_v0!(v0, system)
+ v_ode = vec(v0)
+
+ v = TrixiParticles.wrap_v(v_ode, system, semi)
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+
+ for cache_key in (:kernel_correction_coefficient, :dw_gamma,
+ :correction_matrix)
+ hasproperty(system.cache, cache_key) || continue
+
+ fill!(getproperty(system.cache, cache_key), NaN)
+ end
+
+ TrixiParticles.update_quantities!(system, v, u, v_ode, u_ode, semi, 0.0)
+ TrixiParticles.update_pressure!(system, v, u, v_ode, u_ode, semi, 0.0)
+
+ for cache_key in (:kernel_correction_coefficient, :dw_gamma,
+ :correction_matrix)
+ hasproperty(system.cache, cache_key) || continue
+
+ @test all(isfinite, getproperty(system.cache, cache_key))
+ end
+ end
+ end
+
@trixi_testset "Average Pressure" begin
particle_spacing = 0.1
smoothing_kernel = SchoenbergCubicSplineKernel{2}()
@@ -244,4 +295,35 @@
nparticles(system))
end
end
+
+ @trixi_testset "restart_with! with ContinuityDensity" begin
+ coordinates = [0.5 2.0
+ 1.0 2.0]
+ velocity = 2 * coordinates
+ mass = [1.25, 1.5]
+ density = [990.0, 1000.0]
+ pressure = [5.0, 7.8]
+ smoothing_kernel = Val(:smoothing_kernel)
+ TrixiParticles.ndims(::Val{:smoothing_kernel}) = 2
+ smoothing_length = 0.362
+ sound_speed = 10.0
+
+ initial_condition = InitialCondition(; coordinates, velocity, mass, density,
+ pressure)
+ system = EntropicallyDampedSPHSystem(initial_condition; smoothing_kernel,
+ smoothing_length, sound_speed,
+ density_calculator=ContinuityDensity())
+
+ u_new = coordinates .+ 1
+ velocity_new = velocity .+ 2
+ pressure_new = [11.0, 13.0]
+ density_new = [980.0, 970.0]
+ v_new = vcat(velocity_new, pressure_new', density_new')
+
+ TrixiParticles.restart_with!(system, v_new, u_new)
+
+ @test system.initial_condition.coordinates == u_new
+ @test system.initial_condition.velocity == velocity_new
+ @test system.initial_condition.pressure == pressure_new
+ end
end
diff --git a/test/systems/iisph_system.jl b/test/systems/iisph_system.jl
index 2bcd740552..de38f2cd2c 100644
--- a/test/systems/iisph_system.jl
+++ b/test/systems/iisph_system.jl
@@ -68,6 +68,7 @@
@test system.max_iterations == max_iterations
@test system.time_step == time_step
@test length(system.density) == size(coordinates, 2)
+ @test TrixiParticles.system_state_equation(system) === nothing
# A too-short acceleration vector triggers dimension validation
error_str1 = "`acceleration` must be of length $NDIMS for a $(NDIMS)D problem"
@@ -439,12 +440,12 @@
system_pressure.predicted_density .= [990.0, 1010.0]
system_pressure.sum_term .= [5.0, -2.0]
system_pressure.a_ii .= [0.5, 1.0e-10]
- fill!(system_pressure.density_error, 0.0)
+ system_pressure.density_error .= [0.0, 99.0]
semi = DummySemidiscretization()
# First particle uses standard Jacobi update; second hits the safeguarded zero-a_ii path.
# For particle 1: (1-omega)*0 + omega/a_ii * (source - sum_term) with omega=0.4,
- # source=(1000-990)=10, a_ii=0.5, sum_term=5 gives pressure 4 and density_error -3
+ # source=(1000-990)=10, a_ii=0.5, sum_term=5 gives pressure 4 and abs(density_error) 3
relative_error = TrixiParticles.pressure_update(system_pressure,
system_pressure.pressure,
system_pressure.reference_density,
@@ -454,9 +455,50 @@
system_pressure.density_error,
semi)
- @test isapprox(relative_error, -0.003)
+ @test isapprox(relative_error, 0.003)
@test isapprox(system_pressure.pressure, [4.0, 0.0])
- @test isapprox(system_pressure.density_error, [-3.0, 0.0])
+ @test isapprox(system_pressure.density_error, [3.0, 0.0])
+ end
+
+ @testset "Cross-system pressure sums use neighbor coordinates" begin
+ smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+ smoothing_length = 0.5
+ time_step = 0.5
+
+ coordinates_a = reshape([0.0, 0.0], 2, 1)
+ ic_a = InitialCondition(; coordinates=coordinates_a,
+ velocity=zeros(2, 1),
+ mass=[1.0],
+ density=[1000.0],
+ pressure=[1.0])
+ system = ImplicitIncompressibleSPHSystem(ic_a;
+ smoothing_kernel,
+ smoothing_length,
+ reference_density=1000.0,
+ time_step)
+
+ coordinates_b = [0.1 0.2
+ 0.0 0.0]
+ ic_b = InitialCondition(; coordinates=coordinates_b,
+ velocity=zeros(2, 2),
+ mass=[1.0, 1.0],
+ density=[1000.0, 1000.0],
+ pressure=[1.0, 2.0])
+ neighbor_system = ImplicitIncompressibleSPHSystem(ic_b;
+ smoothing_kernel,
+ smoothing_length,
+ reference_density=1000.0,
+ time_step)
+
+ semi = Semidiscretization(system, neighbor_system)
+ TrixiParticles.initialize_neighborhood_searches!(semi)
+ u_ode = vcat(vec(coordinates_a), vec(coordinates_b))
+ u = TrixiParticles.wrap_u(u_ode, system, semi)
+
+ @test_nowarn TrixiParticles.calculate_sum_d_ij_pj!(system.sum_d_ij_pj,
+ system, neighbor_system,
+ u, u_ode, semi)
+ @test !iszero(system.sum_d_ij_pj[1, 1])
end
@testset "Source term and iteration limits" begin
@@ -489,4 +531,30 @@
@test TrixiParticles.maximum_iisph_iterations(system_iters) == 7
end
end
+
+ @testset "Reject incompatible fluid systems" begin
+ smoothing_kernel = SchoenbergCubicSplineKernel{2}()
+ smoothing_length = 0.5
+ coordinates = [0.0 0.1
+ 0.0 0.2]
+ velocity = zeros(2, 2)
+ mass = [1.0, 1.0]
+ density = [1000.0, 1000.0]
+ pressure = [0.0, 0.0]
+ ic = InitialCondition(; coordinates, velocity, mass, density, pressure)
+
+ iisph_system = ImplicitIncompressibleSPHSystem(ic;
+ smoothing_kernel,
+ smoothing_length,
+ reference_density=1000.0,
+ time_step=0.5)
+ edac_system = EntropicallyDampedSPHSystem(ic; smoothing_kernel,
+ smoothing_length,
+ sound_speed=10.0)
+
+ error_str = "`ImplicitIncompressibleSPHSystem` cannot be used together with " *
+ "`EntropicallyDampedSPHSystem`"
+ @test_throws ArgumentError(error_str) Semidiscretization(iisph_system,
+ edac_system)
+ end
end
diff --git a/test/systems/open_boundary_system.jl b/test/systems/open_boundary_system.jl
index d1b2b3fc1d..eca71a9d6e 100644
--- a/test/systems/open_boundary_system.jl
+++ b/test/systems/open_boundary_system.jl
@@ -2,14 +2,16 @@
@testset "`show`" begin
# Mock fluid system
- struct FluidSystemMock2 <: TrixiParticles.AbstractFluidSystem{2}
+ struct FluidSystemMock2{B} <: TrixiParticles.AbstractFluidSystem{2}
pressure_acceleration_formulation::Nothing
density_diffusion::Nothing
+ buffer::B
end
TrixiParticles.initial_smoothing_length(system::FluidSystemMock2) = 1.0
TrixiParticles.nparticles(system::FluidSystemMock2) = 1
TrixiParticles.system_smoothing_kernel(system::FluidSystemMock2) = nothing
TrixiParticles.density_calculator(system::FluidSystemMock2) = TrixiParticles.ContinuityDensity()
+ TrixiParticles.buffer(system::FluidSystemMock2) = system.buffer
inflow = BoundaryZone(; boundary_face=([0.0, 0.0], [0.0, 1.0]),
particle_spacing=0.05,
@@ -17,7 +19,8 @@
open_boundary_layers=4, boundary_type=InFlow())
system = OpenBoundarySystem(inflow; buffer_size=0,
boundary_model=BoundaryModelCharacteristicsLastiwka(),
- fluid_system=FluidSystemMock2(nothing, nothing))
+ fluid_system=FluidSystemMock2(nothing, nothing,
+ nothing))
show_compact = "OpenBoundarySystem{2}() with 80 particles"
@test repr(system) == show_compact
@@ -40,7 +43,8 @@
boundary_type=OutFlow())
system = OpenBoundarySystem(outflow; buffer_size=0,
boundary_model=BoundaryModelMirroringTafuni(),
- fluid_system=FluidSystemMock2(nothing, nothing))
+ fluid_system=FluidSystemMock2(nothing, nothing,
+ nothing))
show_compact = "OpenBoundarySystem{2}() with 80 particles"
@test repr(system) == show_compact
@@ -59,7 +63,8 @@
system = OpenBoundarySystem(outflow, inflow; buffer_size=0,
boundary_model=BoundaryModelMirroringTafuni(),
- fluid_system=FluidSystemMock2(nothing, nothing))
+ fluid_system=FluidSystemMock2(nothing, nothing,
+ nothing))
show_compact = "OpenBoundarySystem{2}() with 160 particles"
@test repr(system) == show_compact
@@ -78,7 +83,8 @@
system = OpenBoundarySystem(outflow, inflow; buffer_size=0,
boundary_model=BoundaryModelDynamicalPressureZhang(),
- fluid_system=FluidSystemMock2(nothing, nothing))
+ fluid_system=FluidSystemMock2(nothing, nothing,
+ nothing))
show_compact = "OpenBoundarySystem{2}() with 160 particles"
@test repr(system) == show_compact
@@ -96,6 +102,20 @@
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"""
@test repr("text/plain", system) == show_box
+
+ fluid_system_with_buffer = FluidSystemMock2(nothing, nothing,
+ TrixiParticles.SystemBuffer(1, 3))
+ system = OpenBoundarySystem(outflow; fluid_system=fluid_system_with_buffer)
+ @test system.boundary_model isa BoundaryModelMirroringTafuni
+ @test system.buffer.buffer_size == 3
+
+ error_str = "`buffer_size` could not be inferred for `OpenBoundarySystem` " *
+ "because `fluid_system` has no buffer. Pass `buffer_size=...` " *
+ "explicitly or construct `fluid_system` with `buffer_size=...`."
+ @test_throws ArgumentError(error_str) OpenBoundarySystem(outflow;
+ fluid_system=FluidSystemMock2(nothing,
+ nothing,
+ nothing))
end
@testset "boundary zone width" begin
diff --git a/test/systems/packing_system.jl b/test/systems/packing_system.jl
index 20c915fb4c..7b80a723b4 100644
--- a/test/systems/packing_system.jl
+++ b/test/systems/packing_system.jl
@@ -41,6 +41,22 @@
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"""
@test repr("text/plain", system) == show_box
+ signed_distance_field = SignedDistanceField(geometry, 0.1;
+ use_for_boundary_packing=true,
+ max_signed_distance=0.3)
+ boundary_sampled = sample_boundary(signed_distance_field; boundary_density=1.0,
+ boundary_thickness=0.2,
+ place_on_shell=false)
+ system = ParticlePackingSystem(boundary_sampled; signed_distance_field,
+ background_pressure=1.0, is_boundary=true,
+ boundary_thickness=0.2)
+ @test system.shift_length == -0.25
+ @test_throws ArgumentError ParticlePackingSystem(boundary_sampled;
+ signed_distance_field,
+ background_pressure=1.0,
+ is_boundary=true,
+ boundary_thickness=0.4)
+
system = ParticlePackingSystem(initial_condition,
signed_distance_field=nothing,
background_pressure=1.0)