|
| 1 | +# |
| 2 | +# Flux consistency test (AMIP + bucket land) |
| 3 | +# |
| 4 | +# This test sets up an AMIP coupled simulation (atmosphere + bucket land + prescribed |
| 5 | +# ocean + prescribed sea ice), advances one coupling step, and verifies that the |
| 6 | +# surface radiative flux seen by the atmosphere matches what each surface model |
| 7 | +# computes/stores: |
| 8 | +# - Atmosphere: uses `sim.integrator.p.radiation.ᶠradiation_flux` (positive downward). |
| 9 | +# - Bucket land: compares against `sim.integrator.p.bucket.R_n` with opposite sign |
| 10 | +# convention (so atmos ≈ -R_n) on land-dominant cells. |
| 11 | +# - Prescribed ocean: skipped — SST is prescribed so radiative fluxes are not computed |
| 12 | +# in the same way. |
| 13 | +# - Prescribed sea ice: not stored directly; compute using cache fields and compare |
| 14 | +# to the atmospheric flux on ice-dominant cells. |
| 15 | + |
| 16 | +import Test: @test, @testset |
| 17 | +import ClimaCore as CC |
| 18 | +import ClimaComms |
| 19 | +ClimaComms.@import_required_backends |
| 20 | +import ClimaCoupler |
| 21 | + |
| 22 | +# Use the AMIP setup helpers to construct a coupled simulation |
| 23 | +include(joinpath("..", "setup_run.jl")) |
| 24 | + |
| 25 | +@testset "surface radiative flux consistency (AMIP + bucket land)" begin |
| 26 | + # Build AMIP configuration used in CI by default |
| 27 | + config_file = joinpath(pkgdir(ClimaCoupler), "config/ci_configs/amip_default.yml") |
| 28 | + config_dict = get_coupler_config_dict(config_file) |
| 29 | + |
| 30 | + # Make sure radiation is computed during the first step |
| 31 | + config_dict["dt_rad"] = config_dict["dt"] |
| 32 | + |
| 33 | + # Construct coupled simulation and run one coupling step |
| 34 | + cs = CoupledSimulation(config_dict) |
| 35 | + step!(cs) |
| 36 | + boundary_space = Interfacer.boundary_space(cs) |
| 37 | + |
| 38 | + # Unpack component models |
| 39 | + (; atmos_sim, land_sim, ocean_sim, ice_sim) = cs.model_sims |
| 40 | + |
| 41 | + # Atmosphere: radiative flux on the surface interface |
| 42 | + # Convention: positive downward to the surface |
| 43 | + atmos_flux = CC.Spaces.level( |
| 44 | + atmos_sim.integrator.p.radiation.ᶠradiation_flux.components.data.:1, |
| 45 | + CC.Utilities.half, |
| 46 | + ) |
| 47 | + |
| 48 | + # Integrated land: compare to net radiation stored in the bucket cache |
| 49 | + # Convention note: bucket R_n is stored with the opposite sign (see climaland_bucket.jl), |
| 50 | + # so we compare atmos_flux ≈ -R_n on land points. |
| 51 | + p = land_sim.integrator.p |
| 52 | + land_fraction = Interfacer.get_field(land_sim, Val(:area_fraction)) |
| 53 | + land_flux = Interfacer.remap(land_sim.integrator.p.bucket.R_n, boundary_space) |
| 54 | + @. land_flux = ifelse(land_fraction ≈ 0, zero(land_flux), land_flux) |
| 55 | + |
| 56 | + err_land = @. atmos_flux - land_flux |
| 57 | + @. err_land = ifelse(land_fraction ≈ 0, zero(err_land), err_land) |
| 58 | + @show "Integrated land flux error: $(maximum(abs.(err_land)))" |
| 59 | + @test maximum(abs.(err_land)) < 5 |
| 60 | + |
| 61 | + # Prescribed ice: radiative fluxes aren't stored; compute from cache and compare |
| 62 | + p = ice_sim.integrator.p |
| 63 | + Y = ice_sim.integrator.u |
| 64 | + FT = eltype(Y) |
| 65 | + |
| 66 | + # Radiative flux toward surface (positive downward) |
| 67 | + # TODO: get sigma from parameters |
| 68 | + σ = FT(5.67e-8) |
| 69 | + (; k_ice, h, T_base, ρ, c, α, ϵ) = p.params |
| 70 | + ice_rad_flux = |
| 71 | + (1 .- α) .* p.SW_d .+ |
| 72 | + ϵ .* (p.LW_d .- σ .* Interfacer.get_field(ice_sim, Val(:surface_temperature)) .^ 4) |
| 73 | + @. ice_rad_flux = ifelse(p.area_fraction ≈ 0, zero(ice_rad_flux), ice_rad_flux) |
| 74 | + ice_fraction = Interfacer.get_field(ice_sim, Val(:area_fraction)) |
| 75 | + |
| 76 | + # Prescribed ocean: SST is prescribed, but for this test we can still compute |
| 77 | + # the radiative flux seen by the ocean surface using the same formula. |
| 78 | + α = Interfacer.get_field(ocean_sim, Val(:surface_direct_albedo)) |
| 79 | + ϵ = Interfacer.get_field(ocean_sim, Val(:emissivity)) |
| 80 | + ocean_rad_flux = |
| 81 | + (1 .- α) .* cs.fields.SW_d .+ |
| 82 | + ϵ .* ( |
| 83 | + cs.fields.LW_d .- |
| 84 | + σ .* Interfacer.get_field(ocean_sim, Val(:surface_temperature)) .^ 4 |
| 85 | + ) |
| 86 | + ocean_fraction = Interfacer.get_field(ocean_sim, Val(:area_fraction)) |
| 87 | + @. ocean_rad_flux = ifelse(ocean_fraction ≈ 0, zero(ocean_rad_flux), ocean_rad_flux) |
| 88 | + |
| 89 | + # Combine component fluxes by area-weighted sum (incl. bucket sign convention): |
| 90 | + combined_fluxes = |
| 91 | + .-land_fraction .* land_flux .+ ice_fraction .* ice_rad_flux .+ |
| 92 | + ocean_fraction .* ocean_rad_flux |
| 93 | + err_fluxes = atmos_flux .+ combined_fluxes |
| 94 | + @show "Combined fluxes error: $(maximum(abs.(err_fluxes)))" |
| 95 | + @test maximum(abs.(err_fluxes)) < 8 |
| 96 | +end |
0 commit comments