Skip to content

Commit e9ae581

Browse files
efaulhaberLasNikassvchb
authored
Implement OscillatingMotion (#915)
* Add `OscillatingMotion` * Fix docs * Add tests * Reformat * Fix docs * Change API to use keyword arguments * Implement suggestions --------- Co-authored-by: Niklas Neher <[email protected]> Co-authored-by: Sven Berger <[email protected]>
1 parent ce2edcb commit e9ae581

File tree

11 files changed

+238
-9
lines changed

11 files changed

+238
-9
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ TrixiParticles.jl follows the interpretation of
44
[semantic versioning (semver)](https://julialang.github.io/Pkg.jl/dev/compatibility/#Version-specifier-format-1)
55
used in the Julia ecosystem. Notable changes will be documented in this file for human readability.
66

7+
## Version 0.4.2
8+
9+
### Features
10+
11+
- Added `OscillatingMotion2D` to create an oscillating `PrescribedMotion` combining
12+
translation and rotation (#915).
13+
714
## Version 0.4.1
815

916
### Features

docs/src/systems/boundary.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
BoundaryDEMSystem
99
```
1010

11-
```@docs
12-
PrescribedMotion
11+
```@autodocs
12+
Modules = [TrixiParticles]
13+
Pages = [joinpath("schemes", "boundary", "prescribed_motion.jl")]
1314
```
1415

1516

src/TrixiParticles.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export BoundaryModelMonaghanKajtar, BoundaryModelDummyParticles, AdamiPressureEx
8989
BernoulliPressureExtrapolation
9090
export FirstOrderMirroring, ZerothOrderMirroring, SimpleMirroring
9191
export HertzContactModel, LinearContactModel
92-
export PrescribedMotion
92+
export PrescribedMotion, OscillatingMotion2D
9393
export examples_dir, validation_dir
9494
export trixi2vtk, vtk2trixi
9595
export RectangularTank, RectangularShape, SphereShape, ComplexShape

src/schemes/boundary/prescribed_motion.jl

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
are moving or not. Its boolean return value determines
1111
if the neighborhood search will be updated.
1212
13-
# Keyword Arguments
13+
# Keywords
1414
- `moving_particles`: Indices of moving particles. Default is each particle in the system.
1515
1616
# Examples
@@ -98,3 +98,83 @@ function (prescribed_motion::Nothing)(system::AbstractSystem, t, semi)
9898

9999
return system
100100
end
101+
102+
"""
103+
OscillatingMotion2D(; frequency, translation_vector, rotation_angle, rotation_center,
104+
rotation_phase_offset=0, tspan=(-Inf, Inf),
105+
ramp_up_tspan=(0.0, 0.0), moving_particles=nothing)
106+
107+
Create a [`PrescribedMotion`](@ref) for a 2D oscillating motion of particles.
108+
The motion is a combination of a translation and a rotation around a center point
109+
with the same frequency. Note that a phase offset can be specified for a rotation
110+
that is out of sync with the translation.
111+
112+
Create a [`PrescribedMotion`](@ref) for 2D oscillatory particle motion
113+
that combines a translation and a rotation about a specified center.
114+
Both use the same frequency; an optional phase offset allows the rotation
115+
to be out of phase with the translation.
116+
117+
# Keywords
118+
- `frequency`: Frequency of the oscillation in Hz.
119+
- `translation_vector`: Vector specifying the amplitude and direction of the translation.
120+
- `rotation_angle`: Maximum rotation angle in radians.
121+
- `rotation_center`: Center point of the rotation as an `SVector`.
122+
123+
# Keywords (optional)
124+
- `rotation_phase_offset=0`: Phase offset of the rotation in number of periods (`1` = 1 period).
125+
- `tspan=(-Inf, Inf)`: Time span in which the motion is active. Outside of this time span,
126+
particles remain at their last position.
127+
- `ramp_up_tspan`: Time span in which the motion is smoothly ramped up from zero
128+
to full amplitude.
129+
Outside of this time span, the motion has full amplitude.
130+
Default is no ramp-up.
131+
- `moving_particles`: Indices of moving particles. Default is each particle in the system
132+
for the [`WallBoundarySystem`](@ref) and all clamped particles
133+
for the [`TotalLagrangianSPHSystem`](@ref).
134+
135+
# Examples
136+
```jldoctest; output = false, filter = r"PrescribedMotion{.*"
137+
# Oscillating motion with frequency 1 Hz, translation amplitude 1 in x-direction,
138+
# rotation angle 90 degrees (pi/2) around the origin.
139+
frequency = 1.0
140+
translation_vector = SVector(1.0, 0.0)
141+
rotation_angle = pi / 2
142+
rotation_center = SVector(0.0, 0.0)
143+
144+
motion = OscillatingMotion2D(frequency, translation_vector, rotation_angle,
145+
rotation_center)
146+
147+
# output
148+
PrescribedMotion{...}
149+
"""
150+
function OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
151+
rotation_center, rotation_phase_offset=0, tspan=(-Inf, Inf),
152+
ramp_up_tspan=(0.0, 0.0), moving_particles=nothing)
153+
@inline function movement_function(x, t)
154+
if isfinite(tspan[1])
155+
t = t - tspan[1]
156+
end
157+
158+
sin_scaled = sin(frequency * 2pi * t)
159+
translation = sin_scaled * translation_vector
160+
x_centered = x .- rotation_center
161+
sin_scaled_offset = sin(2pi * (frequency * t - rotation_phase_offset))
162+
angle = rotation_angle * sin_scaled_offset
163+
rotated = SVector(x_centered[1] * cos(angle) - x_centered[2] * sin(angle),
164+
x_centered[1] * sin(angle) + x_centered[2] * cos(angle))
165+
166+
result = rotated .+ rotation_center .+ translation
167+
168+
if ramp_up_tspan[2] > ramp_up_tspan[1] && ramp_up_tspan[1] <= t <= ramp_up_tspan[2]
169+
# Apply a smoothstep ramp-up during the ramp-up time span
170+
t_rel = (t - ramp_up_tspan[1]) / (ramp_up_tspan[2] - ramp_up_tspan[1])
171+
ramp_factor = 3 * t_rel^2 - 2 * t_rel^3
172+
return result * ramp_factor + (1 - ramp_factor) * x
173+
end
174+
return result
175+
end
176+
177+
is_moving(t) = tspan[1] <= t <= tspan[2]
178+
179+
return PrescribedMotion(movement_function, is_moving; moving_particles)
180+
end

src/schemes/boundary/wall_boundary/system.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The interaction between fluid and boundary particles is specified by the boundar
99
- `initial_condition`: Initial condition (see [`InitialCondition`](@ref))
1010
- `boundary_model`: Boundary model (see [Boundary Models](@ref boundary_models))
1111
12-
# Keyword Arguments
12+
# Keywords
1313
- `prescribed_motion`: For moving boundaries, a [`PrescribedMotion`](@ref) can be passed.
1414
- `adhesion_coefficient`: Coefficient specifying the adhesion of a fluid to the surface.
1515
Note: currently it is assumed that all fluids have the same adhesion coefficient.

src/schemes/fluid/entropically_damped_sph/system.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ See [Entropically Damped Artificial Compressibility for SPH](@ref edac) for more
2222
- `smoothing_length`: Smoothing length to be used for this system.
2323
See [Smoothing Kernels](@ref smoothing_kernel).
2424
25-
# Keyword Arguments
25+
# Keywords
2626
- `viscosity`: Viscosity model for this system (default: no viscosity).
2727
Recommended: [`ViscosityAdami`](@ref).
2828
- `acceleration`: Acceleration vector for the system. (default: zero vector)

src/schemes/fluid/surface_normal_sph.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
Color field based computation of the interface normals.
66
7-
# Keyword Arguments
7+
# Keywords
88
- `boundary_contact_threshold=0.1`: If this threshold is reached the fluid is assumed to be in contact with the boundary.
99
- `interface_threshold=0.01`: Threshold for normals to be removed as being invalid.
1010
- `ideal_density_threshold=0.0`: Assume particles are inside if they are above this threshold, which is relative to the `ideal_neighbor_count`.

src/schemes/fluid/weakly_compressible_sph/system.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ See [Weakly Compressible SPH](@ref wcsph) for more details on the method.
2626
- `smoothing_length`: Smoothing length to be used for this system.
2727
See [Smoothing Kernels](@ref smoothing_kernel).
2828
29-
# Keyword Arguments
29+
# Keywords
3030
- `acceleration`: Acceleration vector for the system. (default: zero vector)
3131
- `viscosity`: Viscosity model for this system (default: no viscosity).
3232
See [`ArtificialViscosityMonaghan`](@ref) or [`ViscosityAdami`](@ref).

src/schemes/structure/total_lagrangian_sph/system.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ See [Total Lagrangian SPH](@ref tlsph) for more details on the method.
2222
- `smoothing_length`: Smoothing length to be used for this system.
2323
See [Smoothing Kernels](@ref smoothing_kernel).
2424
25-
# Keyword Arguments
25+
# Keywords
2626
- `n_clamped_particles`: Number of clamped particles which are fixed and not integrated
2727
to clamp the structure. Note that the clamped particles must be the **last**
2828
particles in the `InitialCondition`. See the info box below.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Note that more tests for `PrescribedMotion` are in `test/systems/boundary_system.jl`.
2+
@testset verbose=true "PrescribedMotion" begin
3+
@testset "OscillatingMotion2D" begin
4+
@testset "Simple Rotation" begin
5+
frequency = 1.0
6+
translation_vector = SVector(0.0, 0.0)
7+
rotation_angle = pi
8+
rotation_center = SVector(0.0, 0.0)
9+
10+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
11+
rotation_center)
12+
movement_function = motion.movement_function
13+
14+
@test isapprox(movement_function([1.0, 0.0], 0.0), [1.0, 0.0], atol=eps())
15+
# Initial position after one phase
16+
@test isapprox(movement_function([1.0, 0.0], 1.0), [1.0, 0.0], atol=10eps())
17+
# Initial position after one half phase
18+
@test isapprox(movement_function([1.0, 0.0], 0.5), [1.0, 0.0], atol=10eps())
19+
# Half rotation after one quarter phase (rotation angle is one half rotation)
20+
@test isapprox(movement_function([1.0, 0.0], 0.25), [-1.0, 0.0], atol=10eps())
21+
# Quarter rotation (0.5 * rotation_angle = 90 degrees)
22+
@test isapprox(movement_function([1.0, 0.0], asin(0.5) / 2pi), [0.0, 1.0],
23+
atol=10eps())
24+
end
25+
26+
@testset "Simple Translation" begin
27+
frequency = 1.0
28+
translation_vector = SVector(1.0, 0.0)
29+
rotation_angle = 0.0
30+
rotation_center = SVector(0.0, 0.0)
31+
32+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
33+
rotation_center)
34+
movement_function = motion.movement_function
35+
36+
@test isapprox(movement_function([0.0, 0.0], 0.0), [0.0, 0.0], atol=eps())
37+
@test isapprox(movement_function([0.0, 0.0], 0.25), [1.0, 0.0], atol=10eps())
38+
@test isapprox(movement_function([0.0, 0.0], 0.5), [0.0, 0.0], atol=10eps())
39+
@test isapprox(movement_function([0.0, 0.0], 0.75), [-1.0, 0.0], atol=10eps())
40+
@test isapprox(movement_function([0.0, 0.0], 1.0), [0.0, 0.0], atol=10eps())
41+
end
42+
43+
@testset "Simple Translation tspan" begin
44+
frequency = 1.0
45+
translation_vector = SVector(1.0, 0.0)
46+
rotation_angle = 0.0
47+
rotation_center = SVector(0.0, 0.0)
48+
tspan = (0.4, 1.4)
49+
50+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
51+
rotation_center, tspan)
52+
movement_function = motion.movement_function
53+
54+
@test isapprox(movement_function([0.0, 0.0], 0.4), [0.0, 0.0], atol=eps())
55+
@test isapprox(movement_function([0.0, 0.0], 0.65), [1.0, 0.0], atol=10eps())
56+
@test isapprox(movement_function([0.0, 0.0], 0.9), [0.0, 0.0], atol=10eps())
57+
@test isapprox(movement_function([0.0, 0.0], 1.15), [-1.0, 0.0], atol=10eps())
58+
@test isapprox(movement_function([0.0, 0.0], 1.4), [0.0, 0.0], atol=10eps())
59+
end
60+
61+
@testset "Combined Rotation and Translation" begin
62+
frequency = 1.0
63+
translation_vector = SVector(1.0, 0.0)
64+
rotation_angle = pi / 2
65+
rotation_center = SVector(0.0, 0.0)
66+
67+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
68+
rotation_center)
69+
movement_function = motion.movement_function
70+
71+
@test isapprox(movement_function([1.0, 0.0], 0.0), [1.0, 0.0], atol=eps())
72+
@test isapprox(movement_function([1.0, 0.0], 0.25), [1.0, 1.0], atol=10eps())
73+
@test isapprox(movement_function([1.0, 0.0], 0.5), [1.0, 0.0], atol=10eps())
74+
@test isapprox(movement_function([1.0, 0.0], 0.75), [-1.0, -1.0], atol=10eps())
75+
@test isapprox(movement_function([1.0, 0.0], 1.0), [1.0, 0.0], atol=10eps())
76+
end
77+
78+
@testset "Phase Offset" begin
79+
frequency = 1.0
80+
translation_vector = SVector(1.0, 0.0)
81+
rotation_angle = pi / 2
82+
rotation_center = SVector(0.0, 0.0)
83+
rotation_phase_offset = 0.25
84+
85+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
86+
rotation_center, rotation_phase_offset)
87+
movement_function = motion.movement_function
88+
89+
@test isapprox(movement_function([1.0, 0.0], 0.0), [0.0, -1.0], atol=eps())
90+
@test isapprox(movement_function([1.0, 0.0], 0.25), [2.0, 0.0], atol=10eps())
91+
@test isapprox(movement_function([1.0, 0.0], 0.5), [0.0, 1.0], atol=10eps())
92+
@test isapprox(movement_function([1.0, 0.0], 0.75), [0.0, 0.0], atol=10eps())
93+
@test isapprox(movement_function([1.0, 0.0], 1.0), [0.0, -1.0], atol=10eps())
94+
end
95+
96+
@testset "Ramp-up" begin
97+
frequency = 1.0
98+
translation_vector = SVector(1.0, 0.0)
99+
rotation_angle = 0.0
100+
rotation_center = SVector(0.0, 0.0)
101+
ramp_up_tspan = (0.0, 1.0)
102+
103+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
104+
rotation_center, ramp_up_tspan)
105+
movement_function = motion.movement_function
106+
107+
@test isapprox(movement_function([0.0, 0.0], 0.0), [0.0, 0.0], atol=eps())
108+
# During ramp-up, amplitude is reduced
109+
@test isapprox(movement_function([0.0, 0.0], 0.25), [0.15625, 0.0],
110+
atol=10eps())
111+
@test isapprox(movement_function([0.0, 0.0], 0.5), [0.0, 0.0], atol=10eps())
112+
# Less reduction at later times during ramp-up
113+
@test isapprox(movement_function([0.0, 0.0], 0.75), [-0.84375, 0.0],
114+
atol=10eps())
115+
@test isapprox(movement_function([0.0, 0.0], 1.0), [0.0, 0.0], atol=10eps())
116+
# After ramp-up, full amplitude
117+
@test isapprox(movement_function([0.0, 0.0], 1.25), [1.0, 0.0], atol=10eps())
118+
end
119+
120+
@testset "tspan" begin
121+
frequency = 1.0
122+
translation_vector = SVector(1.0, 0.0)
123+
rotation_angle = 0.0
124+
rotation_center = SVector(0.0, 0.0)
125+
tspan = (0.5, 1.5)
126+
127+
motion = OscillatingMotion2D(; frequency, translation_vector, rotation_angle,
128+
rotation_center, tspan)
129+
ismoving = motion.is_moving
130+
131+
@test !ismoving(-0.5)
132+
@test !ismoving(0.0)
133+
@test !ismoving(0.25)
134+
@test ismoving(0.5)
135+
@test ismoving(1.0)
136+
@test ismoving(1.5)
137+
@test !ismoving(1.75)
138+
end
139+
end
140+
end

0 commit comments

Comments
 (0)