Skip to content

Commit 189c13b

Browse files
committed
Add ThrustCalculator
1 parent 1ac2a49 commit 189c13b

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
@@ -213,3 +213,127 @@ function compute_structure_acceleration!(dv, system, eachparticle,
213213

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