Skip to content

Commit 5aa80fe

Browse files
committed
Add ThrustCalculator
1 parent 5b1c85d commit 5aa80fe

4 files changed

Lines changed: 227 additions & 2 deletions

File tree

src/TrixiParticles.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export BoundaryZone, InFlow, OutFlow, BidirectionalFlow
7474
export InfoCallback, SolutionSavingCallback, DensityReinitializationCallback,
7575
PostprocessCallback, StepsizeCallback, UpdateCallback, SteadyStateReachedCallback,
7676
SplitIntegrationCallback, MechanicalWorkCalculator, calculated_mechanical_work,
77+
ThrustCalculator, calculated_thrust,
7778
SortingCallback
7879
export ContinuityDensity, SummationDensity
7980
export PenaltyForceGanzenmueller, TransportVelocityAdami, ParticleShiftingTechnique,

src/general/mechanical_work_calculator.jl

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,127 @@ function compute_structure_acceleration!(dv, system, eachparticle,
212212

213213
return v
214214
end
215+
216+
"""
217+
ThrustCalculator(system::TotalLagrangianSPHSystem, semi;
218+
direction,
219+
eachparticle=eachparticle(system))
220+
221+
Functor that computes the instantaneous hydrodynamic force exerted by the fluid on a
222+
[`TotalLagrangianSPHSystem`](@ref), projected onto `direction`.
223+
It can be passed as a custom quantity to [`PostprocessCallback`](@ref).
224+
225+
For a fixed fluid-interacting structure in a channel flow, choose `direction` as the
226+
direction of useful force and multiply the recorded thrust by the corresponding reference
227+
speed to obtain useful mechanical power.
228+
229+
!!! warning "Experimental implementation"
230+
This is an experimental feature and may change in future releases.
231+
232+
# Arguments
233+
- `system`: The [`TotalLagrangianSPHSystem`](@ref) whose hydrodynamic force should be
234+
monitored.
235+
- `semi`: The [`Semidiscretization`](@ref) that contains `system`.
236+
237+
# Keywords
238+
- `direction`: Direction onto which the hydrodynamic force is projected. The vector is
239+
normalized internally.
240+
- `eachparticle=eachparticle(system)`: Iterator selecting which particles contribute.
241+
242+
# Examples
243+
```jldoctest; output = false, setup = :(system = TotalLagrangianSPHSystem(RectangularShape(0.1, (3, 4), (0.1, 0.0), density=1.0); smoothing_kernel=WendlandC2Kernel{2}(), smoothing_length=1.0, young_modulus=1.0, poisson_ratio=1.0); semi = (; systems=(system,), parallelization_backend=SerialBackend()))
244+
# Create a thrust calculator in x-direction
245+
thrust_calculator = ThrustCalculator(system, semi; direction=SVector(1.0, 0.0))
246+
247+
# After postprocessing, retrieve the latest thrust value
248+
thrust = calculated_thrust(thrust_calculator)
249+
250+
# output
251+
0.0
252+
```
253+
"""
254+
mutable struct ThrustCalculator{ELTYPE, DV, EP, D}
255+
thrust :: ELTYPE
256+
system_index :: Int
257+
dv :: DV
258+
eachparticle :: EP
259+
direction :: D
260+
end
261+
262+
# This should dispatch on `TotalLagrangianSPHSystem`, but this name is not yet defined
263+
# due to the include order.
264+
function ThrustCalculator(system::AbstractStructureSystem, semi;
265+
direction, eachparticle=eachparticle(system))
266+
ELTYPE = eltype(system)
267+
system_index = system_indices(system, semi)
268+
269+
# Check vector length before converting to `SVector` to avoid extremely long
270+
# compile times when accidentally passing large vectors.
271+
if length(direction) != ndims(system)
272+
throw(ArgumentError("length of `direction` must match the number of dimensions"))
273+
end
274+
direction_ = SVector(Tuple(direction))
275+
if iszero(direction_)
276+
throw(ArgumentError("`direction` must not be zero"))
277+
end
278+
direction_ = normalize(direction_)
279+
280+
# Allocate buffer to write hydrodynamic accelerations for all particles.
281+
# `PostprocessCallback` transfers data to the CPU before calling custom quantities,
282+
# so this can be a regular `Array` even when the simulation is running on a GPU.
283+
dv = Array{ELTYPE, 2}(undef, (v_nvariables(system), nparticles(system)))
284+
285+
return ThrustCalculator(zero(ELTYPE), system_index, dv, eachparticle, direction_)
286+
end
287+
288+
function reset!(calculator::ThrustCalculator)
289+
calculator.thrust = zero(calculator.thrust)
290+
291+
return calculator
292+
end
293+
294+
"""
295+
calculated_thrust(calculator::ThrustCalculator)
296+
297+
Get the latest projected hydrodynamic force from a [`ThrustCalculator`](@ref).
298+
"""
299+
function calculated_thrust(calculator::ThrustCalculator)
300+
return calculator.thrust
301+
end
302+
303+
function (calculator::ThrustCalculator)(system, dv_ode, du_ode, v_ode, u_ode, semi, t)
304+
if system_indices(system, semi) != calculator.system_index
305+
return nothing
306+
end
307+
308+
thrust = update_thrust!(system, calculator.eachparticle, calculator.direction,
309+
calculator.dv, v_ode, u_ode, semi, t)
310+
311+
calculator.thrust = thrust
312+
313+
return calculator.thrust
314+
end
315+
316+
function update_thrust!(system, eachparticle, direction, dv, v_ode, u_ode, semi, t)
317+
# Note that the systems and NHS have already been updated by the
318+
# `PostprocessCallback` before calling this function.
319+
@trixi_timeit timer() "calculate thrust" begin
320+
compute_structure_acceleration!(dv, system, eachparticle, true,
321+
v_ode, u_ode, semi, t)
322+
323+
return projected_force(dv, system, eachparticle, direction)
324+
end
325+
end
326+
327+
function projected_force(dv, system, eachparticle, direction)
328+
force = zero(eltype(system))
329+
330+
# Note that this is a reduction, so we cannot use `@threaded` here.
331+
for particle in eachparticle
332+
dv_particle = extract_svector(dv, system, particle)
333+
force_particle = system.mass[particle] * dv_particle
334+
force += dot(force_particle, direction)
335+
end
336+
337+
return force
338+
end

test/callbacks/mechanical_work_calculator.jl

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
# Mock system
33
struct MockSystem <: TrixiParticles.AbstractStructureSystem{2}
44
eltype::Type
5+
mass::Vector
6+
7+
function MockSystem(ELTYPE)
8+
new(ELTYPE, ELTYPE[1, 2, 3, 4])
9+
end
510
end
6-
TrixiParticles.nparticles(::MockSystem) = 4
711
Base.eltype(system::MockSystem) = system.eltype
812

913
function TrixiParticles.create_neighborhood_search(neighborhood_search,
@@ -40,6 +44,100 @@
4044
@test eltype(calculator.t) == Float32
4145
end
4246

47+
@testset "ThrustCalculator constructor and force projection" begin
48+
@test_throws UndefKeywordError ThrustCalculator(system64, semi64)
49+
50+
calculator = ThrustCalculator(system64, semi64; direction=SVector(1.0, 0.0))
51+
@test calculator.system_index == 1
52+
@test calculator.thrust == 0.0
53+
@test calculator.dv isa Array{Float64, 2}
54+
@test size(calculator.dv) == (2, 4)
55+
@test calculator.eachparticle == eachparticle(system64)
56+
@test calculator.direction == SVector(1.0, 0.0)
57+
@test calculated_thrust(calculator) == 0.0
58+
59+
calculator = ThrustCalculator(system32, semi32; direction=(0.0, 2.0),
60+
eachparticle=2:3)
61+
@test eltype(calculator.thrust) == Float32
62+
@test calculator.direction == SVector(0.0f0, 1.0f0)
63+
@test calculator.eachparticle == 2:3
64+
65+
@test_throws ArgumentError ThrustCalculator(system64, semi64; direction=(0.0, 0.0))
66+
67+
dv = [2.0 -1.0 0.0 3.0
68+
4.0 5.0 -2.0 1.0]
69+
@test TrixiParticles.projected_force(dv, system64, eachparticle(system64),
70+
SVector(1.0, 0.0)) == 12.0
71+
@test TrixiParticles.projected_force(dv, system64, 2:3,
72+
SVector(0.0, 1.0)) == 4.0
73+
74+
TrixiParticles.reset!(calculator)
75+
@test calculated_thrust(calculator) == 0.0f0
76+
end
77+
78+
@testset "ThrustCalculator FSI force" begin
79+
particle_spacing = 1.0
80+
smoothing_kernel = SchoenbergCubicSplineKernel{2}()
81+
smoothing_length = 1.0
82+
fluid_density = 1000.0
83+
structure_density = 2000.0
84+
particle_volume = particle_spacing^2
85+
86+
state_equation = StateEquationCole(sound_speed=10.0,
87+
reference_density=fluid_density,
88+
exponent=1.0)
89+
90+
fluid_ic = InitialCondition(; coordinates=reshape([0.0, 0.0], 2, 1),
91+
velocity=zeros(2, 1),
92+
mass=[particle_volume * fluid_density],
93+
density=[fluid_density], particle_spacing)
94+
95+
fluid_system = WeaklyCompressibleSPHSystem(fluid_ic; smoothing_kernel,
96+
smoothing_length,
97+
density_calculator=SummationDensity(),
98+
state_equation,
99+
reference_particle_spacing=particle_spacing)
100+
101+
structure_coordinates = reshape([1.5, 0.0], 2, 1)
102+
structure_ic = InitialCondition(; coordinates=structure_coordinates,
103+
velocity=zeros(2, 1),
104+
mass=[particle_volume * structure_density],
105+
density=[structure_density], particle_spacing)
106+
107+
boundary_model = BoundaryModelDummyParticles([fluid_density],
108+
[particle_volume * fluid_density],
109+
AdamiPressureExtrapolation(),
110+
smoothing_kernel, smoothing_length;
111+
state_equation,
112+
reference_particle_spacing=particle_spacing)
113+
114+
structure_system = TotalLagrangianSPHSystem(structure_ic; smoothing_kernel,
115+
smoothing_length,
116+
young_modulus=1.0e5,
117+
poisson_ratio=0.3,
118+
boundary_model)
119+
120+
semi_ = Semidiscretization(fluid_system, structure_system)
121+
ode = semidiscretize(semi_, (0.0, 0.01))
122+
semi = ode.p.semi
123+
124+
v_ode, u_ode = ode.u0.x
125+
dv_ode = zero(v_ode)
126+
TrixiParticles.kick!(dv_ode, v_ode, u_ode, ode.p, 0.0)
127+
128+
fluid = semi.systems[1]
129+
structure = semi.systems[2]
130+
dv_fluid = TrixiParticles.wrap_v(dv_ode, fluid, semi)
131+
132+
thrust = ThrustCalculator(structure, semi; direction=SVector(1.0, 0.0))
133+
thrust(structure, dv_ode, nothing, v_ode, u_ode, semi, 0.0)
134+
135+
expected_force = -fluid.mass[1] * dv_fluid[1, 1]
136+
@test !iszero(expected_force)
137+
@test isapprox(calculated_thrust(thrust), expected_force;
138+
rtol=sqrt(eps()), atol=sqrt(eps()))
139+
end
140+
43141
@testset "update_mechanical_work_calculator!" begin
44142
# In the first test, we just move the 2x2 grid of particles up against gravity
45143
# and test that the accumulated work is just the potential energy difference.

test/examples/examples.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,9 @@
207207
mechanical_work1 = MechanicalWorkCalculator(tlsph_system_new, semi)
208208
mechanical_work2 = MechanicalWorkCalculator(tlsph_system_new, semi;
209209
only_compute_force_on_fluid=true)
210+
thrust = ThrustCalculator(tlsph_system_new, semi; direction=SVector(0.0, 1.0))
210211
postprocess_callback = PostprocessCallback(; interval=1, mechanical_work1,
211-
mechanical_work2,
212+
mechanical_work2, thrust,
212213
write_file_interval=0)
213214

214215
sol = @trixi_test_nowarn solve(ode, RDPK3SpFSAL35(), save_everystep=false,
@@ -233,6 +234,7 @@
233234
@test isapprox(calculated_mechanical_work(mechanical_work2),
234235
expected_energy_fluid,
235236
rtol=5e-4)
237+
@test isfinite(calculated_thrust(thrust))
236238
end
237239

238240
@trixi_testset "fsi/falling_water_column_2d.jl" begin

0 commit comments

Comments
 (0)