Skip to content

Commit 817e346

Browse files
authored
Merge pull request #4012 from CliMA/j/era5_warming_scm
Adds ability to run ERA5 simulations with temperature change
2 parents 43c9882 + aefe8d3 commit 817e346

File tree

5 files changed

+209
-9
lines changed

5 files changed

+209
-9
lines changed

config/default_configs/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,9 @@ site_latitude:
271271
site_longitude:
272272
help: "Site longitude for single column model. Used for externally driven time varying forcing model to generate the forcing file. Artifact support is currently for eastern Pacific region in July 2007 only. [`-149.0` (default)]"
273273
value: -149.0
274+
era5_diurnal_warming:
275+
help: Applies a warming scenario by adding a constant temperature offset (in Kelvin). Supported only by the `ReanalysisMonthlyAveragedDiurnal` forcing model. This affects the target surface temperature for the entire simulation, the initial atmospheric temperature profile, and the relaxation temperature profile above `gcmdriven_relaxation_minimum_height`. Specific humidity is increased to hold relative humidity constant in the initial and relaxation profiles.
276+
value: ~
274277
subsidence:
275278
help: "Subsidence [`nothing` (default), `Bomex`, `LifeCycleTan2018`, `Rico`, `DYCOMS`, `ISDAC`]"
276279
value: ~

src/solver/model_getters.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,10 @@ function get_external_forcing_model(parsed_args, ::Type{FT}) where {FT}
466466
@assert parsed_args["config"] == "column" "ReanalysisTimeVarying and ReanalysisMonthlyAveragedDiurnal are only supported in column mode."
467467
@assert all(reanalysis_required_fields .== "ReanalysisTimeVarying") "All of external_forcing, surface_setup, surface_temperature and initial_condition must be set to ReanalysisTimeVarying."
468468
end
469+
if !isnothing(parsed_args["era5_diurnal_warming"])
470+
@assert external_forcing == "ReanalysisMonthlyAveragedDiurnal" "era5_diurnal_warming is only supported for ReanalysisMonthlyAveragedDiurnal."
471+
@assert parsed_args["era5_diurnal_warming"] isa Number "era5_diurnal_warming is expected to be a number, but was supplied as a $(typeof(parsed_args["era5_diurnal_warming"]))"
472+
end
469473
return if isnothing(external_forcing)
470474
nothing
471475
elseif external_forcing == "GCM"

