Tracer Conservation test for TripolarGrid#346
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
The checks are: Heat / OHCwhere and Freshwater contentwhere and Sign conventionThe minus sign is required because the diagnosed surface fluxes are defined with the opposite sign to the content tendency:
Pass criterionFor each timestep, the test requires:
to within numerical tolerance. |
…lEarth/NumericalEarth.jl into ts/add-tracer-conservation-test
| ohc_integral = Field(Integral(simulation.model.ocean.model.tracers.T, dims=(1, 2, 3))) | ||
| fw_content_integral = Field(Integral(simulation.model.ocean.model.tracers.S, dims=(1, 2, 3))) |
There was a problem hiding this comment.
| ohc_integral = Field(Integral(simulation.model.ocean.model.tracers.T, dims=(1, 2, 3))) | |
| fw_content_integral = Field(Integral(simulation.model.ocean.model.tracers.S, dims=(1, 2, 3))) | |
| ocean_heat_content = Field(ρᵒᶜ * cᵒᶜ * Integral(simulation.model.ocean.model.tracers.T, dims=(1, 2, 3))) | |
| freshwater_content = Field(-ρᵒᶜ / Sᵒᶜ * Integral(simulation.model.ocean.model.tracers.S, dims=(1, 2, 3))) |
| heat_flux_integral = Field(Integral(surface_forcing.heat_flux, dims=(1, 2, 3))) | ||
| fw_flux_integral = Field(Integral(surface_forcing.fw_flux, dims=(1, 2, 3))) |
There was a problem hiding this comment.
Why are these 3D integrals? Aren't these 2D fields?
Also, when we integrate the heat flux it becomes heat rate, right?
| heat_flux_integral = Field(Integral(surface_forcing.heat_flux, dims=(1, 2, 3))) | |
| fw_flux_integral = Field(Integral(surface_forcing.fw_flux, dims=(1, 2, 3))) | |
| heat_rate = Field(Integral(surface_forcing.heat_flux, dims=(1, 2, 3))) | |
| freshwater_rate = Field(Integral(surface_forcing.fw_flux, dims=(1, 2, 3))) |
| current_ohc = ρᵒᶜ * cᵒᶜ * scalar_integral(ohc_integral) | ||
| current_fw_content = -ρᵒᶜ / Sᵒᶜ * scalar_integral(fw_content_integral) | ||
| current_time = Float64(simulation.model.clock.time) | ||
| Δt_model = current_time - previous_time |
There was a problem hiding this comment.
isn't this the Δt? Why define new variable?
Perhaps just a @test current_time - previous_time == Δt?
There was a problem hiding this comment.
To make it clear that dt = t_n - t_n-1 rather than dt = t_n+1 - t_n
There was a problem hiding this comment.
What's the difference? The simulation you set up has a fixed Δt anyway?
There was a problem hiding this comment.
perhaps we need to call it previous_Δt?
There was a problem hiding this comment.
It's fixed here but perhaps not in all cases... yes previous_dt makes sense
There was a problem hiding this comment.
I think previous_Δt is saved in clock... So you can use model.clock.last_Δt?
Updated grid size and modified initialization of ocean and sea ice models to include date parameter.
|
For the conservation to be machine precision on a RightCenterFolded tripolar grid we need CliMA/Oceananigans.jl#5099 which should be ready to merge (it has been hanging for a little bit but it should be ready) |
| @test current_ocean_heat_content - previous_ocean_heat_content ≈ -previous_heat_flux * last_Δt rtol=1e-4 atol=1e-2 | ||
| @test current_freshwater_content - previous_freshwater_content ≈ -previous_freshwater_flux * last_Δt rtol=1e-4 atol=1e-2 |
There was a problem hiding this comment.
consider adding a comment to explain these choice of tolerances
There was a problem hiding this comment.
definitely
ideally this should pass with machine precision
in the process of debugging we found that even just a HydrostaticFreeSurfaceModel the tracer doesn't change based on the surface flux; I'm preparing a MWE+test for Oceananigans.
There was a problem hiding this comment.
hm... I take it back, the test passes in Oceananigans:
include("dependencies_for_runtests.jl")
arch = CPU()
grid = RectilinearGrid(arch, size=(10, 10, 10), x=(-10, 10), y=(-4, 4), z=(-2, 0), topology = (Bounded, Bounded, Bounded))
Jᶜ_func(x, y, t) = -2.1 + 20 * exp(-x^2 - y^2)
Jᶜ = Field{Center, Center, Nothing}(grid)
set!(Jᶜ, (x, y) -> Jᶜ_func(x, y, 0))
c_bcs = FieldBoundaryConditions(top = FluxBoundaryCondition(Jᶜ_func))
model = HydrostaticFreeSurfaceModel(grid,
timestepper=:SplitRungeKutta3,
tracers=:c,
boundary_conditions=(c=c_bcs,),
free_surface=SplitExplicitFreeSurface(substeps=15))
set!(model, c = (x, y, z) -> 10 * z + exp(x + y + z))
c = model.tracers.c
total_c = Integral(c, dims=(1, 2, 3))
surface_c_flux = Integral(Jᶜ, dims=(1, 2))
Δt = 0.3
simulation = Simulation(model; Δt, stop_iteration=10)
for _ = 1:simulation.stop_iteration
previous_total_c = first(Field(total_c))
previous_surface_c_flux = first(Field(surface_c_flux))
time_step!(simulation, Δt)
current_total_c = first(Field(total_c))
current_surface_c_flux = first(Field(surface_c_flux))
current_time = Float64(simulation.model.clock.time)
last_Δt = simulation.model.clock.last_Δt
@info "Iteration $(simulation.model.clock.iteration), time = $(current_time) seconds"
@show current_total_c
@show previous_total_c
@show current_total_c - previous_total_c
@show - previous_surface_c_flux * last_Δt
@test current_total_c - previous_total_c ≈ - previous_surface_c_flux * last_Δt
endwhich gives
julia> include("test_tracer_conservation.jl")
[2026/06/12 16:53:16.882] INFO Initializing simulation...
[2026/06/12 16:53:16.883] INFO ... simulation initialization complete (493.791 μs)
[2026/06/12 16:53:16.883] INFO Executing initial time step...
[2026/06/12 16:53:16.927] INFO ... initial time step complete (43.986 ms).
[2026/06/12 16:53:16.939] INFO Iteration 1, time = 0.3 seconds
current_total_c = 856826.6742445715
previous_total_c = 856741.5286719211
current_total_c - previous_total_c = 85.14557265036274
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.017] INFO Iteration 2, time = 0.6 seconds
current_total_c = 856911.8198172228
previous_total_c = 856826.6742445715
current_total_c - previous_total_c = 85.14557265129406
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.020] INFO Iteration 3, time = 0.8999999999999999 seconds
current_total_c = 856996.965389874
previous_total_c = 856911.8198172228
current_total_c - previous_total_c = 85.14557265117764
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.022] INFO Iteration 4, time = 1.2 seconds
current_total_c = 857082.1109625254
previous_total_c = 856996.965389874
current_total_c - previous_total_c = 85.14557265141048
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.025] INFO Iteration 5, time = 1.5 seconds
current_total_c = 857167.2565351762
previous_total_c = 857082.1109625254
current_total_c - previous_total_c = 85.1455726508284
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.037] INFO Iteration 6, time = 1.8 seconds
current_total_c = 857252.4021078274
previous_total_c = 857167.2565351762
current_total_c - previous_total_c = 85.14557265117764
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.040] INFO Iteration 7, time = 2.1 seconds
current_total_c = 857337.5476804782
previous_total_c = 857252.4021078274
current_total_c - previous_total_c = 85.1455726508284
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.042] INFO Iteration 8, time = 2.4 seconds
current_total_c = 857422.6932531294
previous_total_c = 857337.5476804782
current_total_c - previous_total_c = 85.14557265117764
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.045] INFO Iteration 9, time = 2.6999999999999997 seconds
current_total_c = 857507.8388257805
previous_total_c = 857422.6932531294
current_total_c - previous_total_c = 85.14557265117764
-previous_surface_c_flux * last_Δt = 85.14557265106653
[2026/06/12 16:53:17.053] INFO Simulation is stopping after running for 71.352 ms.
[2026/06/12 16:53:17.054] INFO Model iteration 10 equals or exceeds stop iteration 10.
[2026/06/12 16:53:17.054] INFO Iteration 10, time = 2.9999999999999996 seconds
current_total_c = 857592.9843984314
previous_total_c = 857507.8388257805
current_total_c - previous_total_c = 85.1455726508284
-previous_surface_c_flux * last_Δt = 85.14557265106653
This PR adds a tracer conservation test for a
TripolarGridsetup. Note that at the moment the test is quite heavy. I would appreciate your advice in making it a bit less heavy, but I wasn't sure what components are important (or not) in ensuring conservation. We know tracers are conserved in Oceananigans.jl, so if there is an issue it is likely in the coupling with other datasets. Hence I took a maximalist approach. Let's work on making this PR more lightweight but still revealing if there is a budget non-closure.