Skip to content

Commit 6552cfe

Browse files
authored
Merge pull request #4073 from CliMA/he/rft-vert-full-strain
rft: separate vertical and full strain
2 parents 560c329 + e5987ed commit 6552cfe

File tree

7 files changed

+190
-33
lines changed

7 files changed

+190
-33
lines changed

src/cache/diagnostic_edmf_precomputed_quantities.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,8 +1376,7 @@ NVTX.@annotate function set_diagnostic_edmf_precomputed_quantities_env_closures!
13761376
# TODO: Currently the shear production only includes vertical gradients
13771377
ᶠu = p.scratch.ᶠtemp_C123
13781378
@. ᶠu = C123(ᶠinterp(Y.c.uₕ)) + C123(ᶠu³)
1379-
ᶜstrain_rate = p.scratch.ᶜtemp_UVWxUVW
1380-
ᶜstrain_rate .= compute_strain_rate_center(ᶠu)
1379+
ᶜstrain_rate = compute_strain_rate_center_vertical(ᶠu)
13811380
@. ᶜstrain_rate_norm = norm_sqr(ᶜstrain_rate)
13821381

13831382
ρatke_flux_values = Fields.field_values(ρatke_flux)

src/cache/prognostic_edmf_precomputed_quantities.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,7 @@ NVTX.@annotate function set_prognostic_edmf_precomputed_quantities_explicit_clos
451451
# TODO: Currently the shear production only includes vertical gradients
452452
ᶠu = p.scratch.ᶠtemp_C123
453453
@. ᶠu = C123(ᶠinterp(Y.c.uₕ)) + C123(ᶠu³)
454-
ᶜstrain_rate = p.scratch.ᶜtemp_UVWxUVW
455-
ᶜstrain_rate .= compute_strain_rate_center(ᶠu)
454+
ᶜstrain_rate = compute_strain_rate_center_vertical(ᶠu)
456455
@. ᶜstrain_rate_norm = norm_sqr(ᶜstrain_rate)
457456

458457
ρatke_flux_values = Fields.field_values(ρatke_flux)