src/utils/era5_observations_to_forcing_file.jl

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ function get_external_monthly_forcing_file_path(
6969
),
7070
)
7171
start_date = parsed_args["start_date"]
72+
warming_amount = parsed_args["era5_diurnal_warming"]
73+
warming_amount_str =
74+
(warming_amount isa Number) ? "_plus_$(float(warming_amount))K" : ""
75+
7276
# round to era5 quarter degree resolution for site selection
7377
site_latitude = round(parsed_args["site_latitude"] * 4) / 4
7478
site_longitude = round(parsed_args["site_longitude"] * 4) / 4
@@ -79,7 +83,7 @@ function get_external_monthly_forcing_file_path(
7983
end
8084
return joinpath(
8185
data_dir,
82-
"monthly_diurnal_cycle_forcing_$(site_latitude)_$(site_longitude)_$(start_date).nc",
86+
"monthly_diurnal_cycle_forcing_$(site_latitude)_$(site_longitude)_$(start_date)$(warming_amount_str).nc",
8387
)
8488
end
8589

@@ -340,6 +344,40 @@ function generate_external_forcing_file(
340344
)
341345
end
342346

347+
warming_amount = parsed_args["era5_diurnal_warming"]
348+
if warming_amount isa Number
349+
# Get the relative humidity before warming; this will be used to scale the specific humidity after warming
350+
param_set = TD.Parameters.ThermodynamicsParameters(FT)
351+
phase_partition =
352+
TD.PhasePartition.(sim_forcing["hus"], sim_forcing["clw"], sim_forcing["cli"])
353+
# expand dimension and convert pressure levels from hPa (ERA5 raw) to Pa (needed for thermodynamics)
354+
p_expanded =
355+
FT.(
356+
repeat(
357+
sim_forcing["pressure_level"] .* 100,
358+
1,
359+
size(sim_forcing["hus"], 2),
360+
)
361+
)
362+
relative_humidity =
363+
TD.relative_humidity.(
364+
param_set,
365+
sim_forcing["ta"],
366+
p_expanded,
367+
TD.PhaseEquil{FT},
368+
phase_partition,
369+
)
370+
371+
# Warming the temperature
372+
sim_forcing["ta"] .+= warming_amount
373+
ρ = TD.air_density.(param_set, sim_forcing["ta"], p_expanded, phase_partition)
374+
375+
# Get the saturation specific humidity at the warmed temperature
376+
saturation_humidity_at_warming =
377+
TD.q_vap_saturation.(param_set, sim_forcing["ta"], ρ, TD.PhaseEquil{FT})
378+
sim_forcing["hus"] = saturation_humidity_at_warming .* relative_humidity
379+
end
380+
343381
sim_forcing["z"] =
344382
sim_forcing["zg"] / external_tv_params.gravitational_acceleration # height in meters
345383

@@ -501,6 +539,11 @@ function generate_external_forcing_file(
501539
smooth_amount = smooth_amount,
502540
)
503541

542+
# warm the surface temperature if warming amount is specified
543+
if warming_amount isa Number
544+
skt .+= warming_amount
545+
end
546+
504547
for time_ind in 1:ds.dim["time"]
505548
ds["coszen"][:, :, :, time_ind] .= coszen_list[time_ind][1]
506549
ds["rsdt"][:, :, :, time_ind] .= coszen_list[time_ind][2]
@@ -557,6 +600,7 @@ function generate_multiday_era5_external_forcing_file(
557600
"start_date" => Dates.format(dd, "yyyymmdd"),
558601
"site_latitude" => parsed_args["site_latitude"],
559602
"site_longitude" => parsed_args["site_longitude"],
603+
"era5_diurnal_warming" => parsed_args["era5_diurnal_warming"],
560604
)
561605
single_file_path = get_external_daily_forcing_file_path(
562606
single_parsed_args;

test/test_helpers.jl

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ function create_mock_era5_datasets(
180180
num_pressure = 37,
181181
base_date = "20000101",
182182
)
183+
params = CA.ClimaAtmosParameters(FT)
183184
column_data_path = joinpath(
184185
temporary_dir,
185186
"forcing_and_cloud_hourly_profiles_$(start_date).nc",
@@ -214,7 +215,7 @@ function create_mock_era5_datasets(
214215
tvforcing["latitude"][:] = collect(-2.0:lat_step:2.0)
215216
tvforcing["longitude"][:] = collect(-2.0:lon_step:2.0)
216217
tvforcing["pressure_level"][:] =
217-
10 .^ (range(1, stop = 4, length = num_pressure))
218+
10 .^ (range(1, stop = 3, length = num_pressure))
218219
# Time values are hours since base_date, not since this file's date
219220
tvforcing["valid_time"][:] =
220221
collect(hours_offset:(hours_offset + hours - 1))
@@ -234,11 +235,19 @@ function create_mock_era5_datasets(
234235
tvforcing["u"][:, :, :, :] .= ones(FT, size(tvforcing["u"]))
235236
tvforcing["v"][:, :, :, :] .= ones(FT, size(tvforcing["v"]))
236237
tvforcing["w"][:, :, :, :] .= ones(FT, size(tvforcing["w"]))
237-
tvforcing["t"][:, :, :, :] .= ones(FT, size(tvforcing["t"]))
238-
tvforcing["q"][:, :, :, :] .= ones(FT, size(tvforcing["q"]))
239-
tvforcing["z"][:, :, :, :] .= ones(FT, size(tvforcing["z"]))
240-
tvforcing["clwc"][:, :, :, :] .= ones(FT, size(tvforcing["clwc"]))
241-
tvforcing["ciwc"][:, :, :, :] .= ones(FT, size(tvforcing["ciwc"]))
238+
tvforcing["clwc"][:, :, :, :] .= zeros(FT, size(tvforcing["clwc"]))
239+
tvforcing["ciwc"][:, :, :, :] .= zeros(FT, size(tvforcing["ciwc"]))
240+
241+
tvforcing["z"] .= reshape(
242+
tvforcing["pressure_level"] .* 100,
243+
(1, 1, length(tvforcing["pressure_level"]), 1),
244+
)
245+
tvforcing["z"] .=
246+
geopotential_from_pressure(tvforcing["z"]; R_d = CA.Parameters.R_d(params))
247+
tvforcing["t"][:, :, :, :] .=
248+
temperature_from_geopotential(tvforcing["z"]; g = CA.Parameters.grav(params))
249+
tvforcing["q"][:, :, :, :] .=
250+
shum_from_geopotential(tvforcing["z"]; g = CA.Parameters.grav(params))
242251
close(tvforcing)
243252

244253
# Create accumulated dataset (3D: lon, lat, time)
@@ -287,3 +296,65 @@ function create_mock_era5_datasets(
287296

288297
return column_data_path, accum_data_path, inst_data_path
289298
end
299+
300+
"""
301+
geopotential_from_pressure(pressure_levels; R_d = 287.05, sp = 101325, T_avg = 250)
302+
303+
Convert pressure levels to geopotential levels using the hypsometric equation.
304+
R_d is the gas constant for dry air, sp is the surface pressure in Pa, and T_avg is the average temperature in the troposphere in K.
305+
"""
306+
function geopotential_from_pressure(pressure_levels; R_d = 287.05, sp = 101325, T_avg = 250)
307+
return R_d .* T_avg .* log.(sp ./ pressure_levels)
308+
end
309+
310+
"""
311+
temperature_from_geopotential(geopotential_levels; Γ = 0.0065, T_surf = 300, T_min = 200, g = 9.81)
312+
313+
Produce a typical temperature profile from geopotential levels.
314+
Γ is the lapse rate in K/m, T_surf is the surface temperature in K, and T_min is the minimum temperature in K, which simulates the tropopause.
315+
"""
316+
function temperature_from_geopotential(
317+
geopotential_levels;
318+
Γ = 0.0065,
319+
T_surf = 300,
320+
T_min = 200,
321+
g = 9.81,
322+
)
323+
return max.(T_min, T_surf .- Γ .* geopotential_levels ./ g)
324+
end
325+
326+
"""
327+
shum_from_geopotential(geopotential_levels; q0 = 0.02, shum_scale_height = 2e3, g = 9.81)
328+
329+
Produce a typical specific humidity profile from geopotential levels.
330+
q0 is the specific humidity at the surface and shum_scale_height is the scale height in geopotential coordinates.
331+
The default values are a 2% mixing ratio at the surface and a 2km scale height.
332+
"""
333+
function shum_from_geopotential(
334+
geopotential_levels;
335+
q0 = 0.02,
336+
shum_scale_height = 2e3,
337+
g = 9.81,
338+
)
339+
return q0 .* exp.(.-geopotential_levels ./ (shum_scale_height * g))
340+
end
341+
342+
"""
343+
monotonic_decreasing(A, dim)
344+
345+
Check if an array is monotonically decreasing along a given dimension.
346+
"""
347+
function monotonic_decreasing(A, dim)
348+
all_diffs = mapslices(x -> diff(x), A; dims = dim)
349+
return all(all_diffs .<= 0)
350+
end
351+
352+
"""
353+
monotonic_increasing(A, dim)
354+
355+
Check if an array is monotonically increasing along a given dimension.
356+
"""
357+
function monotonic_increasing(A, dim)
358+
all_diffs = mapslices(x -> diff(x), A, dims = dim)
359+
return all(all_diffs .>= 0)
360+
end

test/utilities.jl

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ end
309309
"site_latitude" => 0.0,
310310
"site_longitude" => 0.0,
311311
"t_end" => "5hours",
312+
"era5_diurnal_warming" => Nothing,
312313
)
313314

314315
temporary_dir = mktempdir()
@@ -345,10 +346,21 @@ end
345346
processed_data = NCDataset(sim_forcing_daily, "r")
346347

347348
# Test fixed variables - this tests that the variables are copied correctly
348-
for clima_var in ["hus", "ta", "ua", "va", "wap", "zg", "clw", "cli", "ts"]
349+
for clima_var in ["ua", "va", "wap", "ts"]
349350
@test all(isapprox.(processed_data[clima_var][:], 1, atol = 1e-10))
350351
end
351352

353+
for clima_var in ["clw", "cli"]
354+
@test all(isapprox.(processed_data[clima_var][:], 0, atol = 1e-10))
355+
end
356+
357+
# data is stored from top of atmosphere to surface
358+
@test monotonic_decreasing(processed_data["zg"], 3)
359+
@test monotonic_increasing(processed_data["ta"], 3)
360+
@test monotonic_increasing(processed_data["hus"], 3)
361+
@test all(processed_data["hus"] .>= 0)
362+
@test all(processed_data["ta"] .>= 200) # 200K is the minimum temperature set in the helper function
363+
352364
# Test accumulated variables - note that the sign is flipped because of differences between ecmwf and clima
353365
for clima_var in ["hfls", "hfss"]
354366
@test all(
@@ -387,6 +399,7 @@ end
387399
"site_latitude" => 0.0,
388400
"site_longitude" => 0.0,
389401
"t_end" => "2days",
402+
"era5_diurnal_warming" => Nothing,
390403
)
391404

392405
input_dir = mktempdir()
@@ -436,7 +449,7 @@ end
436449
@test length(processed_data["time"][:]) == expected_time_steps
437450

438451
# Test that data is consistent across time
439-
@test all(x -> all(isapprox.(x, 1, atol = 1e-10)), processed_data["ta"][:])
452+
@test monotonic_increasing(processed_data["ta"], 3)
440453
@test all(
441454
x -> all(isapprox.(x, -1 / time_resolution, atol = 1e-10)),
442455
processed_data["hfls"][:],
@@ -457,6 +470,7 @@ end
457470
# compute the vertical temperature gradient
458471
vertical_temperature_gradient =
459472
CA.get_vertical_tendencies(vert_partial_ds, "ta")
473+
@test all(vertical_temperature_gradient .<= 0)
460474

461475
close(processed_data)
462476
end
@@ -558,3 +572,67 @@ end
558572
close(test_ds)
559573
end
560574
end
575+
576+
@testset "ERA5 diurnal warming" begin
577+
FT = Float32
578+
temporary_dir = mktempdir()
579+
580+
parsed_args_0K = Dict(
581+
"start_date" => "20000506",
582+
"site_latitude" => 0.0,
583+
"site_longitude" => 0.0,
584+
"t_end" => "5hours",
585+
"era5_diurnal_warming" => Nothing,
586+
)
587+
588+
parsed_args_4K = Dict(
589+
"start_date" => "20000506",
590+
"site_latitude" => 0.0,
591+
"site_longitude" => 0.0,
592+
"t_end" => "5hours",
593+
"era5_diurnal_warming" => 4,
594+
)
595+
596+
create_mock_era5_datasets(temporary_dir, parsed_args_0K["start_date"], FT)
597+
598+
sim_forcing_0K = CA.get_external_monthly_forcing_file_path(
599+
parsed_args_0K,
600+
data_dir = temporary_dir,
601+
)
602+
sim_forcing_4K = CA.get_external_monthly_forcing_file_path(
603+
parsed_args_4K,
604+
data_dir = temporary_dir,
605+
)
606+
607+
@test basename(sim_forcing_0K) == "monthly_diurnal_cycle_forcing_0.0_0.0_20000506.nc"
608+
@test basename(sim_forcing_4K) ==
609+
"monthly_diurnal_cycle_forcing_0.0_0.0_20000506_plus_4.0K.nc"
610+
611+
CA.generate_external_forcing_file(
612+
parsed_args_0K,
613+
sim_forcing_0K,
614+
FT,
615+
input_data_dir = temporary_dir,
616+
)
617+
618+
CA.generate_external_forcing_file(
619+
parsed_args_4K,
620+
sim_forcing_4K,
621+
FT,
622+
input_data_dir = temporary_dir,
623+
)
624+
625+
# open the datasets and check the temperature and specific humidity profiles have been adjusted
626+
processed_data_0K = NCDataset(sim_forcing_0K, "r")
627+
processed_data_4K = NCDataset(sim_forcing_4K, "r")
628+
629+
# check that air and surface temperatures have increased by the +4K amount
630+
@test all(isapprox.(processed_data_0K["ta"] .+ 4, processed_data_4K["ta"]))
631+
@test isapprox(processed_data_0K["ts"] .+ 4, processed_data_4K["ts"])
632+
633+
# check that the specific humidity has increased
634+
@test all(processed_data_0K["hus"] .< processed_data_4K["hus"])
635+
636+
close(processed_data_0K)
637+
close(processed_data_4K)
638+
end

0 commit comments

Comments
 (0)