Skip to content

Commit 24e35f1

Browse files
nichollshCopilot
andcommitted
Add tests for guillot, ocean, and extend consts/phys coverage
- test_guillot.jl: new testset covering eval_tau (exact values + linearity), stellar temperature geometric relations, T4 positivity, T4 monotonicity, and profile helpers (length, positivity, sorting) - test_ocean.jl: new testset covering empty, basin-fill, planet-flood, two-liquid ordering, and depth-additivity scenarios - test_consts.jl: extend with fundamental constants (σSB, k_B, h_pl, c_vac, G_grav, zero_celcius), more mmw/atom-count entries, colour lookup completeness, species list consistency, and solar metallicity - test_phys.jl: add Psat at water boiling point, Lv at boiling point, Kc positivity/monotonicity, is_vapour for ideal gas, and _pretty_color for known and unknown gases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c8c43ad commit 24e35f1

5 files changed

Lines changed: 320 additions & 3 deletions

File tree

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ end
5757
LoggingExtras.global_logger(Logging.SimpleLogger(Logging.Warn))
5858
include("test_consts.jl")
5959
include("test_phys.jl")
60+
include("test_guillot.jl")
61+
include("test_ocean.jl")
6062
include("test_deep_heating.jl")
6163
if suite != "fast"
6264
include("test_integration.jl")

test/test_consts.jl

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,70 @@ using Test
22
using AGNI
33

44
@testset "consts" begin
5-
@test isapprox(AGNI.consts.R_gas, 8.314462618; atol=1e-12)
6-
@test isapprox(AGNI.consts._lookup_mmw["H2O"], 1.801530E-02; rtol=1e-8)
5+
6+
# -------------
7+
# Fundamental physical constants (NIST CODATA values)
8+
# Update these if the source values are changed.
9+
# -------------
10+
@test isapprox(AGNI.consts.R_gas, 8.314462618; atol=1e-12)
11+
@test isapprox(AGNI.consts.σSB, 5.670374419e-8; rtol=1e-9)
12+
@test isapprox(AGNI.consts.k_B, 1.380649e-23; rtol=1e-9)
13+
@test isapprox(AGNI.consts.h_pl, 6.62607015e-34; rtol=1e-9)
14+
@test isapprox(AGNI.consts.c_vac, 299792458.0; atol=0.1)
15+
@test isapprox(AGNI.consts.G_grav, 6.67430e-11; rtol=1e-5)
16+
@test isapprox(AGNI.consts.zero_celcius, 273.15; atol=1e-10)
17+
18+
# -------------
19+
# Mean molecular weights [kg mol-1] for key gases
20+
# -------------
21+
mmw_cases = [
22+
("H2O", 1.801530e-02),
23+
("CO2", 4.401000e-02),
24+
("N2", 2.801340e-02),
25+
("H2", 2.015880e-03),
26+
]
27+
for (gas, expected) in mmw_cases
28+
@test isapprox(AGNI.consts._lookup_mmw[gas], expected; rtol=1e-6)
29+
end
30+
31+
# -------------
32+
# Atom counts for key molecules
33+
# -------------
734
@test AGNI.consts._lookup_count_atoms["H2O"]["H"] == 2
8-
@test "H2O" in keys(AGNI.consts._lookup_color)
35+
@test AGNI.consts._lookup_count_atoms["H2O"]["O"] == 1
36+
@test AGNI.consts._lookup_count_atoms["CO2"]["C"] == 1
37+
@test AGNI.consts._lookup_count_atoms["CO2"]["O"] == 2
38+
39+
# -------------
40+
# Plotting colours: known gases and key elements must be present
41+
# -------------
42+
for gas in ["H2O", "CO2", "H2", "N2", "CH4"]
43+
@test haskey(AGNI.consts._lookup_color, gas)
44+
end
45+
for elem in ["H", "C", "O", "N", "S", "Fe", "Si"]
46+
@test haskey(AGNI.consts._lookup_color, elem)
47+
end
48+
49+
# -------------
50+
# Standard species lists are non-empty and self-consistent
51+
# -------------
52+
@test length(AGNI.consts.vols_standard) > 0
53+
@test length(AGNI.consts.vaps_standard) > 0
54+
@test length(AGNI.consts.gases_standard) == length(AGNI.consts.vols_standard) +
55+
length(AGNI.consts.vaps_standard)
56+
@test "H2O" in AGNI.consts.vols_standard
57+
@test "H" in AGNI.consts.elems_standard
58+
59+
# -------------
60+
# Solar metallicities: hydrogen is anchored at 12.00 by definition
61+
# -------------
62+
@test isapprox(AGNI.consts._solar_metallicity["H"], 12.00; atol=1e-10)
63+
@test haskey(AGNI.consts._solar_metallicity, "Fe")
64+
65+
# -------------
66+
# Liquid densities for ocean module
67+
# -------------
68+
@test isapprox(AGNI.consts._lookup_liquid_rho["H2O"], 958.37; rtol=1e-5)
69+
@test AGNI.consts._lookup_liquid_rho["CO2"] > AGNI.consts._lookup_liquid_rho["H2O"]
70+
971
end

