Skip to content

Commit 26181e0

Browse files
committed
Fix ConversationChecker
`sum` behaves differently depending on the space, so I added `integral`. In addition to this, the test was not correctly accounting for the movement of water and energy.
1 parent 7369481 commit 26181e0

File tree

4 files changed

+88
-32
lines changed

4 files changed

+88
-32
lines changed

src/ConservationChecker.jl

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This module contains functions that check global conservation of energy and wate
66
module ConservationChecker
77

88
import ..Interfacer, ..Utilities
9+
import ..Utilities: integral
910
import ClimaCore as CC
1011

1112
export AbstractConservationCheck, EnergyConservationCheck, WaterConservationCheck, check_conservation!
@@ -85,26 +86,11 @@ function check_conservation!(
8586
if sim isa Interfacer.AtmosModelSimulation
8687
radiative_energy_flux_toa = Interfacer.get_field(sim, Val(:radiative_energy_flux_toa))
8788

88-
# ClimaCore #1578, if radiative_energy_flux_toa comes from Fields.level
89-
# TODO: Fix this in ClimaCore
90-
rad_grid = CC.Spaces.grid(axes(radiative_energy_flux_toa))
91-
if rad_grid isa CC.Grids.LevelGrid
92-
if rad_grid.level isa CC.Utilities.PlusHalf
93-
# FaceSpace
94-
surface_integral =
95-
sum(radiative_energy_flux_toa ./ CC.Fields.Δz_field(radiative_energy_flux_toa) .* 2)
96-
else
97-
# Center
98-
surface_integral = sum(radiative_energy_flux_toa ./ CC.Fields.Δz_field(radiative_energy_flux_toa))
99-
end
100-
else
101-
surface_integral = sum(radiative_energy_flux_toa)
102-
end
103-
10489
if isempty(ccs.toa_net_source)
105-
radiation_sources_accum = surface_integral * FT(float(coupler_sim.Δt_cpl)) # ∫ J / m^2 dA
90+
radiation_sources_accum = integral(radiative_energy_flux_toa) * FT(float(coupler_sim.Δt_cpl)) # ∫ J / m^2 dA
10691
else
107-
radiation_sources_accum = surface_integral * FT(float(coupler_sim.Δt_cpl)) + ccs.toa_net_source[end] # ∫ J / m^2 dA
92+
radiation_sources_accum =
93+
integral(radiative_energy_flux_toa) * FT(float(coupler_sim.Δt_cpl)) + ccs.toa_net_source[end] # ∫ J / m^2 dA
10894
end
10995
push!(ccs.toa_net_source, radiation_sources_accum)
11096

@@ -122,7 +108,7 @@ function check_conservation!(
122108
current = FT(0)
123109
else
124110
previous = getproperty(ccs, sim_name)
125-
current = sum(Interfacer.get_field(sim, Val(:energy)) .* area_fraction) # # ∫ J / m^3 dV
111+
current = integral(Interfacer.get_field(sim, Val(:energy)) .* area_fraction) # # ∫ J / m^3 dV
126112
end
127113
push!(previous, current)
128114
total += current
@@ -174,19 +160,19 @@ function check_conservation!(
174160

175161
# save atmos
176162
previous = getproperty(ccs, sim_name)
177-
current = sum(Interfacer.get_field(sim, Val(:water))) # kg (∫kg of water / m^3 dV)
163+
current = integral(Interfacer.get_field(sim, Val(:water))) # kg (∫kg of water / m^3 dV)
178164
push!(previous, current)
179165

180166
elseif sim isa Interfacer.SurfaceModelSimulation
181167
# save surfaces
182168
area_fraction = Interfacer.get_field(sim, Val(:area_fraction))
183169
if isnothing(Interfacer.get_field(sim, Val(:water)))
184170
previous = getproperty(ccs, sim_name)
185-
current = sum(PE_net .* area_fraction) # kg (∫kg of water / m^3 dV)
171+
current = integral(PE_net .* area_fraction) # kg (∫kg of water / m^3 dV)
186172
push!(previous, current)
187173
else
188174
previous = getproperty(ccs, sim_name)
189-
current = sum(Interfacer.get_field(sim, Val(:water)) .* area_fraction) # kg (∫kg of water / m^3 dV)
175+
current = integral(Interfacer.get_field(sim, Val(:water)) .* area_fraction) # kg (∫kg of water / m^3 dV)
190176
push!(previous, current)
191177
end
192178
end
@@ -206,7 +192,7 @@ end
206192
"""
207193
surface_water_gain_from_rates(cs::Interfacer.CoupledSimulation)
208194
209-
Determines the total water content gain/loss of a surface from the begining of the simulation based on evaporation and precipitation rates.
195+
Determines the total water content gain/loss of a surface from the beginning of the simulation based on evaporation and precipitation rates.
210196
"""
211197
function surface_water_gain_from_rates(cs::Interfacer.CoupledSimulation)
212198
evaporation = cs.fields.F_turb_moisture # kg / m^2 / s / layer depth

src/Utilities.jl

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ClimaCore as CC
1111
import Logging
1212
import ClimaUtilities.OutputPathGenerator: generate_output_path
1313

14-
export swap_space!, get_device, get_comms_context, show_memory_usage, setup_output_dirs, time_to_seconds
14+
export swap_space!, get_device, get_comms_context, show_memory_usage, setup_output_dirs, time_to_seconds, integral
1515

1616
"""
1717
swap_space!(space_out::CC.Spaces.AbstractSpace, field_in::CC.Fields.Field)
@@ -188,4 +188,34 @@ function time_to_seconds(s::String)
188188
end
189189

190190

191+
"""
192+
integral(field)
193+
194+
Return the integral (a scalar) for `field` along its spatial dimensions.
195+
"""
196+
function integral(field::CC.Fields.Field)
197+
if axes(field) isa CC.Spaces.SpectralElementSpace2D
198+
# ClimaCore #1578, if field comes from Fields.level
199+
# TODO: This should be fixed in ClimaCore
200+
rad_grid = CC.Spaces.grid(axes(field))
201+
if rad_grid isa CC.Grids.LevelGrid
202+
if rad_grid.level isa CC.Utilities.PlusHalf
203+
# FaceSpace
204+
return sum(field ./ CC.Fields.Δz_field(field) .* 2)
205+
else
206+
# Center
207+
return sum(field ./ CC.Fields.Δz_field(field))
208+
end
209+
else
210+
return sum(field)
211+
end
212+
else
213+
return sum(field)
214+
end
215+
end
216+
217+
function integral(field::AbstractArray)
218+
return sum(field)
219+
end
220+
191221
end # module

test/conservation_checker_tests.jl

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import ClimaComms
66
@static pkgversion(ClimaComms) >= v"0.6" && ClimaComms.@import_required_backends
77
import ClimaCore as CC
88
import ClimaCoupler: ConservationChecker, Interfacer
9-
10-
REGRID_DIR = @isdefined(REGRID_DIR) ? REGRID_DIR : joinpath("", "regrid_tmp/")
9+
import ClimaCoupler.Utilities: integral
1110

1211
get_slab_energy(slab_sim, T_sfc) =
1312
slab_sim.integrator.p.params.ρ .* slab_sim.integrator.p.params.c .* T_sfc .* slab_sim.integrator.p.params.h
@@ -17,8 +16,8 @@ struct TestAtmos{I} <: Interfacer.AtmosModelSimulation
1716
end
1817
Interfacer.name(s::TestAtmos) = "TestAtmos"
1918
Interfacer.get_field(s::TestAtmos, ::Val{:radiative_energy_flux_toa}) = ones(s.i.space) .* 200
20-
Interfacer.get_field(s::TestAtmos, ::Val{:water}) = ones(s.i.space)
21-
Interfacer.get_field(s::TestAtmos, ::Val{:energy}) = ones(s.i.space) .* CC.Spaces.undertype(s.i.space)(1e6)
19+
Interfacer.get_field(s::TestAtmos, ::Val{:water}) = s.i.water
20+
Interfacer.get_field(s::TestAtmos, ::Val{:energy}) = s.i.energy
2221

2322
struct TestOcean{I} <: Interfacer.SurfaceModelSimulation
2423
i::I
@@ -41,7 +40,10 @@ for FT in (Float32, Float64)
4140
space = CC.CommonSpaces.CubedSphereSpace(FT; radius = FT(6371e3), n_quad_points = 4, h_elem = 4)
4241

4342
# set up model simulations
44-
atmos = TestAtmos((; space = space))
43+
initial_energy = ones(space) .* CC.Spaces.undertype(space)(1e6)
44+
initial_water = ones(space) .* CC.Spaces.undertype(space)(1e5)
45+
46+
atmos = TestAtmos((; space = space, energy = initial_energy, water = initial_water))
4547
land = TestOcean((; space = space))
4648
ocean = TestLand((; space = space))
4749
ice = Interfacer.SurfaceStub((; area_fraction = CC.Fields.ones(space) .* FT(0.5)))
@@ -84,13 +86,28 @@ for FT in (Float32, Float64)
8486
P = cf.P_liq
8587
Δt = float(cs.Δt_cpl)
8688

89+
volume = integral(ones(space))
90+
91+
area_fraction_scaling =
92+
Interfacer.get_field(land, Val(:area_fraction)) .+ Interfacer.get_field(ocean, Val(:area_fraction))
93+
water_from_precipitation = integral(P .* area_fraction_scaling) .* FT(Δt)
94+
energy_from_radiation = integral(F_r) .* FT(Δt)
95+
energy_per_unit_cell = CC.Fields.ones(space) .* energy_from_radiation ./ volume
96+
water_per_unit_cell = CC.Fields.ones(space) .* water_from_precipitation ./ volume
97+
8798
# analytical solution
88-
tot_energy_an = sum(FT.(F_r .* 3Δt .+ 1e6 .* 1.25))
89-
tot_water_an = sum(FT.(.-P .* 3Δt .* 0.5 .+ CC.Fields.ones(space)))
99+
# Only ocean and atmos have energy
100+
area_fraction_scaling = CC.Fields.ones(space) .+ Interfacer.get_field(ocean, Val(:area_fraction))
101+
tot_energy_an = integral(area_fraction_scaling .* FT.(initial_energy)) + energy_from_radiation
102+
tot_water_an = integral(FT.(initial_water)) - water_from_precipitation
90103

91104
# run check_conservation!
92105
ConservationChecker.check_conservation!(cs, runtime_check = true)
106+
atmos.i.energy .-= energy_per_unit_cell
107+
atmos.i.water .+= water_per_unit_cell
93108
ConservationChecker.check_conservation!(cs, runtime_check = true)
109+
atmos.i.energy .-= energy_per_unit_cell
110+
atmos.i.water .+= water_per_unit_cell
94111
ConservationChecker.check_conservation!(cs, runtime_check = true)
95112

96113
total_energy = cs.conservation_checks.energy.sums.total
@@ -118,7 +135,10 @@ for FT in (Float32, Float64)
118135
space = CC.CommonSpaces.CubedSphereSpace(FT; radius = FT(6371e3), n_quad_points = 4, h_elem = 4)
119136

120137
# set up model simulations
121-
atmos = TestAtmos((; space = space))
138+
initial_energy = ones(space) .* CC.Spaces.undertype(space)(1e6)
139+
initial_water = ones(space) .* CC.Spaces.undertype(space)(1e5)
140+
141+
atmos = TestAtmos((; space = space, energy = initial_energy, water = initial_water))
122142
land = TestOcean((; space = space))
123143
ocean = TestLand((; space = space))
124144
ice = Interfacer.SurfaceStub((; area_fraction = CC.Fields.ones(space) .* FT(0.5)))

test/utilities_tests.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,24 @@ for FT in (Float32, Float64)
5555
@test typeof(Utilities.get_comms_context(parsed_args)) ==
5656
typeof(ClimaComms.context(ClimaComms.CPUSingleThreaded()))
5757
end
58+
59+
@testset "integral" begin
60+
space2d = CC.CommonSpaces.CubedSphereSpace(FT; radius = FT(6371e3), n_quad_points = 4, h_elem = 4)
61+
ones2d = ones(space2d)
62+
63+
space3d = CC.CommonSpaces.ExtrudedCubedSphereSpace(
64+
FT;
65+
z_elem = 10,
66+
z_min = 0,
67+
z_max = 1,
68+
radius = FT(6371e3),
69+
h_elem = 10,
70+
n_quad_points = 4,
71+
staggering = CC.CommonSpaces.CellCenter(),
72+
)
73+
ones3d_level = CC.Fields.level(ones(space3d), 1)
74+
75+
@test isapprox(Utilities.integral(ones3d_level), Utilities.integral(ones2d), rtol = 1e-5)
76+
@test Utilities.integral(ones(space3d)) == sum(ones(space3d))
77+
end
5878
end

0 commit comments

Comments
 (0)