Skip to content

Commit 0ec9cb8

Browse files
author
Sherin Joseph
committed
Add Carreau-Yasuda Poiseuille validation
1 parent 4099559 commit 0ec9cb8

4 files changed

Lines changed: 724 additions & 0 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
using TrixiParticles
2+
using OrdinaryDiffEqLowStorageRK
3+
4+
# ==========================================================================================
5+
# 2D Periodic Poiseuille Flow with Carreau-Yasuda Viscosity
6+
#
7+
# This example simulates pressure-driven channel flow with the
8+
# `ViscosityCarreauYasuda` non-Newtonian viscosity model. The driving pressure
9+
# gradient is represented by an equivalent body acceleration.
10+
# ==========================================================================================
11+
12+
# ==========================================================================================
13+
# ==== Resolution
14+
ny = 50
15+
16+
# ==========================================================================================
17+
# ==== Experiment Setup
18+
t_end_factor = 0.1
19+
eps_factor = 1.0
20+
sound_speed_factor = 60.0
21+
initial_condition_mode = "analytical"
22+
power_law_index = 1.0
23+
24+
channel_height = 1.0
25+
channel_length = 6.0 * channel_height
26+
particle_spacing = channel_height / ny
27+
boundary_layers = 5
28+
29+
fluid_density = 1000.0
30+
nu0 = 1.0e-3
31+
nu_inf = 0.0
32+
lambda_exponent = 2.0
33+
reynolds_number = 200.0
34+
35+
reference_velocity = reynolds_number * nu0 / channel_height
36+
pressure_gradient = 8.0 * fluid_density * reference_velocity^2 /
37+
(reynolds_number * channel_height)
38+
acceleration_x = pressure_gradient / fluid_density
39+
carreau_time_constant = channel_height / max(reference_velocity, eps())
40+
41+
t_end = t_end_factor * channel_height / max(reference_velocity, eps())
42+
tspan = (0.0, t_end)
43+
44+
if !(initial_condition_mode in ("newtonian", "analytical", "zero"))
45+
throw(ArgumentError("initial condition mode must be \"newtonian\", " *
46+
"\"analytical\", or \"zero\""))
47+
end
48+
49+
# ==========================================================================================
50+
# ==== Analytical Solution
51+
function linear_interpolation_clamped(x, y, interpolation_point)
52+
interpolation_point <= first(x) && return first(y)
53+
interpolation_point >= last(x) && return last(y)
54+
55+
i = searchsortedlast(x, interpolation_point)
56+
x0, x1 = x[i], x[i + 1]
57+
y0, y1 = y[i], y[i + 1]
58+
return y0 + (y1 - y0) * (interpolation_point - x0) / (x1 - x0)
59+
end
60+
61+
function carreau_yasuda_kinematic_viscosity(shear_rate, nu0, nu_inf,
62+
time_constant, lambda_exponent,
63+
power_law_index)
64+
return nu_inf + (nu0 - nu_inf) *
65+
(1.0 + (time_constant * shear_rate)^lambda_exponent)^((power_law_index -
66+
1.0) /
67+
lambda_exponent)
68+
end
69+
70+
function solve_shear_rate_from_stress(shear_stress, density, nu0, nu_inf,
71+
time_constant, lambda_exponent,
72+
power_law_index)
73+
shear_stress <= 0 && return 0.0
74+
75+
residual(shear_rate) = density *
76+
carreau_yasuda_kinematic_viscosity(shear_rate, nu0,
77+
nu_inf,
78+
time_constant,
79+
lambda_exponent,
80+
power_law_index) *
81+
shear_rate - shear_stress
82+
83+
lower = 0.0
84+
upper = 1.0
85+
while residual(upper) < 0.0
86+
upper *= 2.0
87+
upper > 1.0e12 &&
88+
error("failed to bracket shear-rate root for shear stress $shear_stress")
89+
end
90+
91+
for _ in 1:120
92+
middle = 0.5 * (lower + upper)
93+
residual_middle = residual(middle)
94+
95+
if abs(residual_middle) <= 1.0e-12 * max(shear_stress, 1.0)
96+
return middle
97+
elseif residual_middle > 0
98+
upper = middle
99+
else
100+
lower = middle
101+
end
102+
end
103+
104+
return 0.5 * (lower + upper)
105+
end
106+
107+
function analytical_ux_profile(y_positions, power_law_index, channel_height,
108+
density, nu0, nu_inf, time_constant,
109+
lambda_exponent, pressure_gradient)
110+
distances_to_centerline = sort(unique(abs.(y_positions .- 0.5 * channel_height)))
111+
shear_rates = similar(distances_to_centerline)
112+
113+
for i in eachindex(distances_to_centerline)
114+
shear_stress = pressure_gradient * distances_to_centerline[i]
115+
shear_rates[i] = solve_shear_rate_from_stress(shear_stress, density, nu0,
116+
nu_inf, time_constant,
117+
lambda_exponent,
118+
power_law_index)
119+
end
120+
121+
velocity_at_distance = zeros(length(distances_to_centerline))
122+
for i in (lastindex(distances_to_centerline) - 1):-1:firstindex(distances_to_centerline)
123+
ds = distances_to_centerline[i + 1] - distances_to_centerline[i]
124+
velocity_at_distance[i] = velocity_at_distance[i + 1] +
125+
0.5 * (shear_rates[i + 1] + shear_rates[i]) * ds
126+
end
127+
128+
velocity = Vector{Float64}(undef, length(y_positions))
129+
for (i, y) in pairs(y_positions)
130+
distance_to_centerline = abs(y - 0.5 * channel_height)
131+
velocity[i] = linear_interpolation_clamped(distances_to_centerline,
132+
velocity_at_distance,
133+
distance_to_centerline)
134+
end
135+
136+
return velocity
137+
end
138+
139+
function newtonian_ux(y, channel_height, density, nu0, pressure_gradient)
140+
return pressure_gradient / (2.0 * density * nu0) * y * (channel_height - y)
141+
end
142+
143+
function l2_velocity_error(system::TrixiParticles.AbstractFluidSystem,
144+
dv_ode, du_ode, v_ode, u_ode, semi, t)
145+
v = TrixiParticles.wrap_v(v_ode, system, semi)
146+
u = TrixiParticles.wrap_u(u_ode, system, semi)
147+
148+
y_positions = [TrixiParticles.current_coords(u, system, particle)[2]
149+
for particle in TrixiParticles.eachparticle(system)]
150+
analytical_velocity = analytical_ux_profile(y_positions, power_law_index,
151+
channel_height, fluid_density, nu0,
152+
nu_inf, carreau_time_constant,
153+
lambda_exponent, pressure_gradient)
154+
155+
squared_error = 0.0
156+
squared_reference = 0.0
157+
for (i, particle) in enumerate(TrixiParticles.eachparticle(system))
158+
ux = TrixiParticles.current_velocity(v, system, particle)[1]
159+
squared_error += (ux - analytical_velocity[i])^2
160+
squared_reference += analytical_velocity[i]^2
161+
end
162+
163+
return sqrt(squared_error / nparticles(system)) /
164+
(sqrt(squared_reference / nparticles(system)) + eps())
165+
end
166+
l2_velocity_error(system, dv_ode, du_ode, v_ode, u_ode, semi, t) = nothing
167+
168+
function max_velocity_error(system::TrixiParticles.AbstractFluidSystem,
169+
dv_ode, du_ode, v_ode, u_ode, semi, t)
170+
v = TrixiParticles.wrap_v(v_ode, system, semi)
171+
u = TrixiParticles.wrap_u(u_ode, system, semi)
172+
173+
y_positions = [TrixiParticles.current_coords(u, system, particle)[2]
174+
for particle in TrixiParticles.eachparticle(system)]
175+
analytical_velocity = analytical_ux_profile(y_positions, power_law_index,
176+
channel_height, fluid_density, nu0,
177+
nu_inf, carreau_time_constant,
178+
lambda_exponent, pressure_gradient)
179+
180+
error = 0.0
181+
for (i, particle) in enumerate(TrixiParticles.eachparticle(system))
182+
ux = TrixiParticles.current_velocity(v, system, particle)[1]
183+
error = max(error, abs(ux - analytical_velocity[i]))
184+
end
185+
186+
return error
187+
end
188+
max_velocity_error(system, dv_ode, du_ode, v_ode, u_ode, semi, t) = nothing
189+
190+
# ==========================================================================================
191+
# ==== Initial Condition
192+
initial_velocity = if initial_condition_mode == "zero"
193+
(0.0, 0.0)
194+
elseif initial_condition_mode == "analytical"
195+
y_reference = collect(range(0.0, channel_height; length=4 * ny + 1))
196+
ux_reference = analytical_ux_profile(y_reference, power_law_index,
197+
channel_height, fluid_density, nu0,
198+
nu_inf, carreau_time_constant,
199+
lambda_exponent, pressure_gradient)
200+
x -> (linear_interpolation_clamped(y_reference, ux_reference, x[2]), 0.0)
201+
else
202+
x -> (newtonian_ux(x[2], channel_height, fluid_density, nu0,
203+
pressure_gradient), 0.0)
204+
end
205+
206+
# ==========================================================================================
207+
# ==== Fluid
208+
tank = RectangularTank(particle_spacing, (channel_length, channel_height),
209+
(channel_length, channel_height), fluid_density;
210+
n_layers=boundary_layers,
211+
faces=(false, false, true, true),
212+
velocity=initial_velocity,
213+
coordinates_eltype=Float64)
214+
215+
smoothing_length = 1.2 * particle_spacing
216+
smoothing_kernel = SchoenbergCubicSplineKernel{2}()
217+
218+
sound_speed = sound_speed_factor * reference_velocity
219+
state_equation = StateEquationCole(; sound_speed,
220+
reference_density=fluid_density,
221+
exponent=7)
222+
223+
viscosity = ViscosityCarreauYasuda(; nu0, nu_inf,
224+
lambda=carreau_time_constant,
225+
a=lambda_exponent,
226+
n=power_law_index,
227+
epsilon=max(0.5, eps_factor) * particle_spacing)
228+
229+
fluid_system = WeaklyCompressibleSPHSystem(tank.fluid;
230+
density_calculator=ContinuityDensity(),
231+
state_equation,
232+
smoothing_kernel,
233+
smoothing_length,
234+
acceleration=(acceleration_x, 0.0),
235+
viscosity,
236+
shifting_technique=nothing)
237+
238+
# ==========================================================================================
239+
# ==== Boundary
240+
boundary_model = BoundaryModelDummyParticles(tank.boundary.density,
241+
tank.boundary.mass,
242+
AdamiPressureExtrapolation(),
243+
smoothing_kernel,
244+
smoothing_length;
245+
state_equation,
246+
viscosity)
247+
248+
boundary_system = WallBoundarySystem(tank.boundary, boundary_model)
249+
250+
# ==========================================================================================
251+
# ==== Simulation
252+
periodic_box = PeriodicBox(min_corner=[0.0, -10.0 * channel_height],
253+
max_corner=[channel_length, 10.0 * channel_height])
254+
neighborhood_search = GridNeighborhoodSearch{2}(; periodic_box)
255+
256+
semi = Semidiscretization(fluid_system, boundary_system;
257+
neighborhood_search,
258+
parallelization_backend=PolyesterBackend())
259+
260+
ode = semidiscretize(semi, tspan)
261+
262+
n_label = replace(string(power_law_index), "." => "p")
263+
output_directory = joinpath("out_poiseuille_carreau", "n_$power_law_index")
264+
result_filename = "validation_run_poiseuille_carreau_2d_n_$(n_label)_ny_$ny"
265+
266+
info_callback = InfoCallback(interval=200)
267+
saving_callback = SolutionSavingCallback(; dt=t_end / 20,
268+
prefix="",
269+
output_directory)
270+
pp_callback = PostprocessCallback(; dt=t_end / 20,
271+
output_directory,
272+
filename=result_filename,
273+
l2_velocity_error,
274+
max_velocity_error,
275+
write_csv=true)
276+
cfl_callback = StepsizeCallback(cfl=0.2)
277+
callbacks = CallbackSet(info_callback, saving_callback, pp_callback,
278+
cfl_callback, UpdateCallback())
279+
280+
sol = solve(ode, RDPK3SpFSAL35();
281+
abstol=1.0e-7,
282+
reltol=1.0e-4,
283+
save_everystep=false,
284+
callback=callbacks,
285+
maxiters=2_000_000);
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# 2D Poiseuille Flow with Carreau-Yasuda Viscosity
2+
3+
This folder contains a periodic 2D Poiseuille validation case for the
4+
`ViscosityCarreauYasuda` non-Newtonian viscosity model in TrixiParticles.jl.
5+
6+
The case checks whether the numerical solution approaches the steady
7+
one-dimensional Carreau-Yasuda velocity profile in a pressure-driven channel.
8+
It also includes `n = 1`, which recovers the Newtonian parabolic Poiseuille
9+
profile. The setup is inspired by the Poiseuille validation in section 3.1 of
10+
Coclite et al. (2020), but uses WCSPH with an equivalent body acceleration
11+
instead of the MRT-LBM pressure-gradient implementation used there.
12+
13+
## Files
14+
15+
- `../../examples/fluid/poiseuille_carreau_2d.jl`: reusable example setup for a
16+
single Carreau-Yasuda power-law index.
17+
- `validation_poiseuille_carreau_2d.jl`: validation runner. It runs one or more
18+
Carreau-Yasuda power-law indices, writes VTU output plus error histories, and
19+
checks the final relative L2 errors against validation bounds.
20+
- `plot_carreau_comparison.jl`: helper script for manual inspection of the
21+
saved VTU profiles and error trends.
22+
23+
## Setup
24+
25+
- Channel height: `H = 1.0`
26+
- Channel length: `L = 6H`
27+
- Periodic direction: `x`
28+
- Solid walls: lower and upper `y` boundaries
29+
- Reference density: `rho0 = 1000.0`
30+
- Zero-shear kinematic viscosity: `nu0 = 1.0e-3`
31+
- Infinite-shear kinematic viscosity: `nu_inf = 0.0`
32+
- Yasuda transition parameter: `a = 2.0`
33+
- Default power-law indices: `n = (1.0, 1.5, 0.5, 0.25)`
34+
35+
## Analytical Profile
36+
37+
For each `n`, the script computes the steady profile by solving the implicit
38+
Carreau-Yasuda stress relation
39+
40+
```text
41+
tau(y) = rho0 * nu(gammadot) * gammadot
42+
```
43+
44+
where `tau(y) = dpdx * abs(y - H / 2)`. The resulting shear-rate profile is
45+
integrated from the wall to the centerline to obtain `u_x(y)`.
46+
47+
The validation records two quantities with `PostprocessCallback`:
48+
49+
- relative L2 velocity error against the analytical profile
50+
- maximum absolute velocity error against the analytical profile
51+
52+
## Running
53+
54+
Default run:
55+
56+
```bash
57+
julia --project=. validation/poiseuille_carreau_2d/validation_poiseuille_carreau_2d.jl
58+
```
59+
60+
The default uses `ny = 50`, `t_end_factor = 0.1`, and analytical initial
61+
conditions so it can be run quickly while still checking the numerical profile
62+
against the analytical solution.
63+
64+
High-resolution run matching the paper-inspired channel resolution:
65+
66+
```bash
67+
julia --project=. validation/poiseuille_carreau_2d/validation_poiseuille_carreau_2d.jl 200 0.02 1.0 60.0 analytical 0.25,0.5,1.0,1.5
68+
```
69+
70+
Optional command line arguments are:
71+
72+
```text
73+
ny t_end_factor eps_factor sound_speed_factor initial_condition_mode n_values
74+
```
75+
76+
where `initial_condition_mode` is one of `newtonian`, `analytical`, or `zero`,
77+
and `n_values` is a comma-separated list such as `1.0,0.5,0.25`.
78+
79+
Results are written to:
80+
81+
```text
82+
out_poiseuille_carreau/n_<n>/
83+
```
84+
85+
Each case contains `fluid_1.pvd`/VTU output and
86+
`validation_run_poiseuille_carreau_2d_*.csv/.json` error histories.
87+
88+
The validation runner checks the final relative L2 error with the following
89+
bounds:
90+
91+
```text
92+
n = 0.25: relative L2 <= 0.06
93+
n = 0.5: relative L2 <= 0.06
94+
n = 1.0: relative L2 <= 0.06
95+
n = 1.5: relative L2 <= 0.06
96+
```
97+
98+
The relative L2 error is the main quantitative comparison metric. The maximum
99+
absolute error is also written to the CSV/JSON files, but it should not be
100+
compared directly across different `n` values without normalization because the
101+
velocity scale changes strongly for shear-thinning cases such as `n = 0.25`.
102+
103+
## Plotting
104+
105+
After a run, create comparison plots with:
106+
107+
```bash
108+
julia --project=. -e 'push!(LOAD_PATH, "test"); append!(ARGS, ["out_poiseuille_carreau"]); include("validation/poiseuille_carreau_2d/plot_carreau_comparison.jl")'
109+
```
110+
111+
The plotting helper uses optional plotting packages that are available through
112+
the test environment. They are not runtime dependencies of TrixiParticles.jl.

0 commit comments

Comments
 (0)