test/test_guillot.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Test
2+
using AGNI
3+
4+
# Access the guillot submodule through setpt
5+
const guillot = AGNI.setpt.guillot
6+
7+
# Guillot (2010) analytical grey-gas T(p) profile module.
8+
# Constants inside the module:
9+
# κ_th = 7e-2 * 10 = 0.7 m2/kg (LW opacity)
10+
# κ_vs = 4e-3 * 10 = 0.04 m2/kg (SW opacity)
11+
# γ = κ_vs/κ_th = 0.04/0.7 (opacity ratio)
12+
13+
@testset "guillot" begin
14+
15+
# -------------
16+
# Optical depth: τ = p * κ_th / g
17+
# -------------
18+
@testset "eval_tau" begin
19+
p_test = [1e3, 1e5, 1e7 ] # pressure [Pa]
20+
g_test = [10.0, 10.0, 25.0 ] # gravity [m s-2]
21+
v_expt = [70.0, 7000.0, 280000.0] # τ = p * 0.7 / g [dimensionless]
22+
for i in eachindex(p_test)
23+
@test isapprox(guillot.eval_tau(p_test[i], g_test[i]), v_expt[i]; rtol=1e-10)
24+
end
25+
# τ must be linear in pressure at fixed gravity
26+
@test isapprox(guillot.eval_tau(2e5, 10.0), 2 * guillot.eval_tau(1e5, 10.0); rtol=1e-10)
27+
end
28+
29+
# -------------
30+
# Stellar temperatures: geometric relations
31+
# eval_Tirr(Tstar, Rstar, sep) = Tstar * sqrt(Rstar / sep)
32+
# eval_Teqm(Tstar, Rstar, sep) = Tstar * sqrt(Rstar / (2*sep))
33+
# So Tirr / Teqm == sqrt(2) exactly.
34+
# -------------
35+
@testset "stellar_temps" begin
36+
Tstar = 5778.0 # solar effective temperature [K]
37+
Rstar = 6.957e8 # solar radius [m]
38+
sep = 1.496e11 # Earth-Sun distance [m]
39+
40+
Tirr = guillot.eval_Tirr(Tstar, Rstar, sep)
41+
Teqm = guillot.eval_Teqm(Tstar, Rstar, sep)
42+
43+
@test Tirr > 0.0
44+
@test Teqm > 0.0
45+
@test Teqm < Tirr
46+
# exact geometric relationship between the two temperatures
47+
@test isapprox(Tirr / Teqm, sqrt(2.0); rtol=1e-10)
48+
end
49+
50+
# -------------
51+
# T^4 functions must return positive values for physical inputs
52+
# Note: eval_T4_avg uses E2(γτ) which diverges at τ=0 (τ>0 only).
53+
# eval_T4_cos does not use E1/E2 so τ=0 is fine.
54+
# -------------
55+
@testset "T4_positive" begin
56+
τ_vals_avg = [0.01, 0.1, 1.0, 10.0, 100.0] # τ > 0 for avg (E1 singularity at 0)
57+
τ_vals_cos = [0.0, 0.1, 1.0, 10.0, 100.0] # τ = 0 is fine for cos
58+
Tint = 100.0 # internal temperature [K]
59+
Teqm = 800.0 # equilibrium temperature [K]
60+
Tirr = 1000.0 # irradiation temperature [K]
61+
θ = 45.0 # zenith angle [degrees]
62+
for τ in τ_vals_avg
63+
@test guillot.eval_T4_avg(τ, Tint, Teqm) > 0.0
64+
end
65+
for τ in τ_vals_cos
66+
@test guillot.eval_T4_cos(τ, Tint, Tirr, θ) > 0.0
67+
end
68+
end
69+
70+
# -------------
71+
# T^4 must increase monotonically with optical depth (deeper = hotter)
72+
# for the average profile at sensible parameters
73+
# -------------
74+
@testset "T4_monotone" begin
75+
τ_lo, τ_hi = 0.1, 10.0
76+
Tint, Teqm, Tirr, θ = 200.0, 1200.0, 1500.0, 30.0
77+
@test guillot.eval_T4_avg(τ_hi, Tint, Teqm) > guillot.eval_T4_avg(τ_lo, Tint, Teqm)
78+
@test guillot.eval_T4_cos(τ_hi, Tint, Tirr, θ) > guillot.eval_T4_cos(τ_lo, Tint, Tirr, θ)
79+
end
80+
81+
# -------------
82+
# Profile helpers: correct length and all-positive temperatures
83+
# -------------
84+
@testset "profiles" begin
85+
grav = 10.0 # [m s-2]
86+
Tint = 100.0 # [K]
87+
Teqm = 900.0 # [K]
88+
Tirr = 1200.0 # [K]
89+
θ = 45.0 # [degrees]
90+
p_arr = [1e1, 1e2, 1e3, 1e4, 1e5, 1e6] # pressure grid [Pa]
91+
92+
T_avg = guillot.calc_profile_avg(p_arr, grav, Tint, Teqm)
93+
@test length(T_avg) == length(p_arr)
94+
@test all(T_avg .> 0.0)
95+
@test issorted(T_avg) # temperature increases towards higher pressures
96+
97+
T_cos = guillot.calc_profile_cos(p_arr, grav, Tint, Tirr, θ)
98+
@test length(T_cos) == length(p_arr)
99+
@test all(T_cos .> 0.0)
100+
@test issorted(T_cos)
101+
end
102+
103+
end