src/prognostic_equations/edmfx_sgs_flux.jl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,7 @@ function edmfx_sgs_diffusive_flux_tendency!(
499499
end
500500

501501
# Momentum diffusion
502-
ᶠstrain_rate = p.scratch.ᶠtemp_UVWxUVW
503-
ᶠstrain_rate .= compute_strain_rate_face(ᶜu⁰)
502+
ᶠstrain_rate = compute_strain_rate_face_vertical(ᶜu⁰)
504503
@. Yₜ.c.uₕ -= C12(ᶜdivᵥ(-(2 * ᶠρaK_u * ᶠstrain_rate)) / Y.c.ρ)
505504
end
506505
return nothing
@@ -625,8 +624,7 @@ function edmfx_sgs_diffusive_flux_tendency!(
625624
end
626625

627626
# Momentum diffusion
628-
ᶠstrain_rate = p.scratch.ᶠtemp_UVWxUVW
629-
ᶠstrain_rate .= compute_strain_rate_face(ᶜu)
627+
ᶠstrain_rate = compute_strain_rate_face_vertical(ᶜu)
630628
@. Yₜ.c.uₕ -= C12(ᶜdivᵥ(-(2 * ᶠρaK_u * ᶠstrain_rate)) / Y.c.ρ)
631629
end
632630

src/prognostic_equations/gm_sgs_closures.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ NVTX.@annotate function compute_gm_mixing_length(Y, p)
8484
# TODO: move strain rate calculation to separate function
8585
ᶠu = p.scratch.ᶠtemp_C123
8686
@. ᶠu = C123(ᶠinterp(Y.c.uₕ)) + C123(ᶠu³)
87-
ᶜstrain_rate = p.scratch.ᶜtemp_UVWxUVW
88-
ᶜstrain_rate .= compute_strain_rate_center(ᶠu)
87+
ᶜstrain_rate = compute_strain_rate_center_vertical(ᶠu)
8988
@. ᶜstrain_rate_norm = norm_sqr(ᶜstrain_rate)
9089

9190
ᶜprandtl_nvec = p.scratch.ᶜtemp_scalar_2

src/prognostic_equations/vertical_diffusion_boundary_layer.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ function vertical_diffusion_boundary_layer_tendency!(
104104
end
105105

106106
if !disable_momentum_vertical_diffusion(p.atmos.vertical_diffusion)
107-
ᶠstrain_rate = p.scratch.ᶠtemp_UVWxUVW
108-
ᶠstrain_rate .= compute_strain_rate_face(ᶜu)
107+
ᶠstrain_rate = compute_strain_rate_face_vertical(ᶜu)
109108
@. Yₜ.c.uₕ -= C12(
110109
ᶜdivᵥ(-2 * ᶠinterp(Y.c.ρ) * ᶠinterp(ᶜK_h) * ᶠstrain_rate) / Y.c.ρ,
111110
) # assumes ᶜK_u = ᶜK_h

src/utils/utilities.jl

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,43 +82,116 @@ state.
8282
compute_kinetic(Y::Fields.FieldVector) = compute_kinetic(Y.c.uₕ, Y.f.u₃)
8383

8484
"""
85-
ϵ .= compute_strain_rate_center(u::Field)
85+
ϵ .= compute_strain_rate_center_vertical(ᶠu)
8686
87-
Compute the strain_rate at cell centers from velocity at cell faces.
87+
Compute the strain rate at cell centers from velocity at cell faces, with vertical gradients only.
88+
89+
Returns a lazy representation of the strain rate tensor.
8890
"""
89-
function compute_strain_rate_center(u::Fields.Field)
90-
@assert eltype(u) <: C123
91+
function compute_strain_rate_center_vertical(ᶠu)
9192
axis_uvw = Geometry.UVWAxis()
9293
return @. lazy(
9394
(
94-
Geometry.project((axis_uvw,), ᶜgradᵥ(UVW(u))) +
95-
adjoint(Geometry.project((axis_uvw,), ᶜgradᵥ(UVW(u))))
95+
Geometry.project((axis_uvw,), ᶜgradᵥ(UVW(ᶠu))) +
96+
adjoint(Geometry.project((axis_uvw,), ᶜgradᵥ(UVW(ᶠu))))
9697
) / 2,
9798
)
9899
end
99100

100101
"""
101-
ϵ .= compute_strain_rate_face(u::Field)
102+
ϵ .= compute_strain_rate_face_vertical(ᶜu::Field)
103+
104+
Compute the strain rate at cell faces from velocity at cell centers, with vertical gradients only.
102105
103-
Compute the strain_rate at cell faces from velocity at cell centers.
106+
Returns a lazy representation of the strain rate tensor.
104107
"""
105-
function compute_strain_rate_face(u::Fields.Field)
106-
@assert eltype(u) <: C123
107-
∇ᵥuvw_boundary =
108-
Geometry.outer(Geometry.WVector(0), Geometry.UVWVector(0, 0, 0))
109-
ᶠgradᵥ = Operators.GradientC2F(
110-
bottom = Operators.SetGradient(∇ᵥuvw_boundary),
111-
top = Operators.SetGradient(∇ᵥuvw_boundary),
112-
)
108+
function compute_strain_rate_face_vertical(ᶜu)
109+
∇ᵥuvw_boundary = Geometry.outer(Geometry.WVector(0), Geometry.UVWVector(0, 0, 0))
110+
∇bc = Operators.SetGradient(∇ᵥuvw_boundary)
111+
ᶠgradᵥ = Operators.GradientC2F(bottom = ∇bc, top = ∇bc)
113112
axis_uvw = Geometry.UVWAxis()
114113
return @. lazy(
115114
(
116-
Geometry.project((axis_uvw,), ᶠgradᵥ(UVW(u))) +
117-
adjoint(Geometry.project((axis_uvw,), ᶠgradᵥ(UVW(u))))
115+
Geometry.project((axis_uvw,), ᶠgradᵥ(UVW(ᶜu))) +
116+
adjoint(Geometry.project((axis_uvw,), ᶠgradᵥ(UVW(ᶜu))))
118117
) / 2,
119118
)
120119
end
121120

121+
"""
122+
compute_strain_rate_center_full!(ᶜε, ᶜu, ᶠu)
123+
124+
Compute the full strain rate tensor at cell centers from velocity
125+
126+
# Arguments
127+
- `ᶜε`: Preallocated `UVW x UVW` tensor field for the strain rate at cell centers
128+
- `ᶜu, ᶠu`: Velocity field at cell centers and faces, respectively.
129+
Both reconstructions are needed to compute the full strain rate tensor.
130+
131+
See also [`compute_strain_rate_face_full!`](@ref) for the analogous calculation on cell faces.
132+
133+
# Notes:
134+
- it is recommended to use `ᶠu` and `ᶜu` as computed by
135+
[`set_velocity_quantities!`](@ref) and [`set_implicit_precomputed_quantities_part1!`](@ref)
136+
- Because the computation involves both vertical and horizontal gradients, this
137+
calculation cannot be lazified (for now). It requires a pre-allocated output field.
138+
"""
139+
function compute_strain_rate_center_full!(ᶜε, ᶜu, ᶠu)
140+
axis_uvw = (Geometry.UVWAxis(),)
141+
@. ᶜε = Geometry.project(axis_uvw, ᶜgradᵥ(UVW(ᶠu))) # vertical component
142+
@. ᶜε += Geometry.project(axis_uvw, gradₕ(UVW(ᶜu))) # horizontal component
143+
@. ᶜε = (ᶜε + adjoint(ᶜε)) / 2
144+
return ᶜε
145+
end
146+
147+
"""
148+
compute_strain_rate_face_full!(ᶠε, ᶜu, ᶠu)
149+
150+
Compute the full strain rate tensor at cell faces from velocity
151+
152+
# Arguments
153+
- `ᶠε`: Preallocated `UVW x UVW` tensor field for the strain rate at cell centers
154+
- `ᶜu, ᶠu`: Velocity field at cell centers and faces, respectively.
155+
Both reconstructions are needed to compute the full strain rate tensor.
156+
157+
See also [`compute_strain_rate_center_full!`](@ref) for the analogous calculation on cell centers.
158+
159+
# Notes:
160+
- it is recommended to use `ᶠu` and `ᶜu` as computed by
161+
[`set_velocity_quantities!`](@ref) and [`set_implicit_precomputed_quantities_part1!`](@ref)
162+
- Because the computation involves both vertical and horizontal gradients, this
163+
calculation cannot be lazified (for now). It requires a pre-allocated output field.
164+
- The calculation assumes zero vertical gradient boundary conditions
165+
"""
166+
function compute_strain_rate_face_full!(ᶠε, ᶜu, ᶠu)
167+
∇ᵥuvw_boundary = Geometry.outer(Geometry.WVector(0), UVW(0, 0, 0))
168+
∇bc = Operators.SetGradient(∇ᵥuvw_boundary)
169+
ᶠgradᵥ = Operators.GradientC2F(bottom = ∇bc, top = ∇bc)
170+
axis_uvw = (Geometry.UVWAxis(),)
171+
@. ᶠε = Geometry.project(axis_uvw, ᶠgradᵥ(UVW(ᶜu))) # vertical component
172+
@. ᶠε += Geometry.project(axis_uvw, gradₕ(UVW(ᶠu))) # horizontal component
173+
@. ᶠε = (ᶠε + adjoint(ᶠε)) / 2
174+
return ᶠε
175+
end
176+
177+
"""
178+
strain_rate_norm(S, axis = Geometry.UVWAxis())
179+
180+
Return a lazy representation of the strain rate norm `|S| = √(2 ∘ S : S)`
181+
182+
If `axis` is provided, project the strain rate tensor `S` onto the specified axis
183+
before computing the norm.
184+
185+
For example,
186+
- `axis = Geometry.UVAxis()` computes the horizontal strain rate norm, while
187+
- `axis = Geometry.WAxis()` computes the vertical strain rate norm.
188+
"""
189+
function strain_rate_norm(S, axis = Geometry.UVWAxis())
190+
S_proj = @. lazy(Geometry.project((axis,), S, (axis,)))
191+
S_norm = @. lazy(sqrt(2 * norm_sqr(S_proj)))
192+
return S_norm
193+
end
194+
122195
"""
123196
g³³_field(space)
124197

test/utilities.jl

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ end
8989
end
9090

9191
@testset "compute_strain_rate (c.f. analytical function)" begin
92-
# Test compute_strain_rate_face
92+
# Test compute_strain_rate_face_vertical
9393
(; helem, cent_space, face_space) = get_cartesian_spaces()
9494
ccoords, fcoords = get_coords(cent_space, face_space)
9595
FT = eltype(ccoords.x)
@@ -118,8 +118,8 @@ end
118118
ᶠu = @. UVW(Geometry.UVector(ᶠu)) +
119119
UVW(Geometry.VVector(ᶠv)) +
120120
UVW(Geometry.WVector(ᶠw))
121-
ᶜϵ .= CA.compute_strain_rate_center(Geometry.Covariant123Vector.(ᶠu))
122-
ᶠϵ .= CA.compute_strain_rate_face(Geometry.Covariant123Vector.(ᶜu))
121+
ᶜϵ .= CA.compute_strain_rate_center_vertical(Geometry.Covariant123Vector.(ᶠu))
122+
ᶠϵ .= CA.compute_strain_rate_face_vertical(Geometry.Covariant123Vector.(ᶜu))
123123

124124
# Center valued strain rate
125125
@test ᶜϵ.components.data.:1 == ᶜϵ.components.data.:1 .* FT(0)
@@ -163,6 +163,96 @@ end
163163
end
164164
end
165165

166+
@testset "compute_full_strain_rate (consistency and symmetry)" begin
167+
(; helem, cent_space, face_space) = get_cartesian_spaces()
168+
ccoords, fcoords = get_coords(cent_space, face_space)
169+
FT = eltype(ccoords.x)
170+
UVW = Geometry.UVWVector
171+
C123 = Geometry.Covariant123Vector
172+
UVec = Geometry.UVector
173+
VVec = Geometry.VVector
174+
WVec = Geometry.WVector
175+
ᶜgradᵥ = Operators.GradientF2C()
176+
gradₕ = Operators.Gradient()
177+
178+
179+
# Alloc scratch space for 3x3 tensor fields
180+
u₀ = UVW(FT(0), FT(0), FT(0))
181+
ᶜε = Fields.Field(typeof(u₀ * u₀'), cent_space)
182+
ᶠε = Fields.Field(typeof(u₀ * u₀'), face_space)
183+
ᶜε_ref = Fields.Field(typeof(u₀ * u₀'), cent_space)
184+
ᶠε_ref = Fields.Field(typeof(u₀ * u₀'), face_space)
185+
186+
# Velocity fields
187+
u, v, w = taylor_green_ic(ccoords)
188+
ᶠu, ᶠv, ᶠw = taylor_green_ic(fcoords)
189+
ᶜu = @. UVW(UVec(u)) + UVW(VVec(v)) + UVW(WVec(w))
190+
ᶠu_vec = @. UVW(UVec(ᶠu)) + UVW(VVec(ᶠv)) + UVW(WVec(ᶠw))
191+
192+
# Compute using API under test
193+
CA.compute_strain_rate_center_full!(ᶜε, ᶜu, ᶠu_vec)
194+
CA.compute_strain_rate_face_full!(ᶠε, ᶜu, ᶠu_vec)
195+
196+
# Build reference tensors explicitly (projection + symmetrization)
197+
axis_uvw = (Geometry.UVWAxis(),)
198+
# Center reference
199+
@. ᶜε_ref = Geometry.project(axis_uvw, ᶜgradᵥ(ᶠu_vec))
200+
@. ᶜε_ref += Geometry.project(axis_uvw, gradₕ(ᶜu))
201+
@. ᶜε_ref = (ᶜε_ref + adjoint(ᶜε_ref)) / 2
202+
203+
# Face reference (construct vertical gradient with the same BCs)
204+
∇ᵥuvw_boundary = Geometry.outer(WVec(0), UVW(0, 0, 0))
205+
∇bc = Operators.SetGradient(∇ᵥuvw_boundary)
206+
ᶠgradᵥ = Operators.GradientC2F(bottom = ∇bc, top = ∇bc)
207+
@. ᶠε_ref = Geometry.project(axis_uvw, ᶠgradᵥ(ᶜu))
208+
@. ᶠε_ref += Geometry.project(axis_uvw, gradₕ(ᶠu_vec))
209+
@. ᶠε_ref = (ᶠε_ref + adjoint(ᶠε_ref)) / 2
210+
211+
# Symmetry checks (independent of reference)
212+
@test ᶜε.components.data.:2 == ᶜε.components.data.:4
213+
@test ᶜε.components.data.:3 == ᶜε.components.data.:7
214+
@test ᶜε.components.data.:6 == ᶜε.components.data.:8
215+
@test ᶠε.components.data.:2 == ᶠε.components.data.:4
216+
@test ᶠε.components.data.:3 == ᶠε.components.data.:7
217+
@test ᶠε.components.data.:6 == ᶠε.components.data.:8
218+
219+
# Consistency with reference (component-wise, tight tolerance)
220+
tol = sqrt(eps(FT))
221+
@test maximum(abs, ᶜε.components.data.:1 .- ᶜε_ref.components.data.:1) < tol
222+
@test maximum(abs, ᶜε.components.data.:2 .- ᶜε_ref.components.data.:2) < tol
223+
@test maximum(abs, ᶜε.components.data.:3 .- ᶜε_ref.components.data.:3) < tol
224+
@test maximum(abs, ᶜε.components.data.:4 .- ᶜε_ref.components.data.:4) < tol
225+
@test maximum(abs, ᶜε.components.data.:5 .- ᶜε_ref.components.data.:5) < tol
226+
@test maximum(abs, ᶜε.components.data.:6 .- ᶜε_ref.components.data.:6) < tol
227+
@test maximum(abs, ᶜε.components.data.:7 .- ᶜε_ref.components.data.:7) < tol
228+
@test maximum(abs, ᶜε.components.data.:8 .- ᶜε_ref.components.data.:8) < tol
229+
@test maximum(abs, ᶜε.components.data.:9 .- ᶜε_ref.components.data.:9) < tol
230+
231+
@test maximum(abs, ᶠε.components.data.:1 .- ᶠε_ref.components.data.:1) < tol
232+
@test maximum(abs, ᶠε.components.data.:2 .- ᶠε_ref.components.data.:2) < tol
233+
@test maximum(abs, ᶠε.components.data.:3 .- ᶠε_ref.components.data.:3) < tol
234+
@test maximum(abs, ᶠε.components.data.:4 .- ᶠε_ref.components.data.:4) < tol
235+
@test maximum(abs, ᶠε.components.data.:5 .- ᶠε_ref.components.data.:5) < tol
236+
@test maximum(abs, ᶠε.components.data.:6 .- ᶠε_ref.components.data.:6) < tol
237+
@test maximum(abs, ᶠε.components.data.:7 .- ᶠε_ref.components.data.:7) < tol
238+
@test maximum(abs, ᶠε.components.data.:8 .- ᶠε_ref.components.data.:8) < tol
239+
@test maximum(abs, ᶠε.components.data.:9 .- ᶠε_ref.components.data.:9) < tol
240+
241+
# Boundary behavior (face): match reference at first and last vertical levels per element
242+
for elem_id in 1:helem
243+
@test maximum(
244+
abs,
245+
Fields.field_values(Fields.slab(ᶠε.components.data.:3, 1, elem_id)) .-
246+
Fields.field_values(Fields.slab(ᶠε_ref.components.data.:3, 1, elem_id)),
247+
) < tol
248+
@test maximum(
249+
abs,
250+
Fields.field_values(Fields.slab(ᶠε.components.data.:3, 11, elem_id)) .-
251+
Fields.field_values(Fields.slab(ᶠε_ref.components.data.:3, 11, elem_id)),
252+
) < tol
253+
end
254+
end
255+
166256
@testset "horizontal integral at boundary" begin
167257
# Test both `horizontal_integral_at_boundary` methods
168258
(; cent_space, face_space) = get_cartesian_spaces()

0 commit comments

Comments
 (0)