Skip to content

Commit 562a22c

Browse files
authored
Merge pull request #125 from jbisits/onflycalculations
Move density calculation to while model is running + inferred vertical diffusivity calculation
2 parents cf672f7 + c168ee5 commit 562a22c

File tree

8 files changed

+238
-36
lines changed

8 files changed

+238
-36
lines changed

examples/twolayer_example.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ using TwoLayerDirectNumericalShenanigans
33

44
architecture = CPU() # or GPU()
55
diffusivities == 1e-4, κ = (S = 1e-5, T = 1e-5))
6+
resolution = (Nx = 10, Ny = 10, Nz = 100)
67

78
## Setup the model
8-
model = DNS(architecture, DOMAIN_EXTENT, HIGH_RESOLUTION, diffusivities;
9+
model = DNS(architecture, DOMAIN_EXTENT, resolution, diffusivities;
910
reference_density = REFERENCE_DENSITY)
1011

1112
## set initial conditions
@@ -18,17 +19,17 @@ profile_function = StepChange(transition_depth)#HyperbolicTangent(INTERFACE_LOCA
1819
tracer_perturbation_depth = find_depth(model, INTERFACE_LOCATION / 2)
1920
tracer_perturbation = SalinityGaussianProfile(tracer_perturbation_depth, 0.0, 1.5)
2021
noise_depth = find_depth(model, INTERFACE_LOCATION)
21-
initial_noise = SalinityNoise(noise_depth, 0.01)
22+
initial_noise = SalinityNoise(noise_depth, 1.0)
2223

23-
dns = TwoLayerDNS(model, profile_function, initial_conditions)
24+
dns = TwoLayerDNS(model, profile_function, initial_conditions; initial_noise)
2425

2526
set_two_layer_initial_conditions!(dns)
2627

2728
## build the simulation
2829
Δt = 1e-4
2930
stop_time = 60
3031
save_schedule = 0.5 # seconds
31-
simulation = DNS_simulation_setup(dns, Δt, stop_time, save_schedule)
32+
simulation = DNS_simulation_setup(dns, Δt, stop_time, save_schedule, save_velocities = true)
3233

3334
## Run the simulation
3435
run!(simulation)

src/TwoLayerDirectNumericalShenanigans.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module TwoLayerDirectNumericalShenanigans
22

33
using Oceananigans, Printf, Reexport, JLD2, Rasters, NCDatasets, GibbsSeaWater
4+
using Oceananigans: AbstractModel, Operators.ℑzᵃᵃᶠ
45
using SeawaterPolynomials: TEOS10EquationOfState
5-
using Oceananigans: AbstractModel
66
using SpecialFunctions: erf
77
using Oceanostics: KineticEnergyDissipationRate
88
using OceanRasterConversions: get_σₚ
@@ -61,6 +61,7 @@ include("initialconditions.jl")
6161
include("continuousprofilefunctions.jl")
6262
include("stepchangeprofilefunction.jl")
6363
include("tracerperturbations.jl")
64+
include("kernelfunctions.jl")
6465
include("twolayerdns.jl")
6566
include("noiseperturbations.jl")
6667
include("set_initialconditions.jl")

src/kernelfunctions.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
C(i, j, k, grid, C)
3+
Get tracer `C` values for use in other function. There may be another way to do this for
4+
`KernelFunctionOperation`s but I have not found it so will use this for now. **Note:** this
5+
return the value of the tracer with no interpolation so if the tracer `C` is at
6+
`(Center, Center, Center)` the value extracted will be at (Center, Center, Center)`.
7+
"""
8+
C(i, j, k, grid, C) = C[i, j, k]
9+
"""
10+
σ(i, j, k, grid, model, reference_pressure)
11+
Compute potential density `σ` at `reference_pressure` from salinity and temperature tracers
12+
in `model`.
13+
"""
14+
σ(i, j, k, grid, model, reference_pressure) = gsw_rho(C(i, j, k, grid, model.tracers.S),
15+
C(i, j, k, grid, model.tracers.T),
16+
reference_pressure)
17+
"""
18+
DensityField(model, reference_pressure)
19+
Return an `KernelFunctionOperation` at `(Center, Center, Center)` that computes the
20+
potential density from the salinity and temperature tracers in `model` at `reference_pressure`.
21+
"""
22+
DensityField(model, reference_pressure) =
23+
KernelFunctionOperation{Center, Center, Center}(σ, model.grid, model,
24+
reference_pressure)
25+
"""
26+
σ_anomalyᶜᶜᶠ(i, j, k, grid, model, reference_pressure)
27+
Compute potential density anomaly at `reference_pressure` from salinity and temperature tracers
28+
in `model` that have been interpolated from `(Center, Center, Center)` to
29+
`(Center, Center, Face)`.
30+
"""
31+
σ_anomalyᶜᶜᶠ(i, j, k, grid, model, reference_pressure, σ_reference) =
32+
gsw_rho(ℑzᵃᵃᶠ(i, j, k, grid, model.tracers.S), ℑzᵃᵃᶠ(i, j, k, grid, model.tracers.T),
33+
reference_pressure) - σ_reference
34+
"""
35+
function InterpolatedDensityAnomaly(model, reference_pressure)
36+
Return a `KernelFunctionOperation` at `(Center, Center, Face)` to compute the potential
37+
density anomaly from the salinity and temperature tracers from `model` that have been
38+
interpolated from `(Center, Center, Center)` to `(Center, Center, Face)`
39+
"""
40+
function InterpolatedDensityAnomaly(model, reference_pressure)
41+
42+
σ_reference = model.buoyancy.model.equation_of_state.reference_density
43+
return KernelFunctionOperation{Center, Center, Face}(σ_anomalyᶜᶜᶠ,
44+
model.grid, model,
45+
reference_pressure,
46+
σ_reference)
47+
48+
end

src/output_utils.jl

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ function field_ts_timemean(field_ts::FieldTimeSeries)
127127

128128
end
129129
"""
130-
function predicted_maximum_density()
131-
Compute the predicted maximum density of water that can form along the mixing line.
130+
function predicted_maximum_density
131+
Compute the predicted maximum density of water that can form along the mixing line between
132+
the salinity and temperature of the upper and lower layers.
132133
"""
133134
function predicted_maximum_density!(simulation::Simulation, dns::TwoLayerDNS; reference_pressure = 0)
134135

@@ -137,8 +138,15 @@ function predicted_maximum_density!(simulation::Simulation, dns::TwoLayerDNS; re
137138
S_mix = range(Sᵘ, Sˡ, step = 0.000001)
138139
T_mix = @. Tᵘ + slope * (Sᵘ - S_mix)
139140
ρ_mix_max = maximum(gsw_rho.(S_mix, T_mix, reference_pressure))
140-
NCDataset(simulation.output_writers[:outputs].filepath, "a") do ds
141-
ds.attrib["Predicted maximum density"] = ρ_mix_max
141+
file_type = find_file_type(simulation.output_writers[:outputs].filepath)
142+
if isequal(file_type, ".nc")
143+
NCDataset(simulation.output_writers[:outputs].filepath, "a") do ds
144+
ds.attrib["Predicted maximum density"] = ρ_mix_max
145+
end
146+
elseif isequal(file_type, ".jld2")
147+
jldopen(simulation.output_writers[:outputs].filepath, "a+") do f
148+
f["Predicted maximum density"] = ρ_mix_max
149+
end
142150
end
143151

144152
return nothing
@@ -208,15 +216,15 @@ function kolmogorov_and_batchelor_scale!(file::AbstractString)
208216
file_type = find_file_type(file)
209217
if isequal(file_type, ".nc")
210218

211-
ϵ = Raster(file, lazy = true, name = )
212-
ds = NCDataset(file, "a")
213-
ν_str = ds.attrib["ν"]
214-
ν = parse(Float64, ν_str[1:findfirst('m', ν_str)-1])
215-
Sc = ds.attrib["Sc"]
216-
η_min = minimum_η(ϵ; ν)
217-
ds.attrib["η (min)"] = η_min # minimum space and time Kolmogorov scale
218-
ds.attrib["λ_B"] = η_min / sqrt(Sc) # minimum space and time Batchelor scale
219-
close(ds)
219+
NCDataset(file, "a") do ds
220+
ϵ = Raster(file, lazy = true, name = )
221+
ν_str = ds.attrib["ν"]
222+
ν = parse(Float64, ν_str[1:findfirst('m', ν_str)-1])
223+
Sc = ds.attrib["Sc"]
224+
η_min = minimum_η(ϵ; ν)
225+
ds.attrib["η (min)"] = η_min # minimum space and time Kolmogorov scale
226+
ds.attrib["λ_B"] = η_min / sqrt(Sc) # minimum space and time Batchelor scale
227+
end
220228

221229
elseif isequal(file_type, ".jld2")
222230

@@ -234,6 +242,35 @@ function kolmogorov_and_batchelor_scale!(file::AbstractString)
234242

235243
return nothing
236244

245+
end
246+
"""
247+
function batchelor_scale!
248+
Compute and append the minimum space-time Batchelor scale and append it to save output.
249+
"""
250+
function batchelor_scale!(file::AbstractString)
251+
252+
file_type = find_file_type(file)
253+
if isequal(file_type, ".nc")
254+
255+
NCDataset(file, "a") do ds
256+
Sc = ds.attrib["Sc"]
257+
η_min = minimum(ds["η_space"][:])
258+
ds.attrib["λ_B"] = η_min / sqrt(Sc) # minimum space and time Batchelor scale
259+
end
260+
261+
elseif isequal(file_type, ".jld2")
262+
263+
Sc = load(file, "Non_dimensional_numbers")["Sc"]
264+
η = FieldTimeSeries(simulation.output_writers[:outputs].filepath, "η_space")
265+
min_η = minimum(η)
266+
jldopen(file, "a+") do f
267+
f["minimum_batchelor_scale"] = min_η / sqrt(Sc)
268+
end
269+
270+
end
271+
272+
return nothing
273+
237274
end
238275
"""
239276
function non_dimensional_numbers(simulation::Simulation, dns::TwoLayerDNS)
@@ -263,16 +300,16 @@ function non_dimensional_numbers!(simulation::Simulation, dns::TwoLayerDNS)
263300

264301
if simulation.output_writers[:outputs] isa NetCDFOutputWriter
265302

266-
ds = NCDataset(simulation.output_writers[:outputs].filepath, "a")
267-
ds.attrib["EOS"] = summary(model.buoyancy.model.equation_of_state.seawater_polynomial)
268-
ds.attrib["Reference density"] = "$(model.buoyancy.model.equation_of_state.reference_density)kgm⁻³"
269-
ds.attrib["ν"] = "$(model.closure.ν) m²s⁻¹"
270-
ds.attrib["κₛ"] = "$(model.closure.κ.S) m²s⁻¹"
271-
ds.attrib["κₜ"] = "$(model.closure.κ.T) m²s⁻¹"
272-
for key keys(nd_nums)
273-
ds.attrib[key] = nd_nums[key]
303+
NCDataset(simulation.output_writers[:outputs].filepath, "a") do ds
304+
ds.attrib["EOS"] = summary(model.buoyancy.model.equation_of_state.seawater_polynomial)
305+
ds.attrib["Reference density"] = "$(model.buoyancy.model.equation_of_state.reference_density)kgm⁻³"
306+
ds.attrib["ν"] = "$(model.closure.ν) m²s⁻¹"
307+
ds.attrib["κₛ"] = "$(model.closure.κ.S) m²s⁻¹"
308+
ds.attrib["κₜ"] = "$(model.closure.κ.T) m²s⁻¹"
309+
for key keys(nd_nums)
310+
ds.attrib[key] = nd_nums[key]
311+
end
274312
end
275-
close(ds)
276313

277314
else
278315

src/twolayerdns.jl

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,32 +151,69 @@ the course of a simulation;
151151
- `cfl` maximum cfl value used to determine the adaptive timestep size;
152152
- `diffusive_cfl` maximum diffusive cfl value used to determine the adaptive timestep size;
153153
- `max_change` maximum change in the timestep size;
154-
- `max_Δt` the maximum timestep.
154+
- `max_Δt` the maximum timestep;
155+
- `density_reference_pressure` for the seawater density calculation;
156+
- `save_velocities` defaults to `false`, if `true` model velocities will be saved to output.
155157
"""
156158
function DNS_simulation_setup(dns::TwoLayerDNS, Δt::Number,
157159
stop_time::Number, save_schedule::Number,
158160
output_writer::Symbol=:netcdf;
159161
cfl = 0.75,
160162
diffusive_cfl = 0.75,
161163
max_change = 1.2,
162-
max_Δt = 1e-1)
164+
max_Δt = 1e-1,
165+
density_reference_pressure = 0,
166+
save_velocities = false)
163167

164-
model, initial_conditions = dns.model, dns.initial_conditions
168+
model = dns.model
165169
simulation = Simulation(model; Δt, stop_time)
166170

167171
# time step adjustments
168172
wizard = TimeStepWizard(; cfl, diffusive_cfl, max_change, max_Δt)
169173
simulation.callbacks[:wizard] = Callback(wizard, IterationInterval(10))
170174

171-
# save output
175+
# model tracers
176+
S, T = model.tracers.S, model.tracers.T
177+
# custom saved output
178+
179+
# Density
180+
σ = DensityField(model, density_reference_pressure)
181+
182+
# Inferred vertical diffusivity
183+
σ_anomaly_interpolated = InterpolatedDensityAnomaly(model, density_reference_pressure)
184+
w = model.velocities.w
185+
κᵥ = Integral((-w * σ_anomaly_interpolated) / σ)
186+
187+
# Minimum in space Kolmogorov length scale
172188
ϵ = KineticEnergyDissipationRate(model)
173-
outputs = (S = model.tracers.S, T = model.tracers.T, ϵ = ϵ, w = model.velocities.w)
189+
η_space(model) = minimum(model.closure.ν ./ ϵ)
190+
191+
# Dimensions and attributes for custom saved output
192+
dims = Dict("η_space" => (), "σ" => ("xC", "xC", "zC"), "κᵥ" => ())
193+
oa = Dict(
194+
"σ" => Dict("longname" => "Seawater potential density calculated using TEOS-10 at $(density_reference_pressure)dbar",
195+
"units" => "kgm⁻³"),
196+
"η_space" => Dict("longname" => "Minimum (in space) Kolmogorov length"),
197+
"κᵥ" => Dict("longname" => "Inferred vertical diffusivity",
198+
"units" => "m²s⁻¹"))
199+
200+
# outputs to be saved during the simulation
201+
outputs = Dict("S" => S, "T" => T, "η_space" => η_space, "σ" => σ, "κᵥ" => κᵥ)
202+
if save_velocities
203+
u, v = model.velocities.u, model.velocities.v
204+
velocities = Dict("u" => u, "v" => v, "w" => w)
205+
merge!(outputs, velocities)
206+
end
207+
174208
filename = form_filename(dns, stop_time, output_writer)
175209
simulation.output_writers[:outputs] = output_writer == :netcdf ?
176210
NetCDFOutputWriter(model, outputs,
177211
filename = filename,
178212
schedule = TimeInterval(save_schedule),
179-
overwrite_existing = true) :
213+
overwrite_existing = true,
214+
dimensions = dims,
215+
output_attributes = oa
216+
) :
180217
JLD2OutputWriter(model, outputs,
181218
filename = filename,
182219
schedule = TimeInterval(save_schedule),
@@ -198,8 +235,6 @@ Create a filename for saved output based on the `profile_function`,`initial_cond
198235
"""
199236
function form_filename(dns::TwoLayerDNS, stop_time::Number, output_writer::Symbol)
200237

201-
pf_string = dns.profile_function isa HyperbolicTangent ? "tanh" :
202-
dns.profile_function isa Erf ? "erf" : "midpoint"
203238
pf_string = lowercase(string(typeof(dns.profile_function))[1:findfirst('{', string(typeof(dns.profile_function))) - 1])
204239
ic_type = typeof(dns.initial_conditions)
205240
ic_string = ic_type <: StableTwoLayerInitialConditions ? "stable" :

test/kernelfunction_test.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using TwoLayerDirectNumericalShenanigans: DensityField, InterpolatedDensityAnomaly
2+
using Oceananigans: Operators.ℑzᵃᵃᶠ
3+
using GibbsSeaWater
4+
5+
architecture = CPU()
6+
diffusivities == 1e-4, κ = (S = 1e-5, T = 1e-5))
7+
resolution = (Nx = 10, Ny = 10, Nz = 100)
8+
9+
model = DNS(architecture, DOMAIN_EXTENT, resolution, diffusivities;
10+
reference_density = REFERENCE_DENSITY)
11+
12+
z = znodes(model.grid, Center())
13+
14+
T₀ᵘ = -1.5
15+
S₀ᵘ = 34.568
16+
cabbeling = StableUpperLayerInitialConditions(S₀ᵘ, T₀ᵘ)
17+
initial_conditions = TwoLayerInitialConditions(cabbeling)
18+
transition_depth = find_depth(model, INTERFACE_LOCATION)
19+
profile_function = StepChange(transition_depth)
20+
21+
dns = TwoLayerDNS(model, profile_function, initial_conditions)
22+
23+
set_two_layer_initial_conditions!(dns)
24+
25+
reference_pressure = 0
26+
σ_field = Field(DensityField(dns.model, 0))
27+
compute!(σ_field)
28+
29+
function test_density_profile(σ_field, computed_density_profile)
30+
31+
vertical_slice = interior(σ_field, rand(1:10), rand(1:10), :)
32+
33+
return vertical_slice .== computed_density_profile
34+
35+
end
36+
37+
σ_anomaly_interp_field = Field(InterpolatedDensityAnomaly(dns.model, reference_pressure))
38+
compute!(σ_anomaly_interp_field)
39+
40+
function test_density_anom_profile(σ_anomaly_interp_field, computed_density_profile)
41+
42+
vertical_slice = interior(σ_anomaly_interp_field, rand(1:10), rand(1:10), :)
43+
44+
return vertical_slice .== computed_density_profile
45+
46+
end

test/runtests.jl

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using TwoLayerDirectNumericalShenanigans, Test
22
using TwoLayerDirectNumericalShenanigans: perturb_tracer
33

4-
include("twolayercontainers.jl")
4+
include("twolayercontainers_test.jl")
55

66
@testset "TwoLayerDNS upper layer containers" begin
77
for (i, uc) enumerate(upper_layer_containers)
@@ -128,3 +128,37 @@ end
128128
test_depth_range = vcat(z[10], z[20])
129129
@test isequal(z[10:20], find_depth(model, test_depth_range))
130130
end
131+
132+
include("kernelfunction_test.jl")
133+
134+
@testset "Density field computation" begin
135+
d_idx = findfirst(z .== transition_depth)
136+
σᵘ = gsw_rho.(interior(model.tracers.S, rand(1:10), rand(1:10), 1:d_idx),
137+
interior(model.tracers.T, rand(1:10), rand(1:10), 1:d_idx),
138+
reference_pressure)
139+
σˡ = gsw_rho.(interior(model.tracers.S, rand(1:10), rand(1:10), d_idx+1:100),
140+
interior(model.tracers.T, rand(1:10), rand(1:10), d_idx+1:100),
141+
reference_pressure)
142+
σ_computed_profile = vcat(σᵘ, σˡ)
143+
@test isequal(trues(length(z)), test_density_profile(σ_field, σ_computed_profile))
144+
end
145+
146+
@testset "Interpolated density anomaly" begin
147+
d_idx = findfirst(z .== transition_depth)
148+
S_interpolation = Field(KernelFunctionOperation{Center, Center, Face}(ℑzᵃᵃᶠ, model.grid,
149+
model.tracers.S))
150+
compute!(S_interpolation)
151+
T_interpolation = Field(KernelFunctionOperation{Center, Center, Face}(ℑzᵃᵃᶠ, model.grid,
152+
model.tracers.T))
153+
compute!(T_interpolation)
154+
σ_reference = dns.model.buoyancy.model.equation_of_state.reference_density
155+
σᵘ = gsw_rho.(interior(S_interpolation, rand(1:10), rand(1:10), 1:d_idx),
156+
interior(T_interpolation, rand(1:10), rand(1:10), 1:d_idx),
157+
reference_pressure) .- σ_reference
158+
σˡ = gsw_rho.(interior(S_interpolation, rand(1:10), rand(1:10), d_idx+1:101),
159+
interior(T_interpolation, rand(1:10), rand(1:10), d_idx+1:101),
160+
reference_pressure) .- σ_reference
161+
σ_anom_computed_profile = vcat(σᵘ, σˡ)
162+
@test isequal(trues(length(σ_anom_computed_profile)),
163+
test_density_anom_profile(σ_anomaly_interp_field, σ_anom_computed_profile))
164+
end

0 commit comments

Comments
 (0)