test/test_ocean.jl

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using Test
2+
using AGNI
3+
4+
# Ocean module: pure geometric functions for distributing condensed liquids
5+
# across ocean basins and continental shelves.
6+
# Liquid densities used below come from consts._lookup_liquid_rho:
7+
# H2O = 958.37 kg/m^3
8+
# CO2 = 1178.4 kg/m^3 (denser → sinks to bottom)
9+
10+
@testset "ocean" begin
11+
12+
# Shared planetary parameters (easy to update for a different test planet)
13+
fOB = 0.3 # ocean basin area fraction
14+
hCS = 1000.0 # continental shelf height [m]
15+
Rpl = 6.371e6 # planet radius [m]
16+
17+
# -------------
18+
# No liquids → empty layers, no coverage
19+
# -------------
20+
@testset "empty" begin
21+
sigs = Dict{String, Float64}()
22+
layers = AGNI.ocean.dist_surf_liq(sigs, fOB, hCS, Rpl)
23+
@test length(layers) == 0
24+
@test AGNI.ocean.get_topliq(layers) == "none"
25+
@test AGNI.ocean.get_maxdepth(layers) == 0.0
26+
@test AGNI.ocean.get_areacov(layers, fOB) == 0.0
27+
end
28+
29+
# -------------
30+
# Small amount of H2O fits entirely within the ocean basins
31+
# Expected: one layer, area coverage = fOB (continental planet)
32+
# -------------
33+
@testset "basin_fill" begin
34+
sigs = Dict("H2O" => 100.0) # 100 kg/m^2 of rainout
35+
layers = AGNI.ocean.dist_surf_liq(sigs, fOB, hCS, Rpl)
36+
@test length(layers) == 1
37+
@test AGNI.ocean.get_topliq(layers) == "H2O"
38+
@test AGNI.ocean.get_maxdepth(layers) > 0.0
39+
@test isapprox(AGNI.ocean.get_areacov(layers, fOB), fOB; atol=1e-10)
40+
end
41+
42+
# -------------
43+
# Very large amount of H2O overflows basins onto the continental shelf
44+
# Expected: area coverage = 1.0 (aqua planet)
45+
# -------------
46+
@testset "planet_flood" begin
47+
sigs = Dict("H2O" => 1.0e6) # huge amount; greatly exceeds basin capacity
48+
layers = AGNI.ocean.dist_surf_liq(sigs, fOB, hCS, Rpl)
49+
@test AGNI.ocean.get_topliq(layers) == "H2O"
50+
@test AGNI.ocean.get_areacov(layers, fOB) == 1.0
51+
end
52+
53+
# -------------
54+
# Two liquids: CO2 is denser (1178 kg/m^3) so it sinks below H2O (958 kg/m^3).
55+
# The top of the ocean is the least-dense liquid exposed to the atmosphere.
56+
# -------------
57+
@testset "two_liquids" begin
58+
sigs = Dict("H2O" => 200.0, "CO2" => 100.0)
59+
layers = AGNI.ocean.dist_surf_liq(sigs, fOB, hCS, Rpl)
60+
@test length(layers) == 2
61+
@test AGNI.ocean.get_topliq(layers) == "H2O" # H2O floats on CO2
62+
@test AGNI.ocean.get_maxdepth(layers) > 0.0
63+
end
64+
65+
# -------------
66+
# Depth is additive over layers (basin + shelf components)
67+
# -------------
68+
@testset "depth_additive" begin
69+
# Place enough liquid that both in-basin and shelf portions are non-zero
70+
sigs = Dict("H2O" => 1.0e5)
71+
layers = AGNI.ocean.dist_surf_liq(sigs, fOB, hCS, Rpl)
72+
depth = AGNI.ocean.get_maxdepth(layers)
73+
manual = sum(la[3] + la[4] for la in layers)
74+
@test isapprox(depth, manual; atol=1e-10)
75+
end
76+
77+
end

