diff --git a/Project.toml b/Project.toml index 00a4caee6..417b90218 100644 --- a/Project.toml +++ b/Project.toml @@ -60,7 +60,7 @@ ArchGDAL = "0.10" Breeze = "0.4" CDSAPI = "2.2.1" CFTime = "0.1, 0.2" -ClimaSeaIce = "0.4.4, 0.5" +ClimaSeaIce = "0.5" CondaPkg = "0.2.33" CopernicusMarine = "0.1.1" CubedSphere = "0.3.4" diff --git a/docs/Project.toml b/docs/Project.toml index 4fda14c3f..0b82e6614 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -22,7 +22,6 @@ Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" XESMF = "2e0b0046-e7a1-486f-88de-807ee8ffabe5" [sources] -Breeze = {rev = "main", url = "https://github.com/NumericalEarth/Breeze.jl/"} NumericalEarth = {path = ".."} [compat] @@ -32,3 +31,4 @@ CUDA = "~6.0" Documenter = "1" DocumenterCitations = "1.3" Literate = "2.2" +ClimaSeaIce = "0.5.0" diff --git a/docs/src/NumericalEarth.bib b/docs/src/NumericalEarth.bib index 84e4628c2..f97dcd243 100644 --- a/docs/src/NumericalEarth.bib +++ b/docs/src/NumericalEarth.bib @@ -84,9 +84,9 @@ @article{holland1999modeling doi={10.1175/1520-0485(1999)029<1787:MTIOIA>2.0.CO;2} } -@article{hieronymus2021comparison, - title={A comparison of ocean-ice flux parametrizations}, - author={Hieronymus, Magnus and Holtermann, Peter and Gr{\"a}we, Ulf and Burchard, Hans}, +@article{shi2021sensitivity, + title={Sensitivity of {Northern Hemisphere} climate to ice--ocean interface heat flux parameterizations}, + author={Shi, Xiaoxu and Notz, Dirk and Liu, Jiping and Yang, Hu and Lohmann, Gerrit}, journal={Geoscientific Model Development}, volume={14}, number={8}, diff --git a/docs/src/interface_fluxes.md b/docs/src/interface_fluxes.md index b776bc475..2d0714510 100644 --- a/docs/src/interface_fluxes.md +++ b/docs/src/interface_fluxes.md @@ -817,7 +817,7 @@ where: - ``\lambda_1, \lambda_2`` are liquidus coefficients The ratio ``R = \alpha_h / \alpha_s`` (typically around 35) reflects the different molecular diffusivities of heat and -salt, with heat diffusing faster than salt [hieronymus2021comparison](@citep). +salt, with heat diffusing faster than salt [shi2021sensitivity](@citep). ```@example interface_fluxes using NumericalEarth.EarthSystemModels: ThreeEquationHeatFlux @@ -975,4 +975,4 @@ Note: The `ComponentInterfaces` call above is illustrative; it requires fully co The implementations follow: - [holland1999modeling](@citet): foundational three-equation model for ice shelf-ocean interaction -- [hieronymus2021comparison](@citet): comparison of different ocean-ice flux parameterizations +- [shi2021sensitivity](@citet): sensitivity of Northern Hemisphere climate to ice-ocean interface heat flux parameterizations diff --git a/ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl b/ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl index 45add2f8e..c6408e02d 100644 --- a/ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl +++ b/ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl @@ -31,15 +31,15 @@ boundary_layer_height(::BreezeAtmosphere) = 600 ##### function ComponentExchanger(atmosphere::BreezeAtmosphere, exchange_grid) - state = (; u = Oceananigans.CenterField(exchange_grid), - v = Oceananigans.CenterField(exchange_grid), - T = Oceananigans.CenterField(exchange_grid), - p = Oceananigans.CenterField(exchange_grid), - q = Oceananigans.CenterField(exchange_grid), + state = (; u = Oceananigans.CenterField(exchange_grid), + v = Oceananigans.CenterField(exchange_grid), + T = Oceananigans.CenterField(exchange_grid), + p = Oceananigans.CenterField(exchange_grid), + q = Oceananigans.CenterField(exchange_grid), ℐꜜˢʷ = Oceananigans.CenterField(exchange_grid), ℐꜜˡʷ = Oceananigans.CenterField(exchange_grid), - Jᶜ = Oceananigans.CenterField(exchange_grid), - Mp = Oceananigans.CenterField(exchange_grid)) + Jʳⁿ = Oceananigans.CenterField(exchange_grid), + Jˢⁿ = Oceananigans.CenterField(exchange_grid)) return ComponentExchanger(state, nothing) end @@ -52,15 +52,15 @@ end i, j = @index(Global, NTuple) @inbounds begin - state.u[i, j, 1] = u[i, j, 1] - state.v[i, j, 1] = v[i, j, 1] - state.T[i, j, 1] = T[i, j, 1] - state.q[i, j, 1] = ρqᵛᵉ[i, j, 1] / ρ₀[i, j, 1] - state.p[i, j, 1] = p₀ + state.u[i, j, 1] = u[i, j, 1] + state.v[i, j, 1] = v[i, j, 1] + state.T[i, j, 1] = T[i, j, 1] + state.q[i, j, 1] = ρqᵛᵉ[i, j, 1] / ρ₀[i, j, 1] + state.p[i, j, 1] = p₀ state.ℐꜜˢʷ[i, j, 1] = 0 state.ℐꜜˡʷ[i, j, 1] = 0 - state.Jᶜ[i, j, 1] = 0 - state.Mp[i, j, 1] = 0 + state.Jʳⁿ[i, j, 1] = 0 + state.Jˢⁿ[i, j, 1] = 0 end end diff --git a/ext/NumericalEarthSpeedyWeatherExt/speedy_weather_exchanger.jl b/ext/NumericalEarthSpeedyWeatherExt/speedy_weather_exchanger.jl index ec0dfa3e7..f7aedb525 100644 --- a/ext/NumericalEarthSpeedyWeatherExt/speedy_weather_exchanger.jl +++ b/ext/NumericalEarthSpeedyWeatherExt/speedy_weather_exchanger.jl @@ -31,14 +31,15 @@ function ComponentExchanger(atmosphere::SpeedySimulation, exchange_grid) to_atmosphere = XESMF.Regridder(exchange_grid, spectral_grid) regridder = (; to_atmosphere, from_atmosphere) - state = (; u = Field{Center, Center, Nothing}(exchange_grid), - v = Field{Center, Center, Nothing}(exchange_grid), - T = Field{Center, Center, Nothing}(exchange_grid), - p = Field{Center, Center, Nothing}(exchange_grid), - q = Field{Center, Center, Nothing}(exchange_grid), + state = (; u = Field{Center, Center, Nothing}(exchange_grid), + v = Field{Center, Center, Nothing}(exchange_grid), + T = Field{Center, Center, Nothing}(exchange_grid), + p = Field{Center, Center, Nothing}(exchange_grid), + q = Field{Center, Center, Nothing}(exchange_grid), ℐꜜˢʷ = Field{Center, Center, Nothing}(exchange_grid), ℐꜜˡʷ = Field{Center, Center, Nothing}(exchange_grid), - Jᶜ = Field{Center, Center, Nothing}(exchange_grid)) + Jʳⁿ = Field{Center, Center, Nothing}(exchange_grid), + Jˢⁿ = Field{Center, Center, Nothing}(exchange_grid)) return ComponentExchanger(state, regridder) end @@ -52,14 +53,19 @@ function interpolate_state!(exchanger, exchange_grid, atmos::SpeedySimulation, c exchange_state = exchanger.state surface_layer = atmos.model.spectral_grid.nlayers - ua = RingGrids.field_view(atmos.variables.grid.u, :, surface_layer).data - va = RingGrids.field_view(atmos.variables.grid.v, :, surface_layer).data - Ta = RingGrids.field_view(atmos.variables.grid.temperature, :, surface_layer).data - qa = RingGrids.field_view(atmos.variables.grid.humidity, :, surface_layer).data - pa = exp.(atmos.variables.grid.pressure.data) + ua = RingGrids.field_view(atmos.variables.grid.u, :, surface_layer).data + va = RingGrids.field_view(atmos.variables.grid.v, :, surface_layer).data + Ta = RingGrids.field_view(atmos.variables.grid.temperature, :, surface_layer).data + qa = RingGrids.field_view(atmos.variables.grid.humidity, :, surface_layer).data + pa = exp.(atmos.variables.grid.pressure.data) ℐꜜˢʷ = atmos.variables.parameterizations.surface_shortwave_down.data ℐꜜˡʷ = atmos.variables.parameterizations.surface_longwave_down.data - Jᶜ = atmos.variables.parameterizations.rain_rate.data .+ atmos.variables.parameterizations.snow_rate.data + Jʳⁿ = atmos.variables.parameterizations.rain_rate.data + + # `snow_rate` is only registered when SpeedyWeather's large-scale + # condensation parameterization is part of the model + Jˢⁿ = haskey(atmos.variables.parameterizations, :snow_rate) ? + atmos.variables.parameterizations.snow_rate.data : nothing regrid!(exchange_state.u, ua) regrid!(exchange_state.v, va) @@ -68,7 +74,8 @@ function interpolate_state!(exchanger, exchange_grid, atmos::SpeedySimulation, c regrid!(exchange_state.p, pa) regrid!(exchange_state.ℐꜜˢʷ, ℐꜜˢʷ) regrid!(exchange_state.ℐꜜˡʷ, ℐꜜˡʷ) - regrid!(exchange_state.Jᶜ, Jᶜ) + regrid!(exchange_state.Jʳⁿ, Jʳⁿ) + isnothing(Jˢⁿ) || regrid!(exchange_state.Jˢⁿ, Jˢⁿ) arch = architecture(exchange_grid) @@ -83,7 +90,8 @@ function interpolate_state!(exchanger, exchange_grid, atmos::SpeedySimulation, c fill_halo_regions!(exchange_state.p) fill_halo_regions!(exchange_state.ℐꜜˢʷ) fill_halo_regions!(exchange_state.ℐꜜˡʷ) - fill_halo_regions!(exchange_state.Jᶜ) + fill_halo_regions!(exchange_state.Jʳⁿ) + isnothing(Jˢⁿ) || fill_halo_regions!(exchange_state.Jˢⁿ) return nothing end diff --git a/ext/NumericalEarthVerosExt/veros_ocean_simulation.jl b/ext/NumericalEarthVerosExt/veros_ocean_simulation.jl index 041e8e05a..60c782a9b 100644 --- a/ext/NumericalEarthVerosExt/veros_ocean_simulation.jl +++ b/ext/NumericalEarthVerosExt/veros_ocean_simulation.jl @@ -29,7 +29,10 @@ Returns a NamedTuple containing package information if successful. Also patches Veros's signal handling to work with PythonCall. """ function install_veros() - CondaPkg.add("hdf5"; version="<2", channel="conda-forge") # Veros uses hdf5 version < 2. Therefore we pin it to prevent dependency issues + # Veros uses hdf5 < 2. The shenanigans in the following two lines + # are necessary to allow loading compatible hdf5 and h5py. + CondaPkg.add("hdf5"; version="<2", channel="conda-forge") + CondaPkg.add("h5py"; version=">=3.0,<3.13", channel="conda-forge") CondaPkg.add_pip("veros", version="@ https://github.com/team-ocean/veros/archive/refs/heads/main.zip") cli = CondaPkg.which("veros") diff --git a/src/Atmospheres/Atmospheres.jl b/src/Atmospheres/Atmospheres.jl index c7aef6a37..e4e4180f8 100644 --- a/src/Atmospheres/Atmospheres.jl +++ b/src/Atmospheres/Atmospheres.jl @@ -1,6 +1,6 @@ module Atmospheres -export atmosphere_simulation, PrescribedAtmosphere +export atmosphere_simulation, PrescribedAtmosphere, PrescribedPrecipitationFlux using Oceananigans using Oceananigans.Fields: Center diff --git a/src/Atmospheres/interpolate_atmospheric_state.jl b/src/Atmospheres/interpolate_atmospheric_state.jl index 2f0b365ec..b781b3c9c 100644 --- a/src/Atmospheres/interpolate_atmospheric_state.jl +++ b/src/Atmospheres/interpolate_atmospheric_state.jl @@ -27,7 +27,8 @@ function interpolate_state!(exchanger, grid, atmosphere::PrescribedAtmosphere, c atmosphere_tracers = (T = atmosphere.tracers.T.data, q = atmosphere.tracers.q.data) - freshwater_flux = map(ϕ -> ϕ.data, atmosphere.freshwater_flux) + rainfall_flux = surface_rainfall_flux(atmosphere) + snowfall_flux = surface_snowfall_flux(atmosphere) atmosphere_pressure = atmosphere.pressure.data # Extract info for time-interpolation @@ -41,12 +42,13 @@ function interpolate_state!(exchanger, grid, atmosphere::PrescribedAtmosphere, c # Simplify NamedTuple to reduce parameter space consumption. # See https://github.com/CliMA/NumericalEarth.jl/issues/116. - atmosphere_data = (u = atmosphere_fields.u.data, - v = atmosphere_fields.v.data, - T = atmosphere_fields.T.data, - p = atmosphere_fields.p.data, - q = atmosphere_fields.q.data, - Jᶜ = atmosphere_fields.Jᶜ.data) + atmosphere_data = (u = atmosphere_fields.u.data, + v = atmosphere_fields.v.data, + T = atmosphere_fields.T.data, + p = atmosphere_fields.p.data, + q = atmosphere_fields.q.data, + Jʳⁿ = atmosphere_fields.Jʳⁿ.data, + Jˢⁿ = atmosphere_fields.Jˢⁿ.data) kernel_parameters = interface_kernel_parameters(grid) @@ -67,7 +69,8 @@ function interpolate_state!(exchanger, grid, atmosphere::PrescribedAtmosphere, c atmosphere_velocities, atmosphere_tracers, atmosphere_pressure, - freshwater_flux, + rainfall_flux, + snowfall_flux, atmosphere_backend, atmosphere_time_indexing) @@ -93,7 +96,8 @@ end atmos_velocities, atmos_tracers, atmos_pressure, - prescribed_freshwater_flux, + rainfall_flux, + snowfall_flux, atmos_backend, atmos_time_indexing) @@ -114,8 +118,8 @@ end qᵃᵗ = interp_atmos_time_series(atmos_tracers.q, atmos_args...) pᵃᵗ = interp_atmos_time_series(atmos_pressure, atmos_args...) - # Usually precipitation - Mh = interp_atmos_time_series(prescribed_freshwater_flux, atmos_args...) + Mr = interp_atmos_time_series(rainfall_flux, atmos_args...) + Ms = interp_atmos_time_series(snowfall_flux, atmos_args...) # Convert atmosphere velocities (usually defined on a latitude-longitude grid) to # the frame of reference of the native grid @@ -128,7 +132,8 @@ end surface_atmos_state.T[i, j, 1] = Tᵃᵗ surface_atmos_state.p[i, j, 1] = pᵃᵗ surface_atmos_state.q[i, j, 1] = qᵃᵗ - surface_atmos_state.Jᶜ[i, j, 1] = Mh + surface_atmos_state.Jʳⁿ[i, j, 1] = Mr + surface_atmos_state.Jˢⁿ[i, j, 1] = Ms end end @@ -136,9 +141,11 @@ end ##### Utility for interpolating tuples of fields ##### +@inline interp_atmos_time_series(::Nothing, X, time, grid, args...) = 0 + # Note: assumes loc = (c, c, nothing) (and the third location should not matter.) -@inline interp_atmos_time_series(J::AbstractArray, x_itp::FractionalIndices, t_itp, args...) = - interpolate(x_itp, t_itp, J, args...) +@inline interp_atmos_time_series(J::AbstractArray, X::FractionalIndices, time, args...) = + interpolate(X, time, J, args...) @inline interp_atmos_time_series(J::AbstractArray, X, time, grid, args...) = interpolate(X, time, J, (Center(), Center(), nothing), grid, args...) diff --git a/src/Atmospheres/prescribed_atmosphere.jl b/src/Atmospheres/prescribed_atmosphere.jl index bbb5158ca..8ce091d5d 100644 --- a/src/Atmospheres/prescribed_atmosphere.jl +++ b/src/Atmospheres/prescribed_atmosphere.jl @@ -42,12 +42,45 @@ function default_atmosphere_tracers(grid, times) return (T=Ta, q=qa) end +""" + PrescribedPrecipitationFlux(; rain=nothing, snow=nothing) + PrescribedPrecipitationFlux(rain, snow) + +Container for prescribed precipitation fluxes. Either component may be `nothing` +to indicate that the corresponding precipitation type is not represented by the +atmosphere (e.g. rain-only datasets). Used as the `freshwater_flux` of a +`PrescribedAtmosphere`; downstream callers query the snow component via +[`surface_snowfall_flux`](@ref) so that prognostic atmospheres with or without +snow can dispatch on this type as well. +""" +struct PrescribedPrecipitationFlux{R, S} + rain :: R + snow :: S +end + +PrescribedPrecipitationFlux(; rain=nothing, snow=nothing) = + PrescribedPrecipitationFlux(rain, snow) + +Adapt.adapt_structure(to, ff::PrescribedPrecipitationFlux) = + PrescribedPrecipitationFlux(adapt(to, ff.rain), adapt(to, ff.snow)) + function default_freshwater_flux(grid, times) rain = FieldTimeSeries{Center, Center, Nothing}(grid, times) snow = FieldTimeSeries{Center, Center, Nothing}(grid, times) - return (; rain, snow) + return PrescribedPrecipitationFlux(rain, snow) end +@inline field_data(::Nothing) = nothing +@inline field_data(field) = field.data + +@inline surface_snowfall_flux(::Nothing) = nothing +@inline surface_snowfall_flux(atmos::PrescribedAtmosphere) = surface_snowfall_flux(atmos.freshwater_flux) +@inline surface_snowfall_flux(ff::PrescribedPrecipitationFlux) = field_data(ff.snow) + +@inline surface_rainfall_flux(::Nothing) = nothing +@inline surface_rainfall_flux(atmos::PrescribedAtmosphere) = surface_rainfall_flux(atmos.freshwater_flux) +@inline surface_rainfall_flux(ff::PrescribedPrecipitationFlux) = field_data(ff.rain) + """ The standard unit of atmospheric pressure; 1 standard atmosphere (atm) = 101,325 Pascals (Pa) in SI units. This is approximately equal to the mean sea-level atmospheric pressure on Earth. """ function default_atmosphere_pressure(grid, times) diff --git a/src/Atmospheres/prescribed_atmosphere_regridder.jl b/src/Atmospheres/prescribed_atmosphere_regridder.jl index 9ed5ebe1f..e281b6b0e 100644 --- a/src/Atmospheres/prescribed_atmosphere_regridder.jl +++ b/src/Atmospheres/prescribed_atmosphere_regridder.jl @@ -2,12 +2,13 @@ function ComponentExchanger(atmosphere::PrescribedAtmosphere, grid) regridder = atmosphere_regridder(atmosphere, grid) - state = (; u = Field{Center, Center, Nothing}(grid), - v = Field{Center, Center, Nothing}(grid), - T = Field{Center, Center, Nothing}(grid), - p = Field{Center, Center, Nothing}(grid), - q = Field{Center, Center, Nothing}(grid), - Jᶜ = Field{Center, Center, Nothing}(grid)) + state = (; u = Field{Center, Center, Nothing}(grid), + v = Field{Center, Center, Nothing}(grid), + T = Field{Center, Center, Nothing}(grid), + p = Field{Center, Center, Nothing}(grid), + q = Field{Center, Center, Nothing}(grid), + Jʳⁿ = Field{Center, Center, Nothing}(grid), + Jˢⁿ = Field{Center, Center, Nothing}(grid)) return ComponentExchanger(state, regridder) end diff --git a/src/DataWrangling/ECCO/ECCO_atmosphere.jl b/src/DataWrangling/ECCO/ECCO_atmosphere.jl index 6f54ea1d4..671bd3294 100644 --- a/src/DataWrangling/ECCO/ECCO_atmosphere.jl +++ b/src/DataWrangling/ECCO/ECCO_atmosphere.jl @@ -1,6 +1,6 @@ using NumericalEarth.DataWrangling: DatasetBackend using Oceananigans.OutputReaders -using NumericalEarth.Atmospheres: PrescribedAtmosphere +using NumericalEarth.Atmospheres: PrescribedAtmosphere, PrescribedPrecipitationFlux """ ECCOPrescribedAtmosphere([architecture = CPU(), FT = Float32]; @@ -51,7 +51,7 @@ function ECCOPrescribedAtmosphere(architecture = CPU(), FT = Float32; pa = FieldTimeSeries(pa_meta, architecture; kw...) Fr = FieldTimeSeries(Fr_meta, architecture; kw...) - freshwater_flux = (; rain = Fr) + freshwater_flux = PrescribedPrecipitationFlux(rain = Fr) times = ua.times grid = ua.grid diff --git a/src/DataWrangling/JRA55/JRA55.jl b/src/DataWrangling/JRA55/JRA55.jl index 57cdcd6b4..841e84c43 100644 --- a/src/DataWrangling/JRA55/JRA55.jl +++ b/src/DataWrangling/JRA55/JRA55.jl @@ -19,7 +19,7 @@ using Oceananigans.OutputReaders: Cyclical, TotallyInMemory, AbstractInMemoryBac using NumericalEarth -using NumericalEarth.Atmospheres: PrescribedAtmosphere +using NumericalEarth.Atmospheres: PrescribedAtmosphere, PrescribedPrecipitationFlux using NumericalEarth.Radiations: PrescribedRadiation, SurfaceRadiationProperties, default_stefan_boltzmann_constant using GPUArraysCore: @allowscalar diff --git a/src/DataWrangling/JRA55/JRA55_prescribed_atmosphere.jl b/src/DataWrangling/JRA55/JRA55_prescribed_atmosphere.jl index 373566c54..9d1d0cc83 100644 --- a/src/DataWrangling/JRA55/JRA55_prescribed_atmosphere.jl +++ b/src/DataWrangling/JRA55/JRA55_prescribed_atmosphere.jl @@ -41,8 +41,7 @@ function JRA55PrescribedAtmosphere(architecture = CPU(), FT = Float32; Fra = JRA55FieldTimeSeries(:rain_freshwater_flux, architecture, FT; kw...) Fsn = JRA55FieldTimeSeries(:snow_freshwater_flux, architecture, FT; kw...) - freshwater_flux = (rain = Fra, - snow = Fsn) + freshwater_flux = PrescribedPrecipitationFlux(rain = Fra, snow = Fsn) times = ua.times grid = ua.grid diff --git a/src/DataWrangling/OSPapa/OSPapa.jl b/src/DataWrangling/OSPapa/OSPapa.jl index 056145019..ccece9cb9 100644 --- a/src/DataWrangling/OSPapa/OSPapa.jl +++ b/src/DataWrangling/OSPapa/OSPapa.jl @@ -15,7 +15,7 @@ using Downloads using Thermodynamics: q_vap_from_RH, Liquid using NumericalEarth.DataWrangling: download_progress -using NumericalEarth.Atmospheres: PrescribedAtmosphere, AtmosphereThermodynamicsParameters +using NumericalEarth.Atmospheres: PrescribedAtmosphere, PrescribedPrecipitationFlux, AtmosphereThermodynamicsParameters using NumericalEarth.Oceans: reference_density, heat_capacity using NumericalEarth.DataWrangling: diff --git a/src/DataWrangling/OSPapa/OSPapa_prescribed_atmosphere.jl b/src/DataWrangling/OSPapa/OSPapa_prescribed_atmosphere.jl index 9990eee11..180d0c69e 100644 --- a/src/DataWrangling/OSPapa/OSPapa_prescribed_atmosphere.jl +++ b/src/DataWrangling/OSPapa/OSPapa_prescribed_atmosphere.jl @@ -79,7 +79,7 @@ function OSPapaPrescribedAtmosphere(architecture = CPU(), FT = Float32; velocities = (u=ua, v=va), tracers = (T=Ta, q=qa), pressure = Pa, - freshwater_flux = (; rain), + freshwater_flux = PrescribedPrecipitationFlux(; rain), thermodynamics_parameters = thermo_params, surface_layer_height = convert(FT, surface_layer_height)) end diff --git a/src/EarthSystemModels/InterfaceComputations/atmosphere_sea_ice_fluxes.jl b/src/EarthSystemModels/InterfaceComputations/atmosphere_sea_ice_fluxes.jl index 420b6ca82..7f9c7f5f4 100644 --- a/src/EarthSystemModels/InterfaceComputations/atmosphere_sea_ice_fluxes.jl +++ b/src/EarthSystemModels/InterfaceComputations/atmosphere_sea_ice_fluxes.jl @@ -89,15 +89,17 @@ end # Sea ice properties uˢⁱ = zero(FT) # ℑxᶜᵃᵃ(i, j, 1, grid, interior_state.u) vˢⁱ = zero(FT) # ℑyᵃᶜᵃ(i, j, 1, grid, interior_state.v) - hˢⁱ = interior_state.h[i, j, 1] + hˢⁱ = interior_state.hi[i, j, 1] + hˢⁿ = interior_state.hs[i, j, 1] hc = interior_state.hc[i, j, 1] ℵᵢ = interior_state.ℵ[i, j, 1] Tₛ = interface_temperature[i, j, 1] Tₛ = convert_to_kelvin(sea_ice_properties.temperature_units, Tₛ) end + # Build thermodynamic and dynamic states in the atmosphere and interface. ℂᵃᵗ = atmosphere_properties.thermodynamics_parameters - zᵃᵗ = atmosphere_properties.surface_layer_height # elevation of atmos variables relative to interface + zᵃᵗ = atmosphere_properties.surface_layer_height local_atmosphere_state = (z = zᵃᵗ, u = uᵃᵗ, @@ -107,7 +109,7 @@ end q = qᵃᵗ, h_bℓ = atmosphere_state.h_bℓ) - local_interior_state = (u=uˢⁱ, v=vˢⁱ, T=Tᵒᶜ, S=Sᵒᶜ, h=hˢⁱ, hc=hc) + local_interior_state = (u=uˢⁱ, v=vˢⁱ, T=Tᵒᶜ, S=Sᵒᶜ, hi=hˢⁱ, hs=hˢⁿ, hc=hc) # Local radiative state at this cell. Returns zero-valued state when # radiation is off. diff --git a/src/EarthSystemModels/InterfaceComputations/component_interfaces.jl b/src/EarthSystemModels/InterfaceComputations/component_interfaces.jl index f97404b09..11c2dd9bb 100644 --- a/src/EarthSystemModels/InterfaceComputations/component_interfaces.jl +++ b/src/EarthSystemModels/InterfaceComputations/component_interfaces.jl @@ -255,7 +255,12 @@ function atmosphere_sea_ice_interface(grid, temperature_formulation, velocity_formulation) - interface_temperature = sea_ice.model.ice_thermodynamics.top_surface_temperature + snow_thermo = sea_ice.model.snow_thermodynamics + interface_temperature = if isnothing(snow_thermo) + sea_ice.model.ice_thermodynamics.top_surface_temperature + else + snow_thermo.top_surface_temperature + end return AtmosphereInterface(fluxes, ai_flux_formulation, interface_temperature, properties) end @@ -394,7 +399,7 @@ function ComponentInterfaces(atmosphere, ocean, sea_ice=nothing; sea_ice_properties = (reference_density = sea_ice_reference_density, heat_capacity = sea_ice_heat_capacity, freshwater_density = freshwater_density, - liquidus = sea_ice.model.ice_thermodynamics.phase_transitions.liquidus, + liquidus = sea_ice.model.phase_transitions.liquidus, temperature_units = sea_ice_temperature_units) else sea_ice_properties = nothing diff --git a/src/EarthSystemModels/InterfaceComputations/interface_states.jl b/src/EarthSystemModels/InterfaceComputations/interface_states.jl index 255f00bbb..34642667b 100644 --- a/src/EarthSystemModels/InterfaceComputations/interface_states.jl +++ b/src/EarthSystemModels/InterfaceComputations/interface_states.jl @@ -254,60 +254,85 @@ end Jᵀ = Qa * λ # Calculating the atmospheric temperature - # We use to compute the sensible heat flux Tᵃᵗ = surface_atmosphere_temperature(Ψₐ, ℙₐ) ΔT = Tᵃᵗ - Ψₛ.T - Ωc = ifelse(ΔT == 0, zero(ΔT), 𝒬ᵀ / ΔT * λ) # Sensible heat transfer coefficient (W/m²K) - # Computing the flux balance temperature - return (Ψᵢ.T * F.κ - (Jᵀ + Ωc * Tᵃᵗ) * F.δ) / (F.κ - Ωc * F.δ) + # Flux balance: T★ = (Tᵢ κ - (Jᵀ + Ωc Tᵃᵗ) δ) / (κ - Ωc δ) + # where Ωc = 𝒬ᵀ λ / ΔT. Multiply through by ΔT to avoid Inf when ΔT → 0. + Ωᵀ = 𝒬ᵀ * λ # unnormalized sensible heat coefficient (= Ωc * ΔT) + D = F.κ * ΔT - Ωᵀ * F.δ + T★ = (Ψᵢ.T * F.κ * ΔT - (Jᵀ * ΔT + Ωᵀ * Tᵃᵗ) * F.δ) / D + + return ifelse(D == 0, Ψₛ.T, T★) end -# 𝒬ᵛ + ℐꜛˡʷ + Qd + Ωc * (Tᵃᵗ - Tˢ) + k / h * (Tˢ - Tˢⁱ) = 0 -# where Ωc (the sensible heat transfer coefficient) is given by Ωc = 𝒬ᵀ / (Tᵃᵗ - Tˢ) -# ⟹ Tₛ = (Tˢⁱ * k - (𝒬ᵛ + ℐꜛˡʷ + Qd + Ωc * Tᵃᵗ) * h / (k - Ωc * h) -@inline function flux_balance_temperature(st::SkinTemperature{<:ClimaSeaIce.ConductiveFlux}, Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) - F = st.internal_flux - k = F.conductivity - h = Ψᵢ.h - hc = Ψᵢ.hc # Critical thickness for ice consolidation - - # Bottom temperature at the melting temperature - Tˢⁱ = ClimaSeaIce.SeaIceThermodynamics.melting_temperature(ℙᵢ.liquidus, Ψᵢ.S) - Tˢⁱ = convert_to_kelvin(ℙᵢ.temperature_units, Tˢⁱ) +# Solve the surface flux balance equation: +# Qa(Tₛ) + Ωc (Tᵃᵗ - Tₛ) + (Tₛ - Tᵦ) / R = 0 +# where R is the total thermal resistance (h/k for bare ice, hₛ/kₛ + hᵢ/kᵢ with snow), +# Ωc = 𝒬ᵀ/(Tᵃᵗ-Tₛ) is the linearized sensible heat coefficient, and Qa = 𝒬ᵛ + ℐꜛˡʷ + Qd. +# The upward longwave ℐꜛˡʷ = σ ε Tₛ⁴ is strongly nonlinear in Tₛ; a pure Picard +# iteration (treating Qa constant) is unstable when 4σεTₛ³ ≳ 1/R (radiation +# dominated). We linearize: Qa(Tₛ) ≈ Qa(Tₛ⁻) + β (Tₛ − Tₛ⁻) with β = 4σεTₛ⁻³, +# yielding the Newton-like semi-implicit update: +# Tₛ = [Tᵦ + β R Tₛ⁻ - Ωc R Tᵃᵗ - Qa R] / [1 + β R - Ωc R] +@inline function conductive_flux_balance_temperature(st, R, Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) + hᵢ = Ψᵢ.hi + hc = Ψᵢ.hc + + # Bottom temperature at the melting point + Tᵦ = ClimaSeaIce.SeaIceThermodynamics.melting_temperature(ℙᵢ.liquidus, Ψᵢ.S) + Tᵦ = convert_to_kelvin(ℙᵢ.temperature_units, Tᵦ) Tₛ⁻ = Ψₛ.T - # Calculating the atmospheric temperature - # We use to compute the sensible heat flux Tᵃᵗ = surface_atmosphere_temperature(Ψₐ, ℙₐ) ΔT = Tᵃᵗ - Tₛ⁻ - Ωc = ifelse(ΔT == 0, zero(h), 𝒬ᵀ / ΔT) # Sensible heat transfer coefficient (W/m²K) - Qa = (𝒬ᵛ + ℐꜛˡʷ + Qd) # Net flux excluding sensible heat (positive out of the ocean) + Qa = 𝒬ᵛ + ℐꜛˡʷ + Qd + + # Sensible transfer coefficient Ωc = 𝒬ᵀ/ΔT, safely handling ΔT → 0. + Ωc = ifelse(ΔT == zero(ΔT), zero(Tₛ⁻), 𝒬ᵀ / ΔT) - # Computing the flux balance temperature - T★ = (Tˢⁱ * k - (Qa + Ωc * Tᵃᵗ) * h) / (k - Ωc * h) + # Newton linearization of upwelling longwave: ℐꜛˡʷ(Tₛ) ≈ ℐꜛˡʷ(Tₛ⁻) + β (Tₛ − Tₛ⁻). + # Since ℐꜛˡʷ = σ ϵ Tₛ⁻⁴, we have β = 4 σ ϵ Tₛ⁻³ = 4 ℐꜛˡʷ / Tₛ⁻. + β = 4 * ℐꜛˡʷ / Tₛ⁻ - # Fix a NaN + # Flux balance solution with T⁴ linearization (stable even at ΔT = 0): + D = 1 + β * R - Ωc * R + T★ = (Tᵦ + β * R * Tₛ⁻ - Ωc * R * Tᵃᵗ - Qa * R) / D + T★ = ifelse(D == 0, Tₛ⁻, T★) T★ = ifelse(isnan(T★), Tₛ⁻, T★) - # To prevent instabilities in the fixed point iteration - # solver we cap the maximum temperature difference with `max_ΔT` + # Cap the temperature step for iteration stability ΔT★ = T★ - Tₛ⁻ max_ΔT = convert(typeof(T★), st.max_ΔT) - abs_ΔT = min(max_ΔT, abs(ΔT★)) - Tₛ⁺ = Tₛ⁻ + abs_ΔT * sign(ΔT★) + Tₛ⁺ = Tₛ⁻ + clamp(ΔT★, -max_ΔT, max_ΔT) - # Under heating fluxes, cap surface temperature by melting temperature + # Cap at melting temperature Tₘ = ℙᵢ.liquidus.freshwater_melting_temperature Tₘ = convert_to_kelvin(ℙᵢ.temperature_units, Tₘ) Tₛ⁺ = min(Tₛ⁺, Tₘ) - # If the ice is not consolidated, use the bottom temperature - Tₛ⁺ = ifelse(h ≥ hc, Tₛ⁺, Tˢⁱ) - + # If ice is not consolidated, use the bottom temperature + Tₛ⁺ = ifelse(hᵢ ≥ hc, Tₛ⁺, Tᵦ) + return Tₛ⁺ end +# Bare ice: R = hᵢ / kᵢ +@inline function flux_balance_temperature(st::SkinTemperature{<:ClimaSeaIce.ConductiveFlux}, + Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) + k = st.internal_flux.conductivity + R = Ψᵢ.hi / k + return conductive_flux_balance_temperature(st, R, Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) +end + +# Snow + ice: R = hₛ / kₛ + hᵢ / kᵢ +@inline function flux_balance_temperature(st::SkinTemperature{<:ClimaSeaIce.SeaIceThermodynamics.IceSnowConductiveFlux}, + Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) + F = st.internal_flux + R = Ψᵢ.hs / F.snow_conductivity + Ψᵢ.hi / F.ice_conductivity + return conductive_flux_balance_temperature(st, R, Ψₛ, ℙₛ, 𝒬ᵀ, 𝒬ᵛ, ℐꜛˡʷ, Qd, Ψᵢ, ℙᵢ, Ψₐ, ℙₐ) +end + @inline function compute_interface_temperature(st::SkinTemperature, interface_state, atmosphere_state, diff --git a/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_fluxes.jl b/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_fluxes.jl index a715493d3..f8d5e30a4 100644 --- a/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_fluxes.jl +++ b/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_fluxes.jl @@ -36,7 +36,7 @@ function compute_sea_ice_ocean_fluxes!(interface, ocean, sea_ice, ocean_properti hˢⁱ = sea_ice.model.ice_thickness hc = sea_ice.model.ice_consolidation_thickness - phase_transitions = sea_ice.model.ice_thermodynamics.phase_transitions + phase_transitions = sea_ice.model.phase_transitions liquidus = phase_transitions.liquidus L = phase_transitions.reference_latent_heat @@ -175,12 +175,12 @@ end qᶠ = δ𝒬ᶠʳᶻ / ℰ @inbounds begin - Tᴺ = Tᵒᶜ[i, j, Nz] - Sᴺ = Sᵒᶜ[i, j, Nz] + Tᴺ = Tᵒᶜ[i, j, Nz] + Sᴺ = Sᵒᶜ[i, j, Nz] Sˢⁱ = ice_salinity[i, j, 1] hˢⁱ = ice_thickness[i, j, 1] - ℵᵢ = ice_concentration[i, j, 1] - hc = ice_consolidation_thickness[i, j, 1] + ℵᵢ = ice_concentration[i, j, 1] + hc = ice_consolidation_thickness[i, j, 1] end # Extract internal temperature (for ConductiveFluxTEF, zero otherwise) @@ -198,8 +198,8 @@ end # ============================================= # Returns interfacial heat flux, melt rate qᵐ, and interface T, S 𝒬ⁱᵒ, qᵐ, Tᵦ, Sᵦ = compute_interface_heat_flux(flux_formulation, - ocean_surface_state, ice_state, - liquidus, ocean_properties, ℰ, u★) + ocean_surface_state, ice_state, + liquidus, ocean_properties, ℰ, u★) # Store interface values and heat flux @inbounds 𝒬ⁱⁿᵗ[i, j, 1] = 𝒬ⁱᵒ diff --git a/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_heat_flux_formulations.jl b/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_heat_flux_formulations.jl index ebf4f4ce8..915fa64b8 100644 --- a/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_heat_flux_formulations.jl +++ b/src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_heat_flux_formulations.jl @@ -113,8 +113,8 @@ References - [holland1999modeling](@citet): Holland, D. M., & Jenkins, A. (1999). Modeling thermodynamic ice–ocean interactions at the base of an ice shelf. *Journal of Physical Oceanography*, 29(8), 1787-1800. -- [hieronymus2021comparison](@citet): Hieronymus, M., et al. (2021). A comparison of ocean-ice flux parametrizations. - *Geosci. Model Dev.*, 14, 4891-4908. +- [shi2021sensitivity](@citet): Shi, X., Notz, D., Liu, J., Yang, H., & Lohmann, G. (2021). Sensitivity of Northern + Hemisphere climate to ice-ocean interface heat flux parameterizations. *Geosci. Model Dev.*, 14, 4891-4908. """ struct ThreeEquationHeatFlux{F, T, FT, U} conductive_flux :: F @@ -139,7 +139,7 @@ Adapt.adapt_structure(to, f::ThreeEquationHeatFlux) = Construct a `ThreeEquationHeatFlux` with the specified parameters. -Default values follow [hieronymus2021comparison](@citet) with ``R = \\alpha_h / \\alpha_s = 35``. +Default values follow [shi2021sensitivity](@citet) with ``R = \\alpha_h / \\alpha_s = 35``. Keyword Arguments ================= diff --git a/src/EarthSystemModels/earth_system_model.jl b/src/EarthSystemModels/earth_system_model.jl index 6cc3bb84b..c4048e8b5 100644 --- a/src/EarthSystemModels/earth_system_model.jl +++ b/src/EarthSystemModels/earth_system_model.jl @@ -303,7 +303,7 @@ function above_freezing_ocean_temperature!(ocean, grid, sea_ice) T = ocean_temperature(ocean) S = ocean_salinity(ocean) ℵ = sea_ice_concentration(sea_ice) - liquidus = sea_ice.model.ice_thermodynamics.phase_transitions.liquidus + liquidus = sea_ice.model.phase_transitions.liquidus arch = architecture(grid) launch!(arch, grid, :xy, _above_freezing_ocean_temperature!, T, grid, S, ℵ, liquidus) diff --git a/src/Oceans/assemble_net_ocean_fluxes.jl b/src/Oceans/assemble_net_ocean_fluxes.jl index 6c364a434..d2171e946 100644 --- a/src/Oceans/assemble_net_ocean_fluxes.jl +++ b/src/Oceans/assemble_net_ocean_fluxes.jl @@ -31,7 +31,8 @@ function update_net_ocean_fluxes!(coupled_model, ocean_model, grid) sea_ice_ocean_fluxes = computed_fluxes(coupled_model.interfaces.sea_ice_ocean_interface) atmosphere_fields = coupled_model.interfaces.exchanger.atmosphere.state - freshwater_flux = atmosphere_fields.Jᶜ.data + rainfall_flux = atmosphere_fields.Jʳⁿ.data + snowfall_flux = atmosphere_fields.Jˢⁿ.data # Extract land freshwater flux if land component is present land_exchanger = coupled_model.interfaces.exchanger.land @@ -50,7 +51,8 @@ function update_net_ocean_fluxes!(coupled_model, ocean_model, grid) sea_ice_ocean_fluxes, ocean_surface_salinity, ice_concentration, - freshwater_flux, + rainfall_flux, + snowfall_flux, land_freshwater_flux, ocean_properties) @@ -67,7 +69,8 @@ Base.@propagate_inbounds get_land_freshwater_flux(i, j, flux) = flux[i, j, 1] sea_ice_ocean_fluxes, ocean_surface_salinity, sea_ice_concentration, - freshwater_flux, + rainfall_flux, + snowfall_flux, land_freshwater_flux, ocean_properties) @@ -82,7 +85,9 @@ Base.@propagate_inbounds get_land_freshwater_flux(i, j, flux) = flux[i, j, 1] ℵᵢ = sea_ice_concentration[i, j, 1] Sᵒᶜ = ocean_surface_salinity[i, j, 1] - Jᶜ = freshwater_flux[i, j, 1] + get_land_freshwater_flux(i, j, land_freshwater_flux) + Jʳⁿ = rainfall_flux[i, j, 1] + Jˢⁿ = snowfall_flux[i, j, 1] + Jˡⁿ = get_land_freshwater_flux(i, j, land_freshwater_flux) 𝒬ᵀ = atmos_ocean_fluxes.sensible_heat[i, j, 1] 𝒬ᵛ = atmos_ocean_fluxes.latent_heat[i, j, 1] Jᵛ = atmos_ocean_fluxes.water_vapor[i, j, 1] @@ -91,13 +96,13 @@ Base.@propagate_inbounds get_land_freshwater_flux(i, j, flux) = flux[i, j, 1] # Turbulent contributions to surface heat flux (radiation added later) ΣQao = (𝒬ᵀ + 𝒬ᵛ) * (1 - ℵᵢ) - # Convert mass flux to volume flux; sign-flip (prescribed flux is positive down) + # Freshwater flux to the ocean per unit cell area (volume flux, positive up = leaving ocean): + # - rain and land runoff reach the ocean everywhere (rain runs through cracks in ice) + # - snow only reaches the ocean through the open-water fraction (1 - ℵᵢ); + # - evaporation acts only over the open-water fraction (1 - ℵᵢ) + # The atmospheric mass-flux convention is positive down; Jᵛ is positive up. ρᵒᶜ⁻¹ = 1 / ocean_properties.reference_density - ΣFao = - Jᶜ * ρᵒᶜ⁻¹ - - # Add turbulent water vapor flux (positive upward sign convention) - Jᵛᵒᶜ = Jᵛ * ρᵒᶜ⁻¹ - ΣFao += Jᵛᵒᶜ + ΣFao = - (Jʳⁿ + Jˡⁿ + (1 - ℵᵢ) * Jˢⁿ) * ρᵒᶜ⁻¹ + (1 - ℵᵢ) * Jᵛ * ρᵒᶜ⁻¹ τˣ = net_ocean_fluxes.u τʸ = net_ocean_fluxes.v @@ -126,6 +131,6 @@ Base.@propagate_inbounds get_land_freshwater_flux(i, j, flux) = flux[i, j, 1] # Tracer fluxes — radiative contributions added later by apply_air_sea_radiative_fluxes! Jᵀ[i, j, 1] = ifelse(inactive, zero(grid), Jᵀao + Jᵀio) - Jˢ[i, j, 1] = ifelse(inactive, zero(grid), (1 - ℵᵢ) * Jˢao + Jˢio) + Jˢ[i, j, 1] = ifelse(inactive, zero(grid), Jˢao + Jˢio) end end diff --git a/src/Radiations/apply_air_sea_radiative_fluxes.jl b/src/Radiations/apply_air_sea_radiative_fluxes.jl index 60e018995..146ca7c37 100644 --- a/src/Radiations/apply_air_sea_radiative_fluxes.jl +++ b/src/Radiations/apply_air_sea_radiative_fluxes.jl @@ -27,7 +27,7 @@ function apply_air_sea_radiative_fluxes!(coupled_model::EarthSystemModel) isnothing(interface_fluxes) && return nothing haskey(interface_fluxes, :ocean) || return nothing - grid = ocean.model.grid + grid = coupled_model.interfaces.exchanger.grid arch = architecture(grid) clock = coupled_model.clock @@ -40,7 +40,7 @@ function apply_air_sea_radiative_fluxes!(coupled_model::EarthSystemModel) interface_temperature = ao_interface.temperature ocean_properties = coupled_model.interfaces.ocean_properties - penetrating_radiation = get_radiative_forcing(ocean.model) + penetrating_radiation = get_radiative_forcing(ocean) launch!(arch, grid, :xy, _apply_air_sea_radiative_fluxes!, diff --git a/src/SeaIces/SeaIces.jl b/src/SeaIces/SeaIces.jl index 57909bdc8..2f16a4f46 100644 --- a/src/SeaIces/SeaIces.jl +++ b/src/SeaIces/SeaIces.jl @@ -44,24 +44,31 @@ interpolate_state!(exchanger, grid, ::FreezingLimitedOceanTemperature, coupled_m # ComponentExchangers ComponentExchanger(sea_ice::FreezingLimitedOceanTemperature, grid) = nothing -function ComponentExchanger(sea_ice::Simulation{<:SeaIceModel}, grid) +function ComponentExchanger(sea_ice::Simulation{<:SeaIceModel}, grid) sea_ice_grid = sea_ice.model.grid - + if sea_ice_grid == grid u = sea_ice.model.velocities.u v = sea_ice.model.velocities.v - h = sea_ice.model.ice_thickness + hi = sea_ice.model.ice_thickness hc = sea_ice.model.ice_consolidation_thickness ℵ = sea_ice.model.ice_concentration + hs = sea_ice.model.snow_thickness else u = Field{Center, Center, Nothing}(grid) v = Field{Center, Center, Nothing}(grid) - h = Field{Center, Center, Nothing}(grid) + hi = Field{Center, Center, Nothing}(grid) hc = Field{Center, Center, Nothing}(grid) ℵ = Field{Center, Center, Nothing}(grid) + hs = Field{Center, Center, Nothing}(grid) + end + + # When there's no snow model, use ZeroField so kernels can read hs[i,j,1] = 0 + if isnothing(hs) + hs = ZeroField(eltype(grid)) end - return ComponentExchanger((; u, v, h, hc, ℵ), nothing) + return ComponentExchanger((; u, v, hi, hc, ℵ, hs), nothing) end end diff --git a/src/SeaIces/assemble_net_sea_ice_fluxes.jl b/src/SeaIces/assemble_net_sea_ice_fluxes.jl index 0021cbffb..5c5974469 100644 --- a/src/SeaIces/assemble_net_sea_ice_fluxes.jl +++ b/src/SeaIces/assemble_net_sea_ice_fluxes.jl @@ -16,7 +16,7 @@ function update_net_fluxes!(coupled_model, sea_ice::Simulation{<:SeaIceModel}) atmosphere_sea_ice_fluxes = computed_fluxes(coupled_model.interfaces.atmosphere_sea_ice_interface) atmosphere_fields = coupled_model.interfaces.exchanger.atmosphere.state - freshwater_flux = atmosphere_fields.Jᶜ.data + snowfall_flux = atmosphere_fields.Jˢⁿ.data sea_ice_properties = coupled_model.interfaces.sea_ice_properties ice_concentration = sea_ice_concentration(sea_ice) @@ -29,7 +29,7 @@ function update_net_fluxes!(coupled_model, sea_ice::Simulation{<:SeaIceModel}) clock, atmosphere_sea_ice_fluxes, sea_ice_ocean_fluxes, - freshwater_flux, + snowfall_flux, ice_concentration, sea_ice_properties) @@ -42,7 +42,7 @@ end clock, atmosphere_sea_ice_fluxes, sea_ice_ocean_fluxes, - freshwater_flux, # Where do we add this one? + snowfall_flux, ice_concentration, sea_ice_properties) @@ -55,6 +55,7 @@ end 𝒬ᵛ = atmosphere_sea_ice_fluxes.latent_heat[i, j, 1] # latent heat flux 𝒬ᶠʳᶻ = sea_ice_ocean_fluxes.frazil_heat[i, j, 1] # frazil heat flux 𝒬ⁱⁿᵗ = sea_ice_ocean_fluxes.interface_heat[i, j, 1] # interfacial heat flux + Jˢⁿ = snowfall_flux[i, j, 1] end ρτˣ = atmosphere_sea_ice_fluxes.x_momentum # zonal momentum flux @@ -67,8 +68,9 @@ end # Mask fluxes over land for convenience inactive = inactive_node(i, j, kᴺ, grid, Center(), Center(), Center()) - @inbounds top_fluxes.heat[i, j, 1] = ifelse(inactive, zero(grid), ΣQt) - @inbounds top_fluxes.u[i, j, 1] = ifelse(inactive, zero(grid), ℑxᶠᵃᵃ(i, j, 1, grid, ρτˣ)) - @inbounds top_fluxes.v[i, j, 1] = ifelse(inactive, zero(grid), ℑyᵃᶠᵃ(i, j, 1, grid, ρτʸ)) - @inbounds bottom_heat_flux[i, j, 1] = ifelse(inactive, zero(grid), ΣQb) + @inbounds top_fluxes.heat[i, j, 1] = ifelse(inactive, zero(grid), ΣQt) + @inbounds top_fluxes.snowfall[i, j, 1] = ifelse(inactive, zero(grid), Jˢⁿ) + @inbounds top_fluxes.u[i, j, 1] = ifelse(inactive, zero(grid), ℑxᶠᵃᵃ(i, j, 1, grid, ρτˣ)) + @inbounds top_fluxes.v[i, j, 1] = ifelse(inactive, zero(grid), ℑyᵃᶠᵃ(i, j, 1, grid, ρτʸ)) + @inbounds bottom_heat_flux[i, j, 1] = ifelse(inactive, zero(grid), ΣQb) end diff --git a/src/SeaIces/sea_ice_simulation.jl b/src/SeaIces/sea_ice_simulation.jl index dc0681041..bb1aae401 100644 --- a/src/SeaIces/sea_ice_simulation.jl +++ b/src/SeaIces/sea_ice_simulation.jl @@ -1,28 +1,45 @@ using ClimaSeaIce -using ClimaSeaIce: SeaIceModel, SlabThermodynamics, PhaseTransitions, ConductiveFlux -using ClimaSeaIce.SeaIceThermodynamics: IceWaterThermalEquilibrium +using ClimaSeaIce: SeaIceModel, PhaseTransitions, ConductiveFlux, + sea_ice_slab_thermodynamics, snow_slab_thermodynamics +using ClimaSeaIce.SeaIceThermodynamics: IceWaterThermalEquilibrium, IceSnowConductiveFlux using ClimaSeaIce.SeaIceDynamics: SplitExplicitSolver, SemiImplicitStress, SeaIceMomentumEquation, StressBalanceFreeDrift using ClimaSeaIce.Rheologies: IceStrength, ElastoViscoPlasticRheology +using Oceananigans.TimeSteppers: SplitRungeKuttaTimeStepper + using NumericalEarth.EarthSystemModels: ocean_surface_salinity, ocean_surface_velocities -using NumericalEarth.Oceans: Default +using NumericalEarth.Oceans: Default, reference_density default_rotation_rate = Oceananigans.defaults.planet_rotation_rate +ocean_reference_density(ocean::Simulation, FT) = convert(FT, reference_density(ocean)) +ocean_reference_density(::Nothing, FT) = convert(FT, 1026.0) + +function default_snow_thermodynamics(grid) + FT = eltype(grid) + snow_conductivity = FT(0.31) + snow_surface_temperature = Field{Center, Center, Nothing}(grid) + top_heat_boundary_condition = PrescribedTemperature(snow_surface_temperature.data) + return snow_slab_thermodynamics(grid; conductivity = snow_conductivity, top_heat_boundary_condition) +end + function sea_ice_simulation(grid, ocean=nothing; Δt = 5minutes, ice_salinity = 4, # psu - advection = nothing, # for the moment + advection = nothing, tracers = (), ice_heat_capacity = 2100, # J kg⁻¹ K⁻¹ ice_consolidation_thickness = 0.05, # m - ice_density = 900, # kg m⁻³ + sea_ice_density = 900, # kg m⁻³ + snow_density = 330, # kg m⁻³ dynamics = sea_ice_dynamics(grid, ocean), bottom_heat_boundary_condition = nothing, top_heat_boundary_condition = nothing, - phase_transitions = PhaseTransitions(; heat_capacity = ice_heat_capacity, density = ice_density), - conductivity = 2, # kg m s⁻³ K⁻¹ - internal_heat_flux = ConductiveFlux(; conductivity)) + timestepper = :SplitRungeKutta3, + phase_transitions = PhaseTransitions(eltype(grid); heat_capacity=ice_heat_capacity, density=sea_ice_density), + conductivity = 2, # W m⁻¹ K⁻¹ + internal_heat_flux = ConductiveFlux(; conductivity), + snow_thermodynamics = default_snow_thermodynamics(grid)) # Build consistent boundary conditions for the ice model: # - bottom -> flux boundary condition @@ -37,20 +54,19 @@ function sea_ice_simulation(grid, ocean=nothing; if isnothing(ocean) surface_ocean_salinity = 0 else - kᴺ = size(grid, 3) surface_ocean_salinity = ocean_surface_salinity(ocean) end bottom_heat_boundary_condition = IceWaterThermalEquilibrium(surface_ocean_salinity) end - ice_thermodynamics = SlabThermodynamics(grid; - internal_heat_flux, - phase_transitions, - top_heat_boundary_condition, - bottom_heat_boundary_condition) + ice_thermodynamics = sea_ice_slab_thermodynamics(grid; + internal_heat_flux, + top_heat_boundary_condition, + bottom_heat_boundary_condition) bottom_heat_flux = Field{Center, Center, Nothing}(grid) top_heat_flux = Field{Center, Center, Nothing}(grid) + snowfall = Field{Center, Center, Nothing}(grid) # Build the sea ice model sea_ice_model = SeaIceModel(grid; @@ -58,30 +74,51 @@ function sea_ice_simulation(grid, ocean=nothing; advection, tracers, ice_consolidation_thickness, + sea_ice_density, + snow_density, + phase_transitions, ice_thermodynamics, + snow_thermodynamics, + snowfall, dynamics, + timestepper, bottom_heat_flux, top_heat_flux) verbose = false - - # Build the simulation sea_ice = Simulation(sea_ice_model; Δt, verbose) return sea_ice end +default_coriolis(ocean::Simulation) = ocean.model.coriolis +default_coriolis(ocean::Nothing) = HydrostaticSphericalCoriolis(; rotation_rate=default_rotation_rate) + +default_solver(grid, ocean) = SplitExplicitSolver(grid; substeps=120) + +# We assume RK3 has a larger timestep +function default_solver(grid, ocean::Simulation) + substeps = if ocean.model.timestepper isa SplitRungeKuttaTimeStepper + 240 + else + 120 + end + return SplitExplicitSolver(grid; substeps) +end + function sea_ice_dynamics(grid, ocean=nothing; - sea_ice_ocean_drag_coefficient = 5.5e-3, + sea_ice_ocean_drag_coefficient = 3.24e-3, rheology = ElastoViscoPlasticRheology(), - coriolis = HydrostaticSphericalCoriolis(; rotation_rate=default_rotation_rate), + coriolis = default_coriolis(ocean), free_drift = nothing, - solver = SplitExplicitSolver(grid; substeps=120)) + solver = default_solver(grid, ocean)) SSU, SSV = ocean_surface_velocities(ocean) - sea_ice_ocean_drag_coefficient = convert(eltype(grid), sea_ice_ocean_drag_coefficient) + FT = eltype(grid) + sea_ice_ocean_drag_coefficient = convert(FT, sea_ice_ocean_drag_coefficient) + ρₑ = ocean_reference_density(ocean, FT) - τo = SemiImplicitStress(uₑ=SSU, vₑ=SSV, Cᴰ=sea_ice_ocean_drag_coefficient) + τo = SemiImplicitStress(uₑ=SSU, vₑ=SSV, Cᴰ=sea_ice_ocean_drag_coefficient, ρₑ=ρₑ) τua = Field{Face, Center, Nothing}(grid) τva = Field{Center, Face, Nothing}(grid) @@ -105,8 +142,8 @@ end sea_ice_thickness(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.ice_thickness sea_ice_concentration(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.ice_concentration -heat_capacity(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.ice_thermodynamics.phase_transitions.heat_capacity -reference_density(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.ice_thermodynamics.phase_transitions.density +heat_capacity(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.phase_transitions.heat_capacity +reference_density(sea_ice::Simulation{<:SeaIceModel}) = sea_ice.model.phase_transitions.density function net_fluxes(sea_ice::Simulation{<:SeaIceModel}) net_momentum_fluxes = if isnothing(sea_ice.model.dynamics) @@ -119,15 +156,21 @@ function net_fluxes(sea_ice::Simulation{<:SeaIceModel}) (; u, v) end - net_top_sea_ice_fluxes = merge((; heat=sea_ice.model.external_heat_fluxes.top), net_momentum_fluxes) + net_top_sea_ice_fluxes = merge((; heat=sea_ice.model.external_heat_fluxes.top, snowfall=sea_ice.model.snowfall), net_momentum_fluxes) net_bottom_sea_ice_fluxes = (; heat=sea_ice.model.external_heat_fluxes.bottom) return (; bottom = net_bottom_sea_ice_fluxes, top = net_top_sea_ice_fluxes) end function default_ai_temperature(sea_ice::Simulation{<:SeaIceModel}) - conductive_flux = sea_ice.model.ice_thermodynamics.internal_heat_flux.parameters.flux - return SkinTemperature(conductive_flux) + ice_flux = sea_ice.model.ice_thermodynamics.internal_heat_flux + snow_thermo = sea_ice.model.snow_thermodynamics + internal_flux = if isnothing(snow_thermo) + ice_flux + else + IceSnowConductiveFlux(snow_thermo.internal_heat_flux.conductivity, ice_flux.conductivity) + end + return SkinTemperature(internal_flux) end # Constructor that accepts the sea-ice model @@ -136,7 +179,7 @@ function ThreeEquationHeatFlux(sea_ice::Simulation{<:SeaIceModel}, FT::DataType salt_transfer_coefficient = heat_transfer_coefficient / 35, friction_velocity = convert(FT, 0.002)) - conductive_flux = sea_ice.model.ice_thermodynamics.internal_heat_flux.parameters.flux + conductive_flux = sea_ice.model.ice_thermodynamics.internal_heat_flux ice_temperature = sea_ice.model.ice_thermodynamics.top_surface_temperature return ThreeEquationHeatFlux(conductive_flux, diff --git a/test/Project.toml b/test/Project.toml index a94e9106e..954fe0668 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -31,7 +31,6 @@ XESMF = "2e0b0046-e7a1-486f-88de-807ee8ffabe5" [sources] NumericalEarth = {path = ".."} -Breeze = {url = "https://github.com/NumericalEarth/Breeze.jl/", rev = "main"} [compat] ArchGDAL = "0.10" @@ -39,7 +38,7 @@ Breeze = "0.4" CDSAPI = "2.2.2" CFTime = "0.1, 0.2" CUDA = "5.9.5" -ClimaSeaIce = "0.4.10" +ClimaSeaIce = "0.5" CondaPkg = "0.2.33" CopernicusMarine = "0.1.1" Dates = "<0.0.1, 1" diff --git a/test/runtests.jl b/test/runtests.jl index 7a88b6ccb..448c91a05 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,9 +79,10 @@ function __init__() try atmosphere = JRA55PrescribedAtmosphere(backend=JRA55NetCDFBackend(2)) + land = JRA55PrescribedLand(backend=JRA55NetCDFBackend(2)) # Touch the radiation variables (rlds/rsds) too, so a corrupted cached # download is caught by the same fallback path. - radiation = JRA55PrescribedRadiation(backend=JRA55NetCDFBackend(2)) + radiation = JRA55PrescribedRadiation(backend=JRA55NetCDFBackend(2)) catch e @warn "Original JRA55 download failed, trying NumericalEarthArtifacts fallback..." exception=(e, catch_backtrace()) emit_ci_warning("Broken JRA55 download", "Original source failed during init") @@ -90,7 +91,7 @@ function __init__() download_from_artifacts(metadata_path(datum)) end atmosphere = JRA55PrescribedAtmosphere(backend=JRA55NetCDFBackend(2)) - radiation = JRA55PrescribedRadiation(backend=JRA55NetCDFBackend(2)) + radiation = JRA55PrescribedRadiation(backend=JRA55NetCDFBackend(2)) end ##### diff --git a/test/test_ecco_atmosphere.jl b/test/test_ecco_atmosphere.jl index 12d120930..59202dd6e 100644 --- a/test/test_ecco_atmosphere.jl +++ b/test/test_ecco_atmosphere.jl @@ -2,7 +2,7 @@ include("runtests_setup.jl") include("download_utils.jl") using Statistics: median -using NumericalEarth.Atmospheres: PrescribedAtmosphere +using NumericalEarth.Atmospheres: PrescribedAtmosphere, PrescribedPrecipitationFlux using NumericalEarth.Radiations: PrescribedRadiation using NumericalEarth.ECCO: ECCOPrescribedAtmosphere, ECCOPrescribedRadiation, ECCO4Monthly using NumericalEarth.DataWrangling: download_dataset, metadata_path, higher_bound @@ -50,7 +50,9 @@ end @test haskey(atmosphere.tracers, :T) @test haskey(atmosphere.tracers, :q) @test !isnothing(atmosphere.pressure) - @test haskey(atmosphere.freshwater_flux, :rain) + @test atmosphere.freshwater_flux isa PrescribedPrecipitationFlux + @test atmosphere.freshwater_flux.rain isa FieldTimeSeries + @test isnothing(atmosphere.freshwater_flux.snow) # Test downwelling radiation components ℐꜜˢʷ = radiation.downwelling_shortwave diff --git a/test/test_ocean_sea_ice_model.jl b/test/test_ocean_sea_ice_model.jl index abfc3200e..035d7e877 100644 --- a/test/test_ocean_sea_ice_model.jl +++ b/test/test_ocean_sea_ice_model.jl @@ -44,7 +44,7 @@ using ClimaSeaIce.Rheologies ocean = ocean_simulation(grid; free_surface) sea_ice = sea_ice_simulation(grid, ocean; advection=WENO(order=7)) - liquidus = sea_ice.model.ice_thermodynamics.phase_transitions.liquidus + liquidus = sea_ice.model.phase_transitions.liquidus # Set the ocean temperature and salinity set!(ocean.model, T=temperature_metadata[1], S=salinity_metadata[1]) diff --git a/test/test_ospapa.jl b/test/test_ospapa.jl index 15a1af718..aea8cb22c 100644 --- a/test/test_ospapa.jl +++ b/test/test_ospapa.jl @@ -1,7 +1,7 @@ include("runtests_setup.jl") using NumericalEarth.OSPapa -using NumericalEarth.Atmospheres: PrescribedAtmosphere +using NumericalEarth.Atmospheres: PrescribedAtmosphere, PrescribedPrecipitationFlux using NumericalEarth.Radiations: PrescribedRadiation using Oceananigans.BoundaryConditions: BoundaryCondition, Flux, getbc using Oceananigans.Units: minutes @@ -32,7 +32,9 @@ const OSPAPA_TEST_END = DateTime(2012, 10, 3) @test haskey(atmosphere.tracers, :T) @test haskey(atmosphere.tracers, :q) @test !isnothing(atmosphere.pressure) - @test haskey(atmosphere.freshwater_flux, :rain) + @test atmosphere.freshwater_flux isa PrescribedPrecipitationFlux + @test atmosphere.freshwater_flux.rain isa FieldTimeSeries + @test isnothing(atmosphere.freshwater_flux.snow) # Radiation sanity checks ℐꜜˢʷ = radiation.downwelling_shortwave diff --git a/test/test_snow_model_integration.jl b/test/test_snow_model_integration.jl new file mode 100644 index 000000000..14928c312 --- /dev/null +++ b/test/test_snow_model_integration.jl @@ -0,0 +1,217 @@ +include("runtests_setup.jl") + +using ClimaSeaIce: SeaIceModel, ConductiveFlux +using ClimaSeaIce.SeaIceThermodynamics: IceSnowConductiveFlux +using NumericalEarth.SeaIces: default_snow_thermodynamics +using NumericalEarth.EarthSystemModels.InterfaceComputations: + ComponentInterfaces, + SkinTemperature, + InterfaceProperties, + conductive_flux_balance_temperature + +using Oceananigans.Fields: ZeroField +using Oceananigans.Units: hours, days + +##### +##### Unit tests +##### + +@testset "Snow model unit tests" begin + for arch in test_architectures + A = typeof(arch) + + grid = RectilinearGrid(arch; + size = (4, 4, 1), + extent = (1, 1, 1), + topology = (Periodic, Periodic, Bounded)) + + @testset "sea_ice_simulation with_snow=false [$A]" begin + sea_ice = sea_ice_simulation(grid; dynamics=nothing, snow_thermodynamics=nothing) + @test sea_ice isa Simulation + @test sea_ice.model isa SeaIceModel + @test sea_ice.model.snow_thermodynamics === nothing + @test sea_ice.model.ice_thermodynamics.internal_heat_flux isa ConductiveFlux + end + + @testset "sea_ice_simulation with_snow=true [$A]" begin + sea_ice = sea_ice_simulation(grid; dynamics=nothing) + @test sea_ice isa Simulation + @test sea_ice.model.snow_thermodynamics !== nothing + @test sea_ice.model.snow_thickness isa Field + end + + @testset "PhaseTransitions API [$A]" begin + sea_ice = sea_ice_simulation(grid; dynamics=nothing) + pt = sea_ice.model.phase_transitions + @test pt.heat_capacity == 2100 + @test pt.density == 900 + end + + @testset "ComponentExchanger includes hs [$A]" begin + using NumericalEarth.EarthSystemModels.InterfaceComputations: ComponentExchanger + + # Without snow: hs should be ZeroField + sea_ice = sea_ice_simulation(grid; dynamics=nothing, snow_thermodynamics=nothing) + exchanger = ComponentExchanger(sea_ice, grid) + @test haskey(exchanger.state, :hs) + @test haskey(exchanger.state, :hi) + @test exchanger.state.hs isa ZeroField + + # With snow: hs should be a Field + sea_ice_snow = sea_ice_simulation(grid; dynamics=nothing) + exchanger_snow = ComponentExchanger(sea_ice_snow, grid) + @test exchanger_snow.state.hs isa Field + end + + @testset "default_ai_temperature dispatches on snow [$A]" begin + using NumericalEarth.SeaIces: default_ai_temperature + + sea_ice = sea_ice_simulation(grid; dynamics=nothing, snow_thermodynamics=nothing) + st = default_ai_temperature(sea_ice) + @test st isa SkinTemperature + @test st.internal_flux isa ConductiveFlux + + sea_ice_snow = sea_ice_simulation(grid; dynamics=nothing) + st_snow = default_ai_temperature(sea_ice_snow) + @test st_snow isa SkinTemperature + @test st_snow.internal_flux isa IceSnowConductiveFlux + end + + @testset "net_fluxes includes snowfall [$A]" begin + using NumericalEarth.SeaIces: net_fluxes + + sea_ice = sea_ice_simulation(grid; dynamics=nothing) + fluxes = net_fluxes(sea_ice) + @test haskey(fluxes.top, :snowfall) + end + + @testset "Snow insulates: warmer surface temperature [$A]" begin + # Build two coupled models — one without snow, one with snow and + # nonzero snow thickness — then compare surface temperatures after + # one coupled time step. Snow adds thermal resistance, so the + # surface should be warmer (closer to the warmer atmosphere). + # + # Radiation is disabled (ε=0) so the surface energy balance + # reduces to conductive + turbulent fluxes; otherwise the + # Stefan–Boltzmann loss swamps the small snow-insulation signal. + ocean_grid = RectilinearGrid(arch; + size = (1, 1, 2), + extent = (1, 1, 1), + topology = (Periodic, Periodic, Bounded)) + + function build_coupled(; with_snow) + ocean = ocean_simulation(ocean_grid; + momentum_advection = nothing, + tracer_advection = nothing, + closure = nothing, + coriolis = nothing) + set!(ocean.model, T = -1.8, S = 34) + + snow_thermodynamics = with_snow ? default_snow_thermodynamics(ocean_grid) : nothing + sea_ice = sea_ice_simulation(ocean_grid, ocean; + dynamics = nothing, + snow_thermodynamics) + set!(sea_ice.model, h = 1.0, ℵ = 1.0) + if with_snow + set!(sea_ice.model, hs = 0.2) + end + + atmosphere = PrescribedAtmosphere(ocean_grid, [0.0]) + parent(atmosphere.velocities.u) .= 2.0 + return OceanSeaIceModel(sea_ice, ocean; atmosphere, radiation=nothing) + end + + bare = build_coupled(with_snow = false) + snowy = build_coupled(with_snow = true) + + time_step!(bare, 1) + time_step!(snowy, 1) + + Ts_bare = bare.interfaces.atmosphere_sea_ice_interface.temperature + Ts_snowy = snowy.interfaces.atmosphere_sea_ice_interface.temperature + + @allowscalar begin + # Snow insulation → warmer (or equal) surface temperature + @test Ts_snowy[1, 1, 1] ≥ Ts_bare[1, 1, 1] + end + end + end +end + +##### +##### Integration tests +##### + +@testset "Snow model integration tests" begin + for arch in test_architectures + A = typeof(arch) + + grid = RectilinearGrid(arch; + size = (4, 4, 2), + extent = (1, 1, 1), + topology = (Periodic, Periodic, Bounded)) + + @testset "Coupled model with snow [$A]" begin + ocean = ocean_simulation(grid; + momentum_advection = nothing, + tracer_advection = nothing, + closure = nothing, + coriolis = nothing) + + sea_ice = sea_ice_simulation(grid, ocean; + dynamics = nothing) + + atmosphere = PrescribedAtmosphere(grid, [0.0]) + radiation = nothing + + @test begin + coupled = OceanSeaIceModel(sea_ice, ocean; atmosphere, radiation) + time_step!(coupled, 1) + true + end + end + + @testset "Snowfall routing [$A]" begin + ocean = ocean_simulation(grid; + momentum_advection = nothing, + tracer_advection = nothing, + closure = nothing, + coriolis = nothing) + + sea_ice = sea_ice_simulation(grid, ocean; + dynamics = nothing, + snow_thermodynamics = nothing) + + atmosphere = PrescribedAtmosphere(grid, [0.0]) + radiation = nothing + + coupled = OceanSeaIceModel(sea_ice, ocean; atmosphere, radiation) + + # The snowfall field should exist in the exchanger + exchanger = coupled.interfaces.exchanger + @test haskey(exchanger.atmosphere.state, :Jˢⁿ) + + # The net fluxes should include snowfall + top_fluxes = coupled.interfaces.net_fluxes.sea_ice.top + @test haskey(top_fluxes, :snowfall) + end + + @testset "Snow insulation effect [$A]" begin + # With snow, the IceSnowConductiveFlux is used for the surface + # temperature solve. Verify this dispatch is wired correctly. + ocean = ocean_simulation(grid; + momentum_advection = nothing, + tracer_advection = nothing, + closure = nothing, + coriolis = nothing) + + sea_ice = sea_ice_simulation(grid, ocean; + dynamics = nothing) + + ai_temp = NumericalEarth.SeaIces.default_ai_temperature(sea_ice) + @test ai_temp.internal_flux isa IceSnowConductiveFlux + @test ai_temp.internal_flux.ice_conductivity == 2.0 + @test ai_temp.internal_flux.snow_conductivity == 0.31 + end + end +end