|
| 1 | +# Held-Suarez test case |
| 2 | +# Following Souza et al.: |
| 3 | +# The Flux-Differencing Discontinuous Galerkin Method Applied to an Idealized Fully |
| 4 | +# Compressible Nonhydrostatic Dry Atmosphere |
| 5 | + |
| 6 | +using OrdinaryDiffEqLowStorageRK |
| 7 | +using Trixi |
| 8 | +using TrixiAtmo |
| 9 | +using LinearAlgebra |
| 10 | + |
| 11 | +function cartesian_to_sphere(x) |
| 12 | + r = norm(x) |
| 13 | + lambda = atan(x[2], x[1]) |
| 14 | + phi = asin(x[3] / r) |
| 15 | + |
| 16 | + return lambda, phi, r |
| 17 | +end |
| 18 | + |
| 19 | +function initial_condition_isothermal(x, t, equations::PassiveTracerEquations) |
| 20 | + # equation (60) in the paper |
| 21 | + temperature = 285 # T_I |
| 22 | + gas_constant = 287 # R_d |
| 23 | + surface_pressure = 1e5 # p_0 |
| 24 | + radius_earth = 6.371229e6 # r_planet |
| 25 | + gravitational_acceleration = 9.80616 # g |
| 26 | + c_v = 717.5 # specific heat capacity at constant volume |
| 27 | + |
| 28 | + r = norm(x) |
| 29 | + # Make sure that r is not smaller than radius_earth |
| 30 | + z = max(r - radius_earth, 0.0) |
| 31 | + r = z + radius_earth |
| 32 | + |
| 33 | + # pressure |
| 34 | + # geopotential formulation? |
| 35 | + p = surface_pressure * |
| 36 | + exp(gravitational_acceleration * |
| 37 | + (radius_earth^2 / r - radius_earth) / |
| 38 | + (gas_constant * temperature)) |
| 39 | + |
| 40 | + # density (via ideal gas law) |
| 41 | + rho = p / (gas_constant * temperature) |
| 42 | + |
| 43 | + # geopotential |
| 44 | + phi = gravitational_acceleration * (radius_earth - radius_earth^2 / r) |
| 45 | + |
| 46 | + E = c_v * temperature + phi |
| 47 | + |
| 48 | + tracer = SVector(ntuple(@inline(v->0.0001), Val(Trixi.ntracers(equations)))) |
| 49 | + |
| 50 | + return vcat(SVector(rho, 0, 0, 0, rho * E, phi), |
| 51 | + rho * tracer, rho * tracer) |
| 52 | +end |
| 53 | + |
| 54 | +@inline function source_terms_coriolis(u, x, t, |
| 55 | + equations::CompressibleEulerEquationsWithGravity3D) |
| 56 | + radius_earth = 6.371229e6 # r_planet |
| 57 | + angular_velocity = 7.29212e-5 # Ω |
| 58 | + # 7.27220521664e-05 (Giraldo) |
| 59 | + |
| 60 | + r = norm(x) |
| 61 | + # Make sure that r is not smaller than radius_earth |
| 62 | + z = max(r - radius_earth, 0.0) |
| 63 | + r = z + radius_earth |
| 64 | + |
| 65 | + du0 = zero(eltype(u)) |
| 66 | + |
| 67 | + # Coriolis term, -2Ω × ρv = -2 * angular_velocity * (0, 0, 1) × u[2:4] |
| 68 | + du2 = 2 * angular_velocity * u[3] |
| 69 | + du3 = -2 * angular_velocity * u[2] |
| 70 | + |
| 71 | + return SVector(du0, du2, du3, du0, du0, du0, du0) |
| 72 | +end |
| 73 | + |
| 74 | +@inline function source_terms_hs_relaxation(u, x, t, |
| 75 | + equations::CompressibleEulerEquationsWithGravity3D) |
| 76 | + # equations (55)-(58) in the paper |
| 77 | + secondsperday = 60 * 60 * 24 |
| 78 | + radius_earth = 6.371229e6 # r_planet |
| 79 | + k_f = 1 / secondsperday # Damping scale for momentum |
| 80 | + k_a = 1 / (40 * secondsperday) # Polar relaxation scale |
| 81 | + k_s = 1 / (4 * secondsperday) # Equatorial relaxation scale |
| 82 | + T_min = 200 # Minimum equilibrium temperature |
| 83 | + T_equator = 315 # Equatorial equilibrium temperature |
| 84 | + surface_pressure = 1e5 # p_0 |
| 85 | + deltaT = 60 # Latitudinal temperature difference |
| 86 | + deltaTheta = 10 # Vertical temperature difference |
| 87 | + sigma_b = 0.7 # Dimensionless damping height |
| 88 | + gas_constant = 287 # R_d |
| 89 | + c_v = 717.5 # Specific heat capacity of dry air at constant volume |
| 90 | + c_p = 1004.5 # Specific heat capacity of dry air at constant pressure |
| 91 | + |
| 92 | + _, _, _, _, pressure = cons2prim(u, equations) |
| 93 | + lon, lat, r = cartesian_to_sphere(x) |
| 94 | + temperature = pressure / (u[1] * gas_constant) |
| 95 | + |
| 96 | + sigma = pressure / surface_pressure # "p_0 instead of instantaneous surface pressure" |
| 97 | + delta_sigma = max(0, (sigma - sigma_b) / (1 - sigma_b)) # "height factor" |
| 98 | + k_v = k_f * delta_sigma |
| 99 | + k_T = k_a + (k_s - k_a) * delta_sigma * cos(lat)^4 |
| 100 | + |
| 101 | + T_equi = max(T_min, |
| 102 | + (T_equator - deltaT * sin(lat)^2 - deltaTheta * log(sigma) * cos(lat)^2) * |
| 103 | + sigma^(gas_constant / c_p)) |
| 104 | + |
| 105 | + # project onto r, normalize! @. Yₜ.c.uₕ -= k_v * Y.c.uₕ |
| 106 | + # Make sure that r is not smaller than radius_earth |
| 107 | + z = max(r - radius_earth, 0.0) |
| 108 | + r = z + radius_earth |
| 109 | + dotprod = (u[2] * x[1] + u[3] * x[2] + u[4] * x[3]) / (r * r) |
| 110 | + |
| 111 | + du2 = -k_v * (u[2] - dotprod * x[1]) |
| 112 | + du3 = -k_v * (u[3] - dotprod * x[2]) |
| 113 | + du4 = -k_v * (u[4] - dotprod * x[3]) |
| 114 | + |
| 115 | + du5 = -k_T * u[1] * c_v * (temperature - T_equi) |
| 116 | + |
| 117 | + du0 = zero(eltype(u)) |
| 118 | + |
| 119 | + return SVector(du0, du2, du3, du4, du5, du0, du0) |
| 120 | +end |
| 121 | + |
| 122 | +# setup |
| 123 | +struct RadonDecaySetup{NPRODUCTS} |
| 124 | + dt::Float64 |
| 125 | + lambda::Vector{Float64} |
| 126 | + pr1::Array{Float64, 2} |
| 127 | + pr2::Array{Float64, 3} |
| 128 | + |
| 129 | + function RadonDecaySetup(; nproducts = 1, dt = 1.0) |
| 130 | + secondsperday = 60 * 60 * 24 |
| 131 | + half_lifes = [3.8 * secondsperday, # 222Rn -> 218Po |
| 132 | + 3.0 * 60, # 218Po -> 214Pb |
| 133 | + 27.0 * 60, # 214Pb -> 214Bi |
| 134 | + 20.0 * 60, # 214Bi -> 214Po -> 210Pb |
| 135 | + 22.0 * 365 * secondsperday] # 210Pb -> ... -> 206Pb |
| 136 | + |
| 137 | + lambda = log(2) ./ half_lifes[1:nproducts] |
| 138 | + |
| 139 | + pr1 = fill(1.0, nproducts, nproducts) |
| 140 | + for i in 1:nproducts |
| 141 | + for m in 1:(i - 1) |
| 142 | + for q in m:(i - 1) |
| 143 | + pr1[i, m] = pr1[i, m] * lambda[q] |
| 144 | + end |
| 145 | + end |
| 146 | + end |
| 147 | + |
| 148 | + pr2 = fill(1.0, nproducts, nproducts, nproducts) |
| 149 | + for i in 1:nproducts |
| 150 | + for m in 1:(i - 1) |
| 151 | + for k in m:i |
| 152 | + for j in m:i |
| 153 | + # TODO if (j==k) CYCLE |
| 154 | + pr2[i, m, k] = pr2[i, m, k] * (lambda[j] - lambda[k]) |
| 155 | + end |
| 156 | + end |
| 157 | + end |
| 158 | + end |
| 159 | + new{nproducts}(dt, lambda, pr1, pr2) |
| 160 | + end |
| 161 | +end |
| 162 | + |
| 163 | +@inline function source_terms_radon_decay(u, tracer_equations::PassiveTracerEquations, |
| 164 | + setup::RadonDecaySetup{5}) |
| 165 | + @unpack dt, lambda, pr1, pr2 = setup |
| 166 | + tracer = tracer_variables(u, tracer_equations) |
| 167 | + |
| 168 | + tracer_new = zeros(eltype(u), 5) |
| 169 | + for i in 1:5 |
| 170 | + tracer_new[i] = tracer[i] * exp(-lambda[i] * dt) |
| 171 | + for m in 1:(i - 1) |
| 172 | + tmp = 0.0 |
| 173 | + for k in m:i |
| 174 | + tmp = tmp + exp(-lambda[k] * dt) / pr2[i, m, k] |
| 175 | + end |
| 176 | + tracer_new[i] = tracer_new[i] + tracer[m] * pr1[i, m] * tmp |
| 177 | + end |
| 178 | + end |
| 179 | + |
| 180 | + return SVector((tracer_new .- tracer) ./ dt) |
| 181 | +end |
| 182 | + |
| 183 | +@inline function source_terms_radon_decay(u, tracer_equations::PassiveTracerEquations, |
| 184 | + setup::RadonDecaySetup{1}) |
| 185 | + @unpack dt, lambda = setup |
| 186 | + tau = inv(lambda[1]) |
| 187 | + tracer = Trixi.tracers(u, tracer_equations) |
| 188 | + y = tracer .* ((exp(-dt / tau) - 1) / dt) |
| 189 | + return y |
| 190 | +end |
| 191 | + |
| 192 | +@inline function source_terms_radon_init(u, x, setup::RadonDecaySetup{1}) |
| 193 | + radius_earth = 6.371229e6 # r_planet |
| 194 | + lon, lat, r = cartesian_to_sphere(x) |
| 195 | + lon_pos = 0.889036791765 |
| 196 | + lon_dist = min(abs(lon - lon_pos), abs(lon + 2 * pi - lon_pos), |
| 197 | + abs(lon - 2 * pi - lon_pos)) |
| 198 | + tracer1 = 0.01 * exp(-500 * (lon_dist / pi)^2 |
| 199 | + - |
| 200 | + 400 * ((lat - 0.889036791765) / pi)^2 |
| 201 | + - |
| 202 | + 100 * ((r - radius_earth) / 30_000)^2) |
| 203 | + tracer2 = 0.009 * exp(-300 * (lon_dist / pi)^2 |
| 204 | + - |
| 205 | + 600 * ((lat - 0.4) / pi)^2 |
| 206 | + - |
| 207 | + 100 * ((r - radius_earth) / 30_000)^2) |
| 208 | + return SVector(u[1] * tracer1, u[1] * tracer2) |
| 209 | +end |
| 210 | + |
| 211 | +# source terms |
| 212 | +@inline function (setup::RadonDecaySetup)(u, x, t, |
| 213 | + tracer_equations::PassiveTracerEquations) |
| 214 | + @unpack flow_equations = tracer_equations |
| 215 | + |
| 216 | + u_flow = Trixi.flow_variables(u, tracer_equations) |
| 217 | + source_terms_flow = source_terms_coriolis(u_flow, x, t, flow_equations) + |
| 218 | + source_terms_hs_relaxation(u_flow, x, t, flow_equations) |
| 219 | + source_terms_tracer = source_terms_radon_init(u, x, setup) + |
| 220 | + source_terms_radon_decay(u, tracer_equations, setup) |
| 221 | + @info source_terms_tracer |
| 222 | + return vcat(source_terms_flow, source_terms_tracer) |
| 223 | +end |
| 224 | + |
| 225 | +############################################################################### |
| 226 | +# semidiscretization of the compressible Euler equations |
| 227 | +gamma = 1.4 |
| 228 | +flow_equations = CompressibleEulerEquationsWithGravity3D(gamma) |
| 229 | + |
| 230 | +#nproducts = 5 |
| 231 | +nproducts = 1 |
| 232 | +equations = PassiveTracerEquations(flow_equations, n_tracers = 2) |
| 233 | +radon_setup = RadonDecaySetup(; nproducts = nproducts, dt = 6.0) # rough estimate for timestep |
| 234 | + |
| 235 | +initial_condition = initial_condition_isothermal |
| 236 | + |
| 237 | +boundary_conditions = Dict(:inside => TrixiAtmo.boundary_condition_slip_wall_noncons, |
| 238 | + :outside => TrixiAtmo.boundary_condition_slip_wall_noncons) |
| 239 | + |
| 240 | +volume_flux = (FluxTracerEquationsCentral(flux_kennedy_gruber), |
| 241 | + TrixiAtmo.FluxTracerEquationsZero(flux_nonconservative_waruszewski)) |
| 242 | +#surface_flux = (FluxTracerEquationsCentral(FluxLMARS(340.0)), |
| 243 | +surface_flux = (flux_lax_friedrichs, |
| 244 | + TrixiAtmo.FluxTracerEquationsZero(flux_nonconservative_waruszewski)) |
| 245 | + |
| 246 | +# Giraldo: (10,8), polydeg 4 |
| 247 | +lat_lon_trees_per_dim = 4 |
| 248 | +layers = 2 |
| 249 | +polydeg = 3 |
| 250 | + |
| 251 | +solver = DGSEM(polydeg = polydeg, surface_flux = surface_flux, |
| 252 | + volume_integral = VolumeIntegralFluxDifferencing(volume_flux)) |
| 253 | + |
| 254 | +mesh = Trixi.T8codeMeshCubedSphere(lat_lon_trees_per_dim, layers, 6.371229e6, 30000.0, |
| 255 | + polydeg = polydeg, initial_refinement_level = 0) |
| 256 | + |
| 257 | +semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver, |
| 258 | + source_terms = radon_setup, |
| 259 | + boundary_conditions = boundary_conditions) |
| 260 | + |
| 261 | +############################################################################### |
| 262 | +# ODE solvers, callbacks etc. |
| 263 | + |
| 264 | +tspan = (0.0, 500 * 24 * 60 * 60) # time in seconds for 1 year |
| 265 | +ode = semidiscretize(semi, tspan) |
| 266 | + |
| 267 | +summary_callback = SummaryCallback() |
| 268 | + |
| 269 | +analysis_interval = 5000 |
| 270 | +#analysis_callback = AnalysisCallback(semi, interval = analysis_interval) |
| 271 | + |
| 272 | +alive_callback = AliveCallback(analysis_interval = analysis_interval) |
| 273 | + |
| 274 | +save_solution = SaveSolutionCallback(interval = 20000, |
| 275 | + save_initial_solution = true, |
| 276 | + save_final_solution = true, |
| 277 | + solution_variables = cons2prim, |
| 278 | + output_directory = "out_heldsuarez_tracer_small_llf") |
| 279 | + |
| 280 | +callbacks = CallbackSet(summary_callback, |
| 281 | + # analysis_callback, |
| 282 | + alive_callback, |
| 283 | + save_solution) |
| 284 | + |
| 285 | +############################################################################### |
| 286 | +# run the simulation |
| 287 | + |
| 288 | +# Use a Runge-Kutta method with automatic (error based) time step size control |
| 289 | +# Enable threading of the RK method for better performance on multiple threads |
| 290 | +sol = solve(ode, |
| 291 | + RDPK3SpFSAL49(); abstol = 1.0e-8, reltol = 1.0e-8, |
| 292 | + ode_default_options()..., maxiters = 1e8, |
| 293 | + callback = callbacks); |
0 commit comments