test/test_phys.jl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,77 @@ TEST_DIR = joinpath(ROOT_DIR,"test/")
187187
atmosphere.deallocate!(atmos)
188188
@test test_pass
189189
end
190+
191+
192+
# -------------
193+
# Test saturation pressure lookup
194+
# At the boiling point of water (373.15 K) Psat should be ~1 atm = 101325 Pa
195+
# -------------
196+
@testset "Psat" begin
197+
gas_H2O::phys.Gas_t = phys.load_gas("$RES_DIR/thermodynamics/", "H2O", true, false)
198+
T_boil = 373.15 # [K]
199+
Psat_e = 101325.0 # expected ~1 atm [Pa]
200+
Psat_o = phys.get_Psat(gas_H2O, T_boil)
201+
@test isfinite(Psat_o)
202+
@test Psat_o > 0.0
203+
@test isapprox(Psat_o, Psat_e; rtol=0.05)
204+
end
205+
206+
207+
# -------------
208+
# Test latent heat lookup
209+
# At the boiling point of water (373.15 K) Lv ≈ 2.256e6 J/kg
210+
# -------------
211+
@testset "Lv" begin
212+
gas_H2O::phys.Gas_t = phys.load_gas("$RES_DIR/thermodynamics/", "H2O", true, false)
213+
T_boil = 373.15 # [K]
214+
Lv_e = 2.256e6 # expected latent heat [J/kg]
215+
Lv_o = phys.get_Lv(gas_H2O, T_boil)
216+
@test isfinite(Lv_o)
217+
@test Lv_o > 0.0
218+
@test isapprox(Lv_o, Lv_e; rtol=0.05)
219+
end
220+
221+
222+
# -------------
223+
# Test thermal conductivity (kinetic theory estimate)
224+
# Must be positive and in a physically reasonable range for water vapour
225+
# -------------
226+
@testset "Kc" begin
227+
gas_H2O::phys.Gas_t = phys.load_gas("$RES_DIR/thermodynamics/", "H2O", true, false)
228+
t_test = [200.0, 500.0, 1000.0] # [K]
229+
for t in t_test
230+
Kc = phys.get_Kc(gas_H2O, t)
231+
@test isfinite(Kc) && Kc > 0.0
232+
end
233+
# conductivity must increase with temperature (∝ sqrt(T))
234+
@test phys.get_Kc(gas_H2O, 500.0) > phys.get_Kc(gas_H2O, 200.0)
235+
end
236+
237+
238+
# -------------
239+
# Test is_vapour
240+
# Ideal gas is always in the vapour phase (no real-gas condensation)
241+
# -------------
242+
@testset "is_vapour" begin
243+
gas_N2::phys.Gas_t = phys.load_gas("$RES_DIR/thermodynamics/", "N2", true, false)
244+
# ideal / no-sat stub is always vapour
245+
@test phys.is_vapour(gas_N2, 300.0, 1e5)
246+
@test phys.is_vapour(gas_N2, 100.0, 1e8)
247+
end
248+
249+
250+
# -------------
251+
# Test _pretty_color
252+
# Known gases return their lookup colour; unknown gases return a valid hex string
253+
# -------------
254+
@testset "pretty_color" begin
255+
# known lookup entry
256+
@test phys._pretty_color("CO2") == AGNI.consts._lookup_color["CO2"]
257+
# unknown molecule: must return a 7-character hex code starting with '#'
258+
col_sio = phys._pretty_color("SiO")
259+
@test length(col_sio) == 7
260+
@test col_sio[1] == '#'
261+
end
262+
190263
end

0 commit comments

Comments
 (0)