diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c72f1c734..bcafae6d0 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -126,3 +126,16 @@ steps: agents: slurm_ntasks: 1 slurm_gpus: 1 + + # Advisory wall-time ratchet against committed per-GPU baselines + # (perf/benchmark_baselines/). Soft-fail: flags regressions without + # blocking merges; see the header of perf/benchmark_ratchet.jl. + - label: "GPU DYAMOND benchmark ratchet" + command: "julia --color=yes --project=test perf/benchmark_ratchet.jl" + soft_fail: true + artifact_paths: "perf/benchmark_baselines/*" + env: + CLIMACOMMS_DEVICE: "CUDA" + agents: + slurm_ntasks: 1 + slurm_gpus: 1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6e0b05f12..6c8677133 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,7 @@ concurrency: jobs: docbuild: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 diff --git a/.gitignore b/.gitignore index 17c36a83d..7cd294a17 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,8 @@ docs/site/ Manifest.toml test/Manifest.toml +# Literate.jl-generated tutorial pages +docs/src/tutorials/ + +# Temporary benchmark measurements +perf/benchmark_baselines/measured_*.txt diff --git a/NEWS.md b/NEWS.md index 74cfaf5a5..daf98159c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,31 @@ RRTMGP.jl Release Notes main ------ +- Documentation reorganized along tutorial / how-to / explanation / reference + lines: two new executable (Literate.jl) tutorials — "A first radiation + calculation" and "Radiative-convective equilibrium", which reproduces + Manabe's classic fixed-relative-humidity climate-sensitivity experiment with + RRTMGP's clear-sky optics — plus four how-to guides (driving RRTMGP from a + host model, running on GPUs, caching the lookup tables, per-band fluxes). + The README now gives a Thermodynamics.jl-style overview with quick starts. +- Struct docstrings migrated off `DocStringExtensions.FIELDS` to explicit + `# Fields` sections (per the CliMA documentation policy), so field + descriptions and units render on the API pages without inline field + docstrings. +- New unit tests for previously indirect paths: every interpolation / + bottom-extrapolation scheme against closed forms (including a dry-adiabat + round-trip through `interpolate_levels!`), solver-constructor and getter + guard errors, `validate_inputs` field-by-field, the incident-longwave-flux + boundary condition, pointwise deep-atmosphere flux scaling, heating-rate = + net-flux divergence, gray `clip!`, aerosol-only `LookupBundle` round-trips, + cloud-radiative-effect sign checks in the all-sky tests, and the McICA + reproducibility tests now also run in `Float32`. +- New advisory GPU benchmark ratchet (`perf/benchmark_ratchet.jl` + a + soft-fail Buildkite step): the DYAMOND-scale `solve_lw!`/`solve_sw!` medians + are compared against per-GPU baselines committed under + `perf/benchmark_baselines/`, flagging wall-time regressions beyond 20% + without blocking merges. The three DYAMOND benchmark scripts are now + includable (their sweeps run only when executed as scripts). - New docs page "Fortran and paper concordance": tables mapping RRTMGP.jl names (containers, RTE/gas-optics/cloud/aerosol kernels) to the Fortran rte-rrtmgp `mo_*`/`ty_*` names and to the papers whose equations they diff --git a/Project.toml b/Project.toml index fd3345def..704f8800c 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ version = "0.21.9" Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d" -DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -27,7 +26,6 @@ Artifacts = "1" CUDA = "5.9, 6" ClimaComms = "0.5.4, 0.6" ClimaParams = "1" -DocStringExtensions = "0.8, 0.9" NCDatasets = "0.14.2" Random = "1" Serialization = "1" diff --git a/README.md b/README.md index 3900a14d6..2596cb982 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,156 @@ +
+ RRTMGP.jl Logo +
+ # RRTMGP.jl -Julia implementation of Radiative Transfer for Energetics and the RRTMGP optics. +The `RRTMGP.jl` package is a Julia implementation of the radiative transfer +solver RTE (Radiative Transfer for Energetics) and the RRTMGP (RRTM for General +circulation model applications—Parallel) correlated-*k* gas optics +([Pincus et al., 2019](https://doi.org/10.1029/2019MS001621)), based on the +reference Fortran implementation +[rte-rrtmgp](https://github.com/earth-system-radiation/rte-rrtmgp). It computes +longwave and shortwave radiative fluxes and heating rates for clear, cloudy, +and aerosol-laden atmospheres, and is the radiation scheme of the +[CliMA Earth System Model](https://clima.caltech.edu). ||| -|---------------------:|:----------------------------------------------| -| **Documentation** | [![latest][docs-latest-img]][docs-latest-url] | -| **Code Coverage** | [![codecov][codecov-img]][codecov-url] | -| **BuildKite** | [![buildkite][buildkite-img]][buildkite-url] | -| **Downloads** | [![downloads][downloads-img]][downloads-url] | - - -[docs-latest-img]: https://img.shields.io/badge/docs-latest-blue.svg -[docs-latest-url]: https://CliMA.github.io/RRTMGP.jl/latest/ +|-----------------:|:-------------------------------------------------------------| +| **Documentation**| [![stable][docs-stable-img]][docs-stable-url] [![dev][docs-dev-img]][docs-dev-url] | +| **Version** | [![version][version-img]][version-url] | +| **License** | [![license][license-img]][license-url] | +| **Tests** | [![gha ci][gha-ci-img]][gha-ci-url] [![buildkite][bk-ci-img]][bk-ci-url] | +| **Code Coverage**| [![codecov][codecov-img]][codecov-url] | +| **Downloads** | [![Downloads][dlt-img]][dlt-url] | +[docs-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg +[docs-stable-url]: https://CliMA.github.io/RRTMGP.jl/stable/ +[docs-dev-img]: https://img.shields.io/badge/docs-dev-blue.svg +[docs-dev-url]: https://CliMA.github.io/RRTMGP.jl/dev/ +[version-img]: https://juliahub.com/docs/General/RRTMGP/stable/version.svg +[version-url]: https://juliahub.com/ui/Packages/General/RRTMGP +[license-img]: https://img.shields.io/badge/license-Apache%202.0-blue.svg +[license-url]: https://github.com/CliMA/RRTMGP.jl/blob/main/LICENSE +[gha-ci-img]: https://github.com/CliMA/RRTMGP.jl/actions/workflows/OS-UnitTests.yml/badge.svg +[gha-ci-url]: https://github.com/CliMA/RRTMGP.jl/actions/workflows/OS-UnitTests.yml +[bk-ci-img]: https://badge.buildkite.com/ee3a0c43cf4925ee14a966f794ac85d0b9439244d23e43b308.svg?branch=main +[bk-ci-url]: https://buildkite.com/clima/rrtmgp-ci/builds?branch=main [codecov-img]: https://codecov.io/gh/CliMA/RRTMGP.jl/branch/main/graph/badge.svg [codecov-url]: https://codecov.io/gh/CliMA/RRTMGP.jl +[dlt-img]: https://img.shields.io/badge/dynamic/json?url=http%3A%2F%2Fjuliapkgstats.com%2Fapi%2Fv1%2Ftotal_downloads%2FRRTMGP&query=total_requests&label=Downloads +[dlt-url]: https://juliapkgstats.com/pkg/RRTMGP -[buildkite-img]: https://badge.buildkite.com/ee3a0c43cf4925ee14a966f794ac85d0b9439244d23e43b308.svg -[buildkite-url]: https://buildkite.com/clima/rrtmgp-ci +## Quick Start -[downloads-img]: https://img.shields.io/badge/dynamic/json?url=http%3A%2F%2Fjuliapkgstats.com%2Fapi%2Fv1%2Ftotal_downloads%2FRRTMGP&query=total_requests&suffix=%2Ftotal&label=Downloads -[downloads-url]: http://juliapkgstats.com/pkg/RRTMGP +### Installation -This is based off of the [rte-rrtmgp](https://github.com/RobertPincus/rte-rrtmgp) repository. +```julia +using Pkg +Pkg.add("RRTMGP") +``` -# Install +### Basic Usage -RRTMGP.jl is registered in the general Julia registry. To install, enter the package manager by typing `]` in the Julia REPL, and then type: +The standalone front door provides a quick start. A gray (single-band) +atmosphere uses analytic formulas: ```julia -pkg> add RRTMGP +using RRTMGP + +out = RRTMGP.solve_gray(Float64; nlay = 60, ncol = 1) +out.net # net flux at each level [W/m²] +out.heating_rate # radiative heating rate at each layer [K/s] ``` -Then, to use +For the full correlated-*k* gas optics, build an idealized clear-sky profile +and solve it (the lookup tables are downloaded automatically; loading +`NCDatasets` activates them): ```julia -julia> using RRTMGP +using RRTMGP, NCDatasets + +profile = RRTMGP.standard_atmosphere(Float64; kind = :tropical) +out = RRTMGP.solve(profile) +out.lw_up[end, 1] # outgoing longwave radiation at the top of the atmosphere [W/m²] +out.heating_rate # radiative heating rate at each layer [K/s] ``` -# Acknowledgments +### Host-Model Usage + +Climate models construct an `RRTMGPSolver` once, write their state through the +[getter contract](https://clima.github.io/RRTMGP.jl/latest/getters/), and call +`update_fluxes!(solver)` every radiation step: + +```julia +solver = RRTMGP.RRTMGPSolver(grid_params, method, params, bcs_lw, bcs_sw, as) +RRTMGP.update_fluxes!(solver) # allocation-free, in place +F = RRTMGP.net_flux(solver) # (nlev, ncol) view into solver memory +``` + +## Key Features + +- **Full RRTMGP gas optics**: longwave and shortwave correlated-*k* lookup + tables, clouds (with McICA sampling of partial cloudiness), and MERRA + aerosols, validated against the Fortran rte-rrtmgp reference fluxes. +- **Gray radiation**: analytic single-band optics for idealized-climate and + teaching use. +- **Two-stream and no-scattering solvers** for both bands, with optional + per-band (spectrally resolved) flux output. +- **CPU and GPU**: the same code runs single-threaded, multi-threaded, and on + CUDA GPUs via [ClimaComms](https://github.com/CliMA/ClimaComms.jl). +- **Zero-allocation driver**: `update_fluxes!` is allocation-free and + type-stable (asserted in CI with `@allocated` and JET), allowing it to run + fast within a climate simulation. +- **Dual precision**: `Float32` and `Float64` throughout, with high accuracy + (~ 1e-3 W/m²) at `Float32` and an accuracy-ratchet test pinning the + `Float32`↔`Float64` flux differences. + +## Design + +RRTMGP.jl is organized in three layers: + +1. **Functional core**: `solve_lw!` / `solve_sw!` kernels acting on explicit + state (`AtmosphericState`, optics, sources, boundary conditions, flux + workspaces). See the [Functional core](https://clima.github.io/RRTMGP.jl/latest/functional_core/) page. +2. **Solver aggregate**: `RRTMGPSolver` bundles the configuration, state, and + workspaces; hosts exchange data through documented + [getters](https://clima.github.io/RRTMGP.jl/latest/getters/) and drive it + with `update_fluxes!`. +3. **Standalone front door**: `solve_gray(FT)`, `standard_atmosphere(FT)`, and + `solve(profile)` for classroom use, single-column experiments, and quick + starts. + +Readers coming from the Fortran implementation can use the +[Fortran and paper concordance](https://clima.github.io/RRTMGP.jl/latest/concordance/) +to map names between the two code bases and the underlying papers. + +## Documentation + +- **[Documentation home](https://clima.github.io/RRTMGP.jl/latest/)**: overview + and page index. +- **[Functional core](https://clima.github.io/RRTMGP.jl/latest/functional_core/)**: + the solver kernels, with runnable gray and clear-sky examples. +- **[Getter contract](https://clima.github.io/RRTMGP.jl/latest/getters/)**: the + host data-exchange interface. +- **[API reference](https://clima.github.io/RRTMGP.jl/latest/api/)**: types and + functions. + +## Integration with Climate Models + +RRTMGP.jl provides radiative fluxes and heating rates for the +[CliMA](https://github.com/CliMA) ecosystem, including: + +- [ClimaAtmos](https://github.com/CliMA/ClimaAtmos.jl) +- [ClimaCoupler](https://github.com/CliMA/ClimaCoupler.jl) + +## Getting Help + +For questions, check the [documentation](https://clima.github.io/RRTMGP.jl/latest/) +or open an issue on [GitHub](https://github.com/CliMA/RRTMGP.jl). + +## Acknowledgments - - [Robert Pincus](https://github.com/RobertPincus) for his invaluable help - - The authors of the [Fortran implementation](https://github.com/earth-system-radiation/rte-rrtmgp) of RTE-RRTMGP on which this code is based - - NASA for images of the sun (for our [logo](https://clima.github.io/RRTMGP.jl/latest/assets/logo.png)) +- [Robert Pincus](https://github.com/RobertPincus) for his invaluable help. +- Robert Pincus, Eli Mlawer, and Jennifer Delamere, the developers of RTE+RRTMGP + and its reference [Fortran implementation](https://github.com/earth-system-radiation/rte-rrtmgp), + on which this code is based. diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 4ce7127d4..c42ea3d96 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -1,42 +1,116 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.9" +julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "e50c98923cd72fd932676f85c10cbb527e8dc93e" +project_hash = "a24ee2b7fc51f57156fa707d7a274c6fac58c965" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" version = "0.0.1" +[[deps.AbstractFFTs]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.5.0" +weakdeps = ["ChainRulesCore", "Test"] + + [deps.AbstractFFTs.extensions] + AbstractFFTsChainRulesCoreExt = "ChainRulesCore" + AbstractFFTsTestExt = "Test" + [[deps.AbstractTrees]] git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" version = "0.4.5" +[[deps.Accessors]] +deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"] +git-tree-sha1 = "7063ad1083578215c7c4bf410368150abe8d5524" +uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" +version = "0.1.45" + + [deps.Accessors.extensions] + AxisKeysExt = "AxisKeys" + IntervalSetsExt = "IntervalSets" + LinearAlgebraExt = "LinearAlgebra" + StaticArraysExt = "StaticArrays" + StructArraysExt = "StructArrays" + TestExt = "Test" + UnitfulExt = "Unitful" + + [deps.Accessors.weakdeps] + AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + [[deps.Adapt]] -deps = ["LinearAlgebra", "Requires"] -git-tree-sha1 = "f7817e2e585aa6d924fd714df1e2a84be7896c60" +deps = ["LinearAlgebra"] +git-tree-sha1 = "daa72978cd7a624246e894a4f4f067706d4e17e2" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -version = "4.3.0" +version = "4.7.0" +weakdeps = ["SparseArrays", "StaticArrays"] [deps.Adapt.extensions] AdaptSparseArraysExt = "SparseArrays" AdaptStaticArraysExt = "StaticArrays" - [deps.Adapt.weakdeps] - SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +[[deps.AdaptivePredicates]] +git-tree-sha1 = "7e651ea8d262d2d74ce75fdf47c4d63c07dba7a6" +uuid = "35492f91-a3bd-45ad-95db-fcad7dcfedb7" +version = "1.2.0" + +[[deps.AliasTables]] +deps = ["PtrArrays", "Random"] +git-tree-sha1 = "9876e1e164b144ca45e9e3198d0b689cadfed9ff" +uuid = "66dad0bd-aa9a-41b7-9441-69ab47430ed8" +version = "1.1.3" + +[[deps.Animations]] +deps = ["Colors"] +git-tree-sha1 = "e092fa223bf66a3c41f9c022bd074d916dc303e7" +uuid = "27a7e980-b3e6-11e9-2bcd-0b925532e340" +version = "0.4.2" [[deps.ArgTools]] uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" -version = "1.1.1" +version = "1.1.2" [[deps.Artifacts]] uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Automa]] +deps = ["PrecompileTools", "TranscodingStreams"] +git-tree-sha1 = "94eab0b3ccdcac361188cc661daf69d4433c1818" +uuid = "67c07d97-cdcb-5c2c-af73-a7f9c32a568b" +version = "1.2.0" + +[[deps.AxisAlgorithms]] +deps = ["LinearAlgebra", "Random", "SparseArrays", "WoodburyMatrices"] +git-tree-sha1 = "01b8ccb13d68535d73d2b0c23e39bd23155fb712" +uuid = "13072b0f-2c55-5437-9ae7-d433b7a33950" +version = "1.1.0" + +[[deps.AxisArrays]] +deps = ["Dates", "IntervalSets", "IterTools", "RangeArrays"] +git-tree-sha1 = "4126b08903b777c88edf1754288144a0492c05ad" +uuid = "39de3d68-74b9-583c-8d2d-e117c070f3a9" +version = "0.4.8" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.BaseDirs]] +git-tree-sha1 = "8c290a1b223deaeea9aea44b235d24546da8eb98" +uuid = "18cc8868-cbac-4acf-b575-c8ff214dc66f" +version = "1.4.0" [[deps.BibInternal]] deps = ["TestItems"] @@ -46,26 +120,93 @@ version = "0.3.7" [[deps.BibParser]] deps = ["BibInternal", "DataStructures", "Dates", "JSONSchema", "TestItems", "YAML"] -git-tree-sha1 = "33478bed83bd124ea8ecd9161b3918fb4c70e529" +git-tree-sha1 = "2c9ed5108e3317571ef92c1cb0766fdd38c7d459" uuid = "13533e5b-e1c2-4e57-8cef-cac5e52f6474" -version = "0.2.2" +version = "0.2.3" [[deps.Bibliography]] deps = ["BibInternal", "BibParser", "DataStructures", "Dates", "FileIO", "TestItems", "YAML"] -git-tree-sha1 = "0f25be9708ae20d7b94d3bf9d0a91defcca4c884" +git-tree-sha1 = "0b3e1837077d570bb0c231b9548093de4fa46629" uuid = "f1be7e48-bf82-45af-a471-ae754a193061" -version = "0.3.0" +version = "0.3.1" [[deps.Bijections]] git-tree-sha1 = "a2d308fcd4c2fb90e943cf9cd2fbfa9c32b69733" uuid = "e2ed5e7c-b2de-5872-ae92-c73ca462fb04" version = "0.2.2" +[[deps.Blosc_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Lz4_jll", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "535c80f1c0847a4c967ea945fca21becc9de1522" +uuid = "0b7ba130-8d10-5ba8-a3d6-c5182647fed9" +version = "1.21.7+0" + +[[deps.Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1b96ea4a01afe0ea4090c5c8039690672dd13f2e" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.9+0" + +[[deps.CEnum]] +git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.5.0" + +[[deps.CFTime]] +deps = ["Dates", "Printf"] +git-tree-sha1 = "fad2f199d1f1ae0c8e820ab68f81f6dbf62e60b2" +uuid = "179af706-886a-5703-950a-314cd64e0468" +version = "0.2.10" + +[[deps.CRC32c]] +uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" +version = "1.11.0" + +[[deps.CRlibm]] +deps = ["CRlibm_jll"] +git-tree-sha1 = "66188d9d103b92b6cd705214242e27f5737a1e5e" +uuid = "96374032-68de-5a5b-8d9e-752f78720389" +version = "1.0.2" + +[[deps.CRlibm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e329286945d0cfc04456972ea732551869af1cfc" +uuid = "4e9b3aee-d8a1-5a3d-ad8b-7d824db253f0" +version = "1.0.1+0" + +[[deps.Cairo]] +deps = ["Cairo_jll", "Colors", "Glib_jll", "Graphics", "Libdl", "Pango_jll"] +git-tree-sha1 = "71aa551c5c33f1a4415867fe06b7844faadb0ae9" +uuid = "159f3aea-2a34-519c-b102-8c37f9878175" +version = "1.1.1" + +[[deps.CairoMakie]] +deps = ["CRC32c", "Cairo", "Cairo_jll", "Colors", "FileIO", "FreeType", "GeometryBasics", "LinearAlgebra", "Makie", "PrecompileTools"] +git-tree-sha1 = "47142129b1777e21da58cff265050b10d8560588" +uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +version = "0.15.13" + +[[deps.Cairo_jll]] +deps = ["Artifacts", "Bzip2_jll", "CompilerSupportLibraries_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "Libdl", "Pixman_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "1fa950ebc3e37eccd51c6a8fe1f92f7d86263522" +uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" +version = "1.18.7+0" + +[[deps.ChainRulesCore]] +deps = ["Compat", "LinearAlgebra"] +git-tree-sha1 = "12177ad6b3cad7fd50c8b3825ce24a99ad61c18f" +uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +version = "1.26.1" +weakdeps = ["SparseArrays"] + + [deps.ChainRulesCore.extensions] + ChainRulesCoreSparseArraysExt = "SparseArrays" + [[deps.ClimaComms]] deps = ["Adapt", "Logging", "LoggingExtras"] -git-tree-sha1 = "f3961fa943c1bbbc376af910cb98db67084dc642" +git-tree-sha1 = "99825dd76c1036c5e80c1dab91bbd2c371f34446" uuid = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d" -version = "0.6.9" +version = "0.6.11" [deps.ClimaComms.extensions] ClimaCommsCUDAExt = "CUDA" @@ -75,17 +216,84 @@ version = "0.6.9" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195" +[[deps.ClimaParams]] +deps = ["Dates", "TOML"] +git-tree-sha1 = "9741f6b6d55df618cd0d893620202e108f136980" +uuid = "5c42b081-d73a-476f-9059-fd94b934656c" +version = "1.1.1" + [[deps.CodecZlib]] deps = ["TranscodingStreams", "Zlib_jll"] git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" uuid = "944b1d66-785c-5afd-91f1-9de20f533193" version = "0.7.8" +[[deps.CodecZstd]] +deps = ["TranscodingStreams", "Zstd_jll"] +git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56" +uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2" +version = "0.8.7" + +[[deps.ColorBrewer]] +deps = ["Colors", "JSON"] +git-tree-sha1 = "07da79661b919001e6863b81fc572497daa58349" +uuid = "a2cac450-b92f-5266-8821-25eda20663c8" +version = "0.4.2" + +[[deps.ColorSchemes]] +deps = ["ColorTypes", "ColorVectorSpace", "Colors", "FixedPointNumbers", "PrecompileTools", "Random"] +git-tree-sha1 = "b0fd3f56fa442f81e0a47815c92245acfaaa4e34" +uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +version = "3.31.0" + +[[deps.ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "67e11ee83a43eb71ddc950302c53bf33f0690dfe" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.12.1" +weakdeps = ["StyledStrings"] + + [deps.ColorTypes.extensions] + StyledStringsExt = "StyledStrings" + +[[deps.ColorVectorSpace]] +deps = ["ColorTypes", "FixedPointNumbers", "LinearAlgebra", "Requires", "Statistics", "TensorCore"] +git-tree-sha1 = "8b3b6f87ce8f65a2b4f857528fd8d70086cd72b1" +uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4" +version = "0.11.0" +weakdeps = ["SpecialFunctions"] + + [deps.ColorVectorSpace.extensions] + SpecialFunctionsExt = "SpecialFunctions" + +[[deps.Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] +git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.13.1" + +[[deps.CommonDataModel]] +deps = ["CFTime", "DataStructures", "Dates", "DiskArrays", "Preferences", "Printf", "Statistics"] +git-tree-sha1 = "54d50f8a0a68ff4160564dfa09fc5de32465cfca" +uuid = "1fbeeb36-5f17-413c-809b-666fb144f157" +version = "0.4.4" + +[[deps.CommonSolve]] +git-tree-sha1 = "99ee296f88c12485402e37c2fd025f95ae097637" +uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" +version = "0.2.9" + +[[deps.CommonSubexpressions]] +deps = ["MacroTools"] +git-tree-sha1 = "cda2cfaebb4be89c9084adaca7dd7333369715c5" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.3.1" + [[deps.Compat]] deps = ["TOML", "UUIDs"] -git-tree-sha1 = "0037835448781bb46feb39866934e243886d756a" +git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.18.0" +version = "4.18.1" weakdeps = ["Dates", "LinearAlgebra"] [deps.Compat.extensions] @@ -94,17 +302,118 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" +version = "1.3.0+1" + +[[deps.CompositionsBase]] +git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" +uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b" +version = "0.1.2" +weakdeps = ["InverseFunctions"] + + [deps.CompositionsBase.extensions] + CompositionsBaseInverseFunctionsExt = "InverseFunctions" + +[[deps.ComputePipeline]] +deps = ["Observables", "Preferences"] +git-tree-sha1 = "7bc84b769c1d384315e7b5c4ac03a6c303e6cf35" +uuid = "95dc2771-c249-4cd0-9c9f-1f3b4330693c" +version = "0.1.8" + +[[deps.ConstructionBase]] +git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb" +uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" +version = "1.6.0" +weakdeps = ["IntervalSets", "LinearAlgebra", "StaticArrays"] + + [deps.ConstructionBase.extensions] + ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseLinearAlgebraExt = "LinearAlgebra" + ConstructionBaseStaticArraysExt = "StaticArrays" + +[[deps.Contour]] +git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8" +uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" +version = "0.6.3" + +[[deps.CoreMath]] +deps = ["CoreMath_jll"] +git-tree-sha1 = "8c0480f92b1b1796239156a1b9b1bfb1b39499b4" +uuid = "b7a15901-be09-4a0e-87d2-2e66b0e09b5a" +version = "0.1.0" + +[[deps.CoreMath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a692a4c1dc59a4b8bc0b6403876eb3250fde2bc3" +uuid = "a38c48d9-6df1-5ac9-9223-b6ada3b5572b" +version = "0.1.0+0" + +[[deps.DataAPI]] +git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.16.0" [[deps.DataStructures]] -deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" +deps = ["OrderedCollections"] +git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.22" +version = "0.19.5" + +[[deps.DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" [[deps.Dates]] deps = ["Printf"] uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.DelaunayTriangulation]] +deps = ["AdaptivePredicates", "EnumX", "ExactPredicates", "Random"] +git-tree-sha1 = "c55f5a9fd67bdbc8e089b5a3111fe4292986a8e8" +uuid = "927a84f5-c5f4-47a5-9785-b46e178433df" +version = "1.6.6" + +[[deps.DiffResults]] +deps = ["StaticArraysCore"] +git-tree-sha1 = "782dd5f4561f5d267313f23853baaaa4c52ea621" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "1.1.0" + +[[deps.DiffRules]] +deps = ["IrrationalConstants", "LogExpFunctions", "NaNMath", "Random", "SpecialFunctions"] +git-tree-sha1 = "79a2aca180a85c690c58a020d47b426954b590f8" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "1.16.0" + +[[deps.DiskArrays]] +deps = ["ConstructionBase", "LRUCache", "Mmap", "OffsetArrays"] +git-tree-sha1 = "9903195c34a488c5265d9a105f39718b7a2b3568" +uuid = "3c3547ce-8d99-4f5e-a174-61eb10b00ae3" +version = "0.4.22" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +version = "1.11.0" + +[[deps.Distributions]] +deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] +git-tree-sha1 = "cd3c5ac74cd3923c8945c6a81518c46abd0e73a3" +uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" +version = "0.25.129" + + [deps.Distributions.extensions] + DistributionsChainRulesCoreExt = "ChainRulesCore" + DistributionsDensityInterfaceExt = "DensityInterface" + DistributionsSparseConnectivityTracerExt = "SparseConnectivityTracer" + DistributionsTestExt = "Test" + + [deps.Distributions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" + SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [[deps.DocStringExtensions]] git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" @@ -126,19 +435,48 @@ version = "1.4.0" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +version = "1.7.0" + +[[deps.EarCut_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e3290f2d49e661fbd94046d7e3726ffcb2d41053" +uuid = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" +version = "2.2.4+0" + +[[deps.EnumX]] +git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c" +uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" +version = "1.0.7" + +[[deps.ExactPredicates]] +deps = ["IntervalArithmetic", "Random", "StaticArrays"] +git-tree-sha1 = "83231673ea4d3d6008ac74dc5079e77ab2209d8f" +uuid = "429591f6-91af-11e9-00e2-59fbe8cec110" +version = "2.2.9" [[deps.Expat_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "d55dffd9ae73ff72f1c0482454dcf2ec6c6c4a63" +git-tree-sha1 = "e6c4a6407a949e79a9d3f249bf49e6987c80e01f" uuid = "2e619515-83b5-522b-bb60-26c02a35a201" -version = "2.6.5+0" +version = "2.8.2+0" + +[[deps.FFMPEG_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libva_jll", "libvorbis_jll", "x264_jll", "x265_jll"] +git-tree-sha1 = "7a58e45171b63ed4782f2d36fdee8713a469e6e0" +uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" +version = "8.1.2+0" + +[[deps.FFTA]] +deps = ["AbstractFFTs", "DocStringExtensions", "LinearAlgebra", "MuladdMacro", "Primes", "Random", "Reexport"] +git-tree-sha1 = "65e55303b72f4a567a51b174dd2c47496efeb95a" +uuid = "b86e33f2-c0db-4aa1-a6e0-ab43e668529e" +version = "0.3.1" [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] -git-tree-sha1 = "b66970a70db13f45b7e57fbda1736e1cf72174ea" +git-tree-sha1 = "8e9c059d6857607253e837730dbf780b6b151acd" uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -version = "1.17.0" +version = "1.19.0" [deps.FileIO.extensions] HTTPExt = "HTTP" @@ -146,20 +484,199 @@ version = "1.17.0" [deps.FileIO.weakdeps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +[[deps.FilePaths]] +deps = ["FilePathsBase", "MacroTools", "Reexport"] +git-tree-sha1 = "a1b2fbfe98503f15b665ed45b3d149e5d8895e4c" +uuid = "8fc22ac5-c921-52a6-82fd-178b2807b824" +version = "0.9.0" + + [deps.FilePaths.extensions] + FilePathsGlobExt = "Glob" + FilePathsURIParserExt = "URIParser" + FilePathsURIsExt = "URIs" + + [deps.FilePaths.weakdeps] + Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" + URIParser = "30578b45-9adc-5946-b283-645ec420af67" + URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" + +[[deps.FilePathsBase]] +deps = ["Compat", "Dates"] +git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2" +uuid = "48062228-2e41-5def-b9a4-89aafe57970f" +version = "0.9.24" +weakdeps = ["Mmap", "Test"] + + [deps.FilePathsBase.extensions] + FilePathsBaseMmapExt = "Mmap" + FilePathsBaseTestExt = "Test" + [[deps.FileWatching]] uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.FillArrays]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3" +uuid = "1a297f60-69ca-5386-bcde-b61e274b549b" +version = "1.16.0" +weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"] + + [deps.FillArrays.extensions] + FillArraysPDMatsExt = "PDMats" + FillArraysSparseArraysExt = "SparseArrays" + FillArraysStaticArraysExt = "StaticArrays" + FillArraysStatisticsExt = "Statistics" + +[[deps.FixedPointNumbers]] +deps = ["Random", "Statistics"] +git-tree-sha1 = "59af96b98217c6ef4ae0dfe065ac7c20831d1a84" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.6" + +[[deps.Fontconfig_jll]] +deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Zlib_jll"] +git-tree-sha1 = "f85dac9a96a01087df6e3a749840015a0ca3817d" +uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" +version = "2.17.1+0" + +[[deps.Format]] +git-tree-sha1 = "9c68794ef81b08086aeb32eeaf33531668d5f5fc" +uuid = "1fa38f19-a742-5d3f-a2b9-30dd87b9d5f8" +version = "1.3.7" + +[[deps.ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] +git-tree-sha1 = "2c5d0b0e12088cde2cf84afb2784415b1ea3dfee" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "1.4.1" +weakdeps = ["StaticArrays"] + + [deps.ForwardDiff.extensions] + ForwardDiffStaticArraysExt = "StaticArrays" + +[[deps.FreeType]] +deps = ["CEnum", "FreeType2_jll"] +git-tree-sha1 = "907369da0f8e80728ab49c1c7e09327bf0d6d999" +uuid = "b38be410-82b0-50bf-ab77-7b57e271db43" +version = "4.1.1" + +[[deps.FreeType2_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "70329abc09b886fd2c5d94ad2d9527639c421e3e" +uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +version = "2.14.3+1" + +[[deps.FreeTypeAbstraction]] +deps = ["BaseDirs", "ColorVectorSpace", "Colors", "FreeType", "GeometryBasics", "Mmap"] +git-tree-sha1 = "4ebb930ef4a43817991ba35db6317a05e59abd11" +uuid = "663a7486-cb36-511b-a19d-713bb74d65c9" +version = "0.10.8" + +[[deps.FriBidi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "7a214fdac5ed5f59a22c2d9a885a16da1c74bbc7" +uuid = "559328eb-81f9-559d-9380-de523a88c83c" +version = "1.0.17+0" + +[[deps.Gamma]] +git-tree-sha1 = "86f86b6168a016ed88e4ae4e64577b98c3b59e8e" +uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b" +version = "1.1.0" + +[[deps.GeometryBasics]] +deps = ["EarCut_jll", "LinearAlgebra", "PrecompileTools", "Random", "StaticArrays"] +git-tree-sha1 = "364685f5ffde25deb1bbcfd5bb278a5c6b7a9b37" +uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +version = "0.5.11" + + [deps.GeometryBasics.extensions] + ExtentsExt = "Extents" + GeometryBasicsGeoInterfaceExt = "GeoInterface" + IntervalSetsExt = "IntervalSets" + + [deps.GeometryBasics.weakdeps] + Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" + GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + +[[deps.GettextRuntime_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll"] +git-tree-sha1 = "45288942190db7c5f760f59c04495064eedf9340" +uuid = "b0724c58-0f36-5564-988d-3bb0596ebc4a" +version = "0.22.4+0" + +[[deps.Giflib_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "6570366d757b50fabae9f4315ad74d2e40c0560a" +uuid = "59f7168a-df46-5410-90c8-f2779963d0ec" +version = "5.2.3+0" [[deps.Git]] -deps = ["Git_jll", "JLLWrappers", "OpenSSH_jll"] -git-tree-sha1 = "2230a9cc32394b11a3b3aa807a382e3bbab1198c" +deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] +git-tree-sha1 = "824a1890086880696fc908fe12a17bcf61738bd8" uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" -version = "1.4.0" +version = "1.5.0" + +[[deps.Git_LFS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8c66e385d631bb934ff05e76d4a566c640c8df69" +uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" +version = "3.7.1+0" [[deps.Git_jll]] deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] -git-tree-sha1 = "cb151153e40ad40a6dbf984fcd767e1d266fcc9c" +git-tree-sha1 = "0dd4cfb426924210c8f42742751cbde74b27bfa3" uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" -version = "2.50.1+0" +version = "2.54.0+0" + +[[deps.Glib_jll]] +deps = ["Artifacts", "GettextRuntime_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "24f6def62397474a297bfcec22384101609142ed" +uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" +version = "2.86.3+0" + +[[deps.Graphics]] +deps = ["Colors", "LinearAlgebra", "NaNMath"] +git-tree-sha1 = "a641238db938fff9b2f60d08ed9030387daf428c" +uuid = "a2bd30eb-e257-5431-a919-1863eab51364" +version = "1.1.3" + +[[deps.Graphite2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "69ffb934a5c5b7e086a0b4fee3427db2556fba6e" +uuid = "3b182d85-2403-5c21-9c21-1e1f0cc25472" +version = "1.3.16+0" + +[[deps.GridLayoutBase]] +deps = ["GeometryBasics", "InteractiveUtils", "Observables"] +git-tree-sha1 = "93d5c27c8de51687a2c70ec0716e6e76f298416f" +uuid = "3955a311-db13-416c-9275-1d80ed98e5e9" +version = "0.11.2" + +[[deps.HDF5_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "OpenSSL_jll", "TOML", "Zlib_jll", "aws_c_s3_jll", "dlfcn_win32_jll", "libaec_jll", "mpif_jll"] +git-tree-sha1 = "45337643a2d97262d5fe72ce1f13e8a662d13d62" +uuid = "0234f1f7-429e-5d53-9886-15a909be8d59" +version = "2.1.2+0" + +[[deps.HarfBuzz_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] +git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" +uuid = "2e76f6c2-a576-52d4-95c1-20adfe4de566" +version = "8.5.1+0" + +[[deps.Hwloc_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "XML2_jll", "Xorg_libpciaccess_jll"] +git-tree-sha1 = "c35847ca5b4997fc8418836354a56c459bcf48d8" +uuid = "e33a78d0-f292-5ffc-b300-72abe9b543c8" +version = "2.14.0+0" + +[[deps.HypergeometricFunctions]] +deps = ["Gamma", "LinearAlgebra"] +git-tree-sha1 = "18d7deab5fb0440dc6a7b6993c5c27b25420de10" +uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" +version = "0.3.29" [[deps.IOCapture]] deps = ["Logging", "Random"] @@ -167,39 +684,224 @@ git-tree-sha1 = "b6d6bfdd7ce25b0f9b2f6b3dd56b2673a66c8770" uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" version = "0.2.5" +[[deps.ImageAxes]] +deps = ["AxisArrays", "ImageBase", "ImageCore", "Reexport", "SimpleTraits"] +git-tree-sha1 = "e12629406c6c4442539436581041d372d69c55ba" +uuid = "2803e5a7-5153-5ecf-9a86-9b4c37f5f5ac" +version = "0.6.12" + +[[deps.ImageBase]] +deps = ["ImageCore", "Reexport"] +git-tree-sha1 = "eb49b82c172811fd2c86759fa0553a2221feb909" +uuid = "c817782e-172a-44cc-b673-b171935fbb9e" +version = "0.1.7" + +[[deps.ImageCore]] +deps = ["ColorVectorSpace", "Colors", "FixedPointNumbers", "MappedArrays", "MosaicViews", "OffsetArrays", "PaddedViews", "PrecompileTools", "Reexport"] +git-tree-sha1 = "8c193230235bbcee22c8066b0374f63b5683c2d3" +uuid = "a09fc81d-aa75-5fe9-8630-4744c3626534" +version = "0.10.5" + +[[deps.ImageIO]] +deps = ["FileIO", "IndirectArrays", "JpegTurbo", "LazyModules", "Netpbm", "OpenEXR", "PNGFiles", "QOI", "Sixel", "TiffImages", "UUIDs", "WebP"] +git-tree-sha1 = "696144904b76e1ca433b886b4e7edd067d76cbf7" +uuid = "82e4d734-157c-48bb-816b-45c225c6df19" +version = "0.6.9" + +[[deps.ImageMetadata]] +deps = ["AxisArrays", "ImageAxes", "ImageBase", "ImageCore"] +git-tree-sha1 = "2a81c3897be6fbcde0802a0ebe6796d0562f63ec" +uuid = "bc367c6b-8a6b-528e-b4bd-a4b897500b49" +version = "0.9.10" + +[[deps.Imath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "dcc8d0cd653e55213df9b75ebc6fe4a8d3254c65" +uuid = "905a6f67-0a94-5f89-b386-d35d92009cd1" +version = "3.2.2+0" + +[[deps.IndirectArrays]] +git-tree-sha1 = "012e604e1c7458645cb8b436f8fba789a51b257f" +uuid = "9b13fd28-a010-5f03-acff-a1bbcff69959" +version = "1.0.0" + +[[deps.Inflate]] +git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" +uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +version = "0.1.5" + +[[deps.IntegerMathUtils]] +git-tree-sha1 = "4c1acff2dc6b6967e7e750633c50bc3b8d83e617" +uuid = "18e54dd8-cb9d-406c-a71d-865a43cbb235" +version = "0.1.3" + [[deps.InteractiveUtils]] deps = ["Markdown"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.Interpolations]] +deps = ["Adapt", "AxisAlgorithms", "ChainRulesCore", "LinearAlgebra", "OffsetArrays", "Random", "Ratios", "SharedArrays", "SparseArrays", "StaticArrays", "WoodburyMatrices"] +git-tree-sha1 = "48922d06068130f87e43edef52382e6a94305ae6" +uuid = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +version = "0.16.3" +weakdeps = ["ForwardDiff", "Unitful"] + + [deps.Interpolations.extensions] + InterpolationsForwardDiffExt = "ForwardDiff" + InterpolationsUnitfulExt = "Unitful" + +[[deps.IntervalArithmetic]] +deps = ["CRlibm", "CoreMath", "MacroTools", "OpenBLASConsistentFPCSR_jll", "Printf", "Random", "RoundingEmulator"] +git-tree-sha1 = "c3ee408ae340565f41699e3a3fa1053698c7626e" +uuid = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253" +version = "1.0.10" + + [deps.IntervalArithmetic.extensions] + IntervalArithmeticArblibExt = "Arblib" + IntervalArithmeticDiffRulesExt = "DiffRules" + IntervalArithmeticForwardDiffExt = "ForwardDiff" + IntervalArithmeticIntervalSetsExt = "IntervalSets" + IntervalArithmeticIrrationalConstantsExt = "IrrationalConstants" + IntervalArithmeticLinearAlgebraExt = "LinearAlgebra" + IntervalArithmeticRecipesBaseExt = "RecipesBase" + IntervalArithmeticSparseArraysExt = "SparseArrays" + + [deps.IntervalArithmetic.weakdeps] + Arblib = "fb37089c-8514-4489-9461-98f9c8763369" + DiffRules = "b552c78f-8df3-52c6-915a-8e097449b14b" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[deps.IntervalSets]] +git-tree-sha1 = "79d6bd28c8d9bccc2229784f1bd637689b256377" +uuid = "8197267c-284f-5f27-9208-e0e47529a953" +version = "0.7.14" + + [deps.IntervalSets.extensions] + IntervalSetsRandomExt = "Random" + IntervalSetsRecipesBaseExt = "RecipesBase" + IntervalSetsStatisticsExt = "Statistics" + + [deps.IntervalSets.weakdeps] + Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" + Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[deps.InverseFunctions]] +git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" +uuid = "3587e190-3f89-42d0-90ee-14403ec27112" +version = "0.1.17" +weakdeps = ["Dates", "Test"] + + [deps.InverseFunctions.extensions] + InverseFunctionsDatesExt = "Dates" + InverseFunctionsTestExt = "Test" + +[[deps.IrrationalConstants]] +git-tree-sha1 = "b2d91fe939cae05960e760110b328288867b5758" +uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" +version = "0.2.6" + +[[deps.Isoband]] +deps = ["isoband_jll"] +git-tree-sha1 = "f9b6d97355599074dc867318950adaa6f9946137" +uuid = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" +version = "0.1.1" + +[[deps.IterTools]] +git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023" +uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" +version = "1.10.0" + +[[deps.IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" [[deps.JLLWrappers]] deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.7.1" +version = "1.8.0" [[deps.JSON]] -deps = ["Dates", "Mmap", "Parsers", "Unicode"] -git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.21.4" - -[[deps.JSON3]] -deps = ["Dates", "Mmap", "Parsers", "PrecompileTools", "StructTypes", "UUIDs"] -git-tree-sha1 = "411eccfe8aba0814ffa0fdf4860913ed09c34975" -uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.14.3" +version = "1.6.1" - [deps.JSON3.extensions] - JSON3ArrowExt = ["ArrowTypes"] + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] - [deps.JSON3.weakdeps] + [deps.JSON.weakdeps] ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" [[deps.JSONSchema]] -deps = ["Downloads", "JSON", "JSON3", "URIs"] -git-tree-sha1 = "243f1cdb476835d7c249deb9f29ad6b7827da7d3" +deps = ["Downloads", "JSON", "URIs"] +git-tree-sha1 = "d13f79c4242969874da7d00bda17d59bc7699aa7" uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" -version = "1.4.1" +version = "1.5.0" + + [deps.JSONSchema.extensions] + JSONSchemaJSON3Ext = "JSON3" + + [deps.JSONSchema.weakdeps] + JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" + +[[deps.JpegTurbo]] +deps = ["CEnum", "FileIO", "ImageCore", "JpegTurbo_jll", "TOML"] +git-tree-sha1 = "9496de8fb52c224a2e3f9ff403947674517317d9" +uuid = "b835a17e-a41a-41e7-81f0-2f016b05efe0" +version = "0.1.6" + +[[deps.JpegTurbo_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1dae3057da6f2b9c857afef03177bbdc7c4afe92" +uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +version = "3.2.0+0" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.KernelDensity]] +deps = ["Distributions", "DocStringExtensions", "FFTA", "Interpolations", "StatsBase"] +git-tree-sha1 = "9eda8292dd3268b3b7ec9df21bbfac24e177ec52" +uuid = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" +version = "0.6.12" + +[[deps.LAME_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "059aabebaa7c82ccb853dd4a0ee9d17796f7e1bc" +uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" +version = "3.100.3+0" + +[[deps.LERC_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "17b94ecafcfa45e8360a4fc9ca6b583b049e4e37" +uuid = "88015f11-f218-50d7-93a8-a6af411a945d" +version = "4.1.0+0" + +[[deps.LLVMOpenMP_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "3ac157462e1e800777cc97d0eafd1bdb5356a470" +uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" +version = "21.1.8+0" + +[[deps.LRUCache]] +git-tree-sha1 = "5519b95a490ff5fe629c4a7aa3b3dfc9160498b3" +uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" +version = "1.6.2" +weakdeps = ["Serialization"] + + [deps.LRUCache.extensions] + SerializationExt = ["Serialization"] [[deps.LaTeXStrings]] git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c" @@ -211,32 +913,56 @@ git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0" uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" version = "1.3.0" +[[deps.LazyArtifacts]] +deps = ["Artifacts", "Pkg"] +uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3" +version = "1.11.0" + +[[deps.LazyModules]] +git-tree-sha1 = "a560dd966b386ac9ae60bdd3a3d3a326062d3c3e" +uuid = "8cdb02fc-e678-4876-92c5-9defec4f444e" +version = "0.3.1" + [[deps.LibCURL]] deps = ["LibCURL_jll", "MozillaCACerts_jll"] uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" version = "0.6.4" [[deps.LibCURL_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.4.0+0" +version = "8.15.0+0" [[deps.LibGit2]] -deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" [[deps.LibGit2_jll]] -deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" -version = "1.6.4+0" +version = "1.9.0+0" [[deps.LibSSH2_jll]] -deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" -version = "1.11.0+1" +version = "1.11.3+1" [[deps.Libdl]] uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Libffi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "c8da7e6a91781c41a863611c7e966098d783c57a" +uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +version = "3.4.7+0" + +[[deps.Libglvnd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll"] +git-tree-sha1 = "d36c21b9e7c172a44a10484125024495e2625ac0" +uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" +version = "1.7.1+1" [[deps.Libiconv_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -244,108 +970,417 @@ git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" version = "1.18.0+0" +[[deps.Libmount_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "cc3ad4faf30015a3e8094c9b5b7f19e85bdf2386" +uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +version = "2.42.0+0" + +[[deps.Libtiff_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "LERC_jll", "Libdl", "XZ_jll", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "aebd334d06cee9f24cea70bd19a39749daf73881" +uuid = "89763e89-9b03-5906-acba-b20f662cd828" +version = "4.7.3+0" + +[[deps.Libuuid_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "d620582b1f0cbe2c72dd1d5bd195a9ce73370ab1" +uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +version = "2.42.0+0" + [[deps.LinearAlgebra]] deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.12.0" + +[[deps.Literate]] +deps = ["Base64", "IOCapture", "JSON", "REPL"] +git-tree-sha1 = "bb26d8b8ed0fa451ce3511e99c950653a2f31fe1" +uuid = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +version = "2.21.0" + +[[deps.LogExpFunctions]] +deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] +git-tree-sha1 = "bba2d9aa057d8f126415de240573e86a8f39d2a1" +uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" +version = "1.0.1" + + [deps.LogExpFunctions.extensions] + LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" + LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" + LogExpFunctionsInverseFunctionsExt = "InverseFunctions" + + [deps.LogExpFunctions.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ChangesOfVariables = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" [[deps.LoggingExtras]] deps = ["Dates", "Logging"] -git-tree-sha1 = "f02b56007b064fbfddb4c9cd60161b6dd0f40df3" +git-tree-sha1 = "f00544d95982ea270145636c181ceda21c4e2575" uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" -version = "1.1.0" +version = "1.2.0" + +[[deps.Lz4_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33" +uuid = "5ced341a-0733-55b8-9ab6-a4889d929147" +version = "1.10.1+0" + +[[deps.MPIABI_jll]] +deps = ["Artifacts", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "9be143b6045719e8fb019d2b3bc2aebad1184fef" +uuid = "b5ada748-db0f-5fc0-8972-9331c762740c" +version = "0.1.5+0" + +[[deps.MPICH_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "07dbec8aab01696edc0151a401a6cdfe95b9b885" +uuid = "7cb0a576-ebde-5e09-9194-50597f1243b4" +version = "5.0.1+0" + +[[deps.MPIPreferences]] +deps = ["Libdl", "Preferences"] +git-tree-sha1 = "8e98d5d80b87403c311fd51e8455d4546ba7a5f8" +uuid = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267" +version = "0.1.12" + +[[deps.MPItrampoline_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "675df097f8eeb28998b2cfe3b25655af73d5f7df" +uuid = "f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748" +version = "5.5.6+0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Makie]] +deps = ["Animations", "Base64", "CRC32c", "ColorBrewer", "ColorSchemes", "ColorTypes", "Colors", "ComputePipeline", "Contour", "Dates", "DelaunayTriangulation", "Distributions", "DocStringExtensions", "Downloads", "FFMPEG_jll", "FileIO", "FilePaths", "FixedPointNumbers", "Format", "FreeType", "FreeTypeAbstraction", "GeometryBasics", "GridLayoutBase", "ImageBase", "ImageIO", "InteractiveUtils", "Interpolations", "IntervalSets", "InverseFunctions", "Isoband", "KernelDensity", "LaTeXStrings", "LinearAlgebra", "MacroTools", "Markdown", "MathTeXEngine", "Observables", "OffsetArrays", "PNGFiles", "Packing", "Pkg", "PlotUtils", "PolygonOps", "PrecompileTools", "Printf", "REPL", "Random", "RelocatableFolders", "Scratch", "ShaderAbstractions", "SignedDistanceFields", "SparseArrays", "Statistics", "StatsBase", "StatsFuns", "StructArrays", "TriplotBase", "UnicodeFun", "Unitful"] +git-tree-sha1 = "f2c8715d05bf10f9d4dc354e69dee30b6be53239" +uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +version = "0.24.13" + + [deps.Makie.extensions] + MakieDynamicQuantitiesExt = "DynamicQuantities" + + [deps.Makie.weakdeps] + DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" + +[[deps.MappedArrays]] +git-tree-sha1 = "0ee4497a4e80dbd29c058fcee6493f5219556f40" +uuid = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" +version = "0.4.3" [[deps.Markdown]] -deps = ["Base64"] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" [[deps.MarkdownAST]] deps = ["AbstractTrees", "Markdown"] -git-tree-sha1 = "465a70f0fc7d443a00dcdc3267a497397b8a3899" +git-tree-sha1 = "93c718d892e73931841089cdc0e982d6dd9cc87b" uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" -version = "0.1.2" +version = "0.1.3" -[[deps.MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.2+1" +[[deps.MathTeXEngine]] +deps = ["AbstractTrees", "Automa", "DataStructures", "FreeTypeAbstraction", "GeometryBasics", "LaTeXStrings", "REPL", "RelocatableFolders", "UnicodeFun"] +git-tree-sha1 = "aa1078778be5a8e5259ff04fbc3d258b3e78d464" +uuid = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" +version = "0.6.9" + +[[deps.MicrosoftMPI_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bc95bf4149bf535c09602e3acdf950d9b4376227" +uuid = "9237b28f-5490-5468-be7b-bb81f5f5e6cf" +version = "10.1.4+3" + +[[deps.Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.2.0" [[deps.Mmap]] uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + +[[deps.MosaicViews]] +deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"] +git-tree-sha1 = "7b86a5d4d70a9f5cdf2dacb3cbe6d251d1a61dbe" +uuid = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" +version = "0.3.4" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2023.1.10" +version = "2025.11.4" + +[[deps.MuladdMacro]] +deps = ["PrecompileTools"] +git-tree-sha1 = "e8dcbeef032ba2f9051a44ac22b4e54e3a1a0099" +uuid = "46d2c3a1-f734-5fdb-9937-b9b9aeba4221" +version = "0.2.6" + +[[deps.NCDatasets]] +deps = ["CFTime", "CommonDataModel", "DataStructures", "Dates", "DiskArrays", "NetCDF_jll", "NetworkOptions", "Printf"] +git-tree-sha1 = "5eb7747d10437f5acb2675c1f865ffa0353f3f9c" +uuid = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" +version = "0.14.15" + + [deps.NCDatasets.extensions] + NCDatasetsMPIExt = "MPI" + + [deps.NCDatasets.weakdeps] + MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195" + +[[deps.NaNMath]] +deps = ["OpenLibm_jll"] +git-tree-sha1 = "dbd2e8cd2c1c27f0b584f6661b4309609c5a685e" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "1.1.4" + +[[deps.NetCDF_jll]] +deps = ["Artifacts", "Blosc_jll", "Bzip2_jll", "HDF5_jll", "JLLWrappers", "LazyArtifacts", "LibCURL_jll", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "TOML", "XML2_jll", "Zlib_jll", "Zstd_jll", "libaec_jll", "libzip_jll"] +git-tree-sha1 = "8a36db9b934b0e72583e624abc8f3b3d60554f2c" +uuid = "7243133f-43d8-5620-bbf4-c2c921802cf3" +version = "401.1000.0+0" + +[[deps.Netpbm]] +deps = ["FileIO", "ImageCore", "ImageMetadata"] +git-tree-sha1 = "d92b107dbb887293622df7697a2223f9f8176fcd" +uuid = "f09324ee-3d7c-5217-9330-fc30815ba969" +version = "1.1.1" [[deps.NetworkOptions]] uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" -version = "1.2.0" +version = "1.3.0" + +[[deps.Observables]] +git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" +uuid = "510215fc-4207-5dde-b226-833fc4488ee2" +version = "0.5.5" + +[[deps.OffsetArrays]] +git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151" +uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +version = "1.17.0" +weakdeps = ["Adapt"] + + [deps.OffsetArrays.extensions] + OffsetArraysAdaptExt = "Adapt" + +[[deps.Ogg_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b6aa4566bb7ae78498a5e68943863fa8b5231b59" +uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" +version = "1.3.6+0" + +[[deps.OpenBLASConsistentFPCSR_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "dafdaa3ff15f20ff703d909d3a6f574a5b0586f3" +uuid = "6cdc7f73-28fd-5e50-80fb-958a8875b1af" +version = "0.3.33+1" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.23+4" +version = "0.3.29+0" + +[[deps.OpenEXR]] +deps = ["Colors", "FileIO", "OpenEXR_jll"] +git-tree-sha1 = "97db9e07fe2091882c765380ef58ec553074e9c7" +uuid = "52e1d378-f018-4a11-a4be-720524705ac7" +version = "0.3.3" + +[[deps.OpenEXR_jll]] +deps = ["Artifacts", "Imath_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "0d621a4beb5e48d195f907c3c5b0bea285d9ff9d" +uuid = "18a262bb-aa17-5467-a713-aee519bc75cb" +version = "3.4.13+0" + +[[deps.OpenLibm_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "05823500-19ac-5b8b-9628-191a04bc5112" +version = "0.8.7+0" + +[[deps.OpenMPI_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML", "Zlib_jll"] +git-tree-sha1 = "6d6c0ca4824268c1a7dca1f4721c535ac63d9074" +uuid = "fe0851c0-eecd-5654-98d4-656369965a5c" +version = "5.0.11+0" [[deps.OpenSSH_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] -git-tree-sha1 = "cb7acd5d10aff809b4d0191dfe1956c2edf35800" +git-tree-sha1 = "b862f484cf659efabffd601c5c920e981c942b63" uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" -version = "10.0.1+0" +version = "10.4.1+0" [[deps.OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "2ae7d4ddec2e13ad3bddf5c0796f7547cf682391" +deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.2+0" +version = "3.5.4+0" + +[[deps.OpenSpecFun_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1346c9208249809840c91b26703912dff463d335" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.6+0" + +[[deps.Opus_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e2bb57a313a74b8104064b7efd01406c0a50d2ff" +uuid = "91d4177d-7536-5919-b921-800302f37372" +version = "1.6.1+0" [[deps.OrderedCollections]] -git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4" uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.1" +version = "1.8.2" [[deps.PCRE2_jll]] deps = ["Artifacts", "Libdl"] uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" -version = "10.42.0+1" +version = "10.44.0+1" + +[[deps.PDMats]] +deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"] +git-tree-sha1 = "26766d4b5f1a410c218a19b85a672c6edb693c65" +uuid = "90014a1f-27ba-587c-ab20-58faa44d9150" +version = "0.11.40" +weakdeps = ["StatsBase"] + + [deps.PDMats.extensions] + StatsBaseExt = "StatsBase" + +[[deps.PNGFiles]] +deps = ["Base64", "CEnum", "ImageCore", "IndirectArrays", "OffsetArrays", "libpng_jll"] +git-tree-sha1 = "32b657a0d57c310a1a172bfc8c8cf68c5e674323" +uuid = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" +version = "0.4.5" + +[[deps.Packing]] +deps = ["GeometryBasics"] +git-tree-sha1 = "bc5bf2ea3d5351edf285a06b0016788a121ce92c" +uuid = "19eb6ba3-879d-56ad-ad62-d5c202156566" +version = "0.5.1" + +[[deps.PaddedViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "0fac6313486baae819364c52b4f483450a9d793f" +uuid = "5432bcbf-9aad-5242-b902-cca2824c8663" +version = "0.5.12" + +[[deps.Pango_jll]] +deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl"] +git-tree-sha1 = "58e5ed5e386e156bd93e86b305ebd21ac63d2d04" +uuid = "36c8627f-9965-5494-a995-c6b170f724f3" +version = "1.57.1+0" [[deps.Parsers]] deps = ["Dates", "PrecompileTools", "UUIDs"] -git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" +git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.8.3" +version = "2.8.6" + +[[deps.Pixman_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] +git-tree-sha1 = "e4a6721aa89e62e5d4217c0b21bd714263779dda" +uuid = "30392449-352a-5448-841d-b1acce4e97dc" +version = "0.46.4+0" [[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.10.0" +version = "1.12.1" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" + +[[deps.PkgVersion]] +deps = ["Pkg"] +git-tree-sha1 = "f9501cc0430a26bc3d156ae1b5b0c1b47af4d6da" +uuid = "eebad327-c553-4316-9ea0-9fa01ccd7688" +version = "0.3.3" + +[[deps.PlotUtils]] +deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "StableRNGs", "Statistics"] +git-tree-sha1 = "26ca162858917496748aad52bb5d3be4d26a228a" +uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" +version = "1.4.4" + +[[deps.PolygonOps]] +git-tree-sha1 = "77b3d3605fc1cd0b42d95eba87dfcd2bf67d5ff6" +uuid = "647866c9-e3ac-4575-94e7-e3d426903924" +version = "0.1.2" [[deps.PrecompileTools]] deps = ["Preferences"] -git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f" +git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.2.1" +version = "1.3.4" [[deps.Preferences]] deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" +version = "1.5.2" + +[[deps.Primes]] +deps = ["IntegerMathUtils"] +git-tree-sha1 = "25cdd1d20cd005b52fc12cb6be3f75faaf59bb9b" +uuid = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae" +version = "0.5.7" [[deps.Printf]] deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.ProgressMeter]] +deps = ["Distributed", "Printf"] +git-tree-sha1 = "fbb92c6c56b34e1a2c4c36058f68f332bec840e7" +uuid = "92933f4c-e287-5a05-a399-4b506db050ca" +version = "1.11.0" + +[[deps.PtrArrays]] +git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" +uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" +version = "1.4.0" + +[[deps.QOI]] +deps = ["ColorTypes", "FileIO", "FixedPointNumbers"] +git-tree-sha1 = "472daaa816895cb7aee81658d4e7aec901fa1106" +uuid = "4b34888f-f399-49d4-9bb3-47ed5cae4e65" +version = "1.0.2" + +[[deps.QuadGK]] +deps = ["DataStructures", "LinearAlgebra"] +git-tree-sha1 = "5e8e8b0ab68215d7a2b14b9921a946fee794749e" +uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" +version = "2.11.3" + + [deps.QuadGK.extensions] + QuadGKEnzymeExt = "Enzyme" + + [deps.QuadGK.weakdeps] + Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" [[deps.REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" [[deps.RRTMGP]] -deps = ["Adapt", "Artifacts", "ClimaComms", "DocStringExtensions", "Random"] +deps = ["Adapt", "Artifacts", "ClimaComms", "Random", "Serialization"] path = ".." uuid = "a01a1ee8-cea4-48fc-987c-fc7878d79da1" -version = "0.21.31" +version = "0.21.9" [deps.RRTMGP.extensions] CreateParametersExt = "ClimaParams" @@ -360,6 +1395,27 @@ version = "0.21.31" [[deps.Random]] deps = ["SHA"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.RangeArrays]] +git-tree-sha1 = "b9039e93773ddcfc828f12aadf7115b4b4d225f5" +uuid = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d" +version = "0.3.2" + +[[deps.Ratios]] +deps = ["Requires"] +git-tree-sha1 = "1342a47bf3260ee108163042310d26f2be5ec90b" +uuid = "c84ed2f1-dad5-54f0-aa8e-dbefe2724439" +version = "0.4.5" +weakdeps = ["FixedPointNumbers"] + + [deps.Ratios.extensions] + RatiosFixedPointNumbersExt = "FixedPointNumbers" + +[[deps.Reexport]] +git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.2.2" [[deps.RegistryInstances]] deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"] @@ -367,21 +1423,197 @@ git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51" uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3" version = "0.1.0" +[[deps.RelocatableFolders]] +deps = ["SHA", "Scratch"] +git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864" +uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" +version = "1.0.1" + [[deps.Requires]] deps = ["UUIDs"] git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64" uuid = "ae029012-a4dd-5104-9daa-d747884805df" version = "1.3.1" +[[deps.Rmath]] +deps = ["Random", "Rmath_jll"] +git-tree-sha1 = "5b3d50eb374cea306873b371d3f8d3915a018f0b" +uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa" +version = "0.9.0" + +[[deps.Rmath_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8" +uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f" +version = "0.5.1+0" + +[[deps.RootSolvers]] +deps = ["ForwardDiff", "Printf"] +git-tree-sha1 = "cb118a120361be967f172791efb2ee4badacd7c8" +uuid = "7181ea78-2dcb-4de3-ab41-2b8ab5a31e74" +version = "1.0.3" + +[[deps.Roots]] +deps = ["Accessors", "CommonSolve", "Printf"] +git-tree-sha1 = "46d2af536e1afe8f04cf31a59298adadf96e99e6" +uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" +version = "3.0.2" + + [deps.Roots.extensions] + RootsChainRulesCoreExt = "ChainRulesCore" + RootsForwardDiffExt = "ForwardDiff" + RootsIntervalRootFindingExt = "IntervalRootFinding" + RootsSymPyExt = "SymPy" + RootsSymPyPythonCallExt = "SymPyPythonCall" + RootsUnitfulExt = "Unitful" + + [deps.Roots.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" + SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" + SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" + Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" + +[[deps.RoundingEmulator]] +git-tree-sha1 = "40b9edad2e5287e05bd413a38f61a8ff55b9557b" +uuid = "5eaf0fd0-dfba-4ccb-bf02-d820a40db705" +version = "0.2.1" + [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" +[[deps.SIMD]] +deps = ["PrecompileTools"] +git-tree-sha1 = "e24dc23107d426a096d3eae6c165b921e74c18e4" +uuid = "fdea26ae-647d-5447-a871-4b548cad5224" +version = "3.7.2" + +[[deps.Scratch]] +deps = ["Dates"] +git-tree-sha1 = "9b81b8393e50b7d4e6d0a9f14e192294d3b7c109" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.3.0" + [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.ShaderAbstractions]] +deps = ["ColorTypes", "FixedPointNumbers", "GeometryBasics", "LinearAlgebra", "Observables", "StaticArrays"] +git-tree-sha1 = "818554664a2e01fc3784becb2eb3a82326a604b6" +uuid = "65257c39-d410-5151-9873-9b3e5be5013e" +version = "0.5.0" + +[[deps.SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +version = "1.11.0" + +[[deps.SignedDistanceFields]] +deps = ["Statistics"] +git-tree-sha1 = "3949ad92e1c9d2ff0cd4a1317d5ecbba682f4b92" +uuid = "73760f76-fbc4-59ce-8f25-708e95d2df96" +version = "0.4.1" + +[[deps.SimpleTraits]] +deps = ["InteractiveUtils", "MacroTools"] +git-tree-sha1 = "7ddb0b49c109481b046972c0e4ab02b2127d6a75" +uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +version = "0.9.6" + +[[deps.Sixel]] +deps = ["Dates", "FileIO", "ImageCore", "IndirectArrays", "OffsetArrays", "REPL", "libsixel_jll"] +git-tree-sha1 = "0494aed9501e7fb65daba895fb7fd57cc38bc743" +uuid = "45858cf5-a6b0-47a3-bbea-62219f50df47" +version = "0.1.5" [[deps.Sockets]] uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.2.3" + +[[deps.SparseArrays]] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.12.0" + +[[deps.SpecialFunctions]] +deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "2.8.0" +weakdeps = ["ChainRulesCore"] + + [deps.SpecialFunctions.extensions] + SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" + +[[deps.StableRNGs]] +deps = ["Random"] +git-tree-sha1 = "4f96c596b8c8258cc7d3b19797854d368f243ddc" +uuid = "860ef19b-820b-49d6-a774-d7a799459cd3" +version = "1.0.4" + +[[deps.StackViews]] +deps = ["OffsetArrays"] +git-tree-sha1 = "be1cf4eb0ac528d96f5115b4ed80c26a8d8ae621" +uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15" +version = "0.1.2" + +[[deps.StaticArrays]] +deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] +git-tree-sha1 = "246a8bb2e6667f832eea063c3a56aef96429a3db" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.9.18" +weakdeps = ["ChainRulesCore", "Statistics"] + + [deps.StaticArrays.extensions] + StaticArraysChainRulesCoreExt = "ChainRulesCore" + StaticArraysStatisticsExt = "Statistics" + +[[deps.StaticArraysCore]] +git-tree-sha1 = "6ab403037779dae8c514bad259f32a447262455a" +uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +version = "1.4.4" + +[[deps.Statistics]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.1" +weakdeps = ["SparseArrays"] + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + +[[deps.StatsAPI]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "178ed29fd5b2a2cfc3bd31c13375ae925623ff36" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.8.0" + +[[deps.StatsBase]] +deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "e4d7a1a0edc20af42689ea6f4f3587a2175d50ee" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.34.12" + +[[deps.StatsFuns]] +deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"] +git-tree-sha1 = "770240df9a3b8888065046948f7a09b4e0f997d5" +uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +version = "2.2.0" +weakdeps = ["ChainRulesCore", "InverseFunctions"] + + [deps.StatsFuns.extensions] + StatsFunsChainRulesCoreExt = "ChainRulesCore" + StatsFunsInverseFunctionsExt = "InverseFunctions" [[deps.StringEncodings]] deps = ["Libiconv_jll"] @@ -389,36 +1621,120 @@ git-tree-sha1 = "b765e46ba27ecf6b44faf70df40c57aa3a547dcb" uuid = "69024149-9ee7-55f6-a4c4-859efe599b68" version = "0.3.7" -[[deps.StructTypes]] +[[deps.StructArrays]] +deps = ["ConstructionBase", "DataAPI", "Tables"] +git-tree-sha1 = "ad8002667372439f2e3611cfd14097e03fa4bccd" +uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" +version = "0.7.3" + + [deps.StructArrays.extensions] + StructArraysAdaptExt = "Adapt" + StructArraysGPUArraysCoreExt = ["GPUArraysCore", "KernelAbstractions"] + StructArraysLinearAlgebraExt = "LinearAlgebra" + StructArraysSparseArraysExt = "SparseArrays" + StructArraysStaticArraysExt = "StaticArrays" + + [deps.StructArrays.weakdeps] + Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" + GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" + KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[[deps.StructUtils]] deps = ["Dates", "UUIDs"] -git-tree-sha1 = "159331b30e94d7b11379037feeb9b690950cace8" -uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" +git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.8.2" + + [deps.StructUtils.extensions] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" version = "1.11.0" +[[deps.SuiteSparse]] +deps = ["Libdl", "LinearAlgebra", "Serialization", "SparseArrays"] +uuid = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" + +[[deps.SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "7.8.3+2" + [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" version = "1.0.3" +[[deps.TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[deps.Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"] +git-tree-sha1 = "0f38a06c83f0007bbab3cf911262841c9a0f07e0" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.13.0" + [[deps.Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" version = "1.10.0" +[[deps.TensorCore]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "1feb45f88d133a655e001435632f019a9a1bcdb6" +uuid = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50" +version = "0.1.1" + [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" [[deps.TestItems]] git-tree-sha1 = "42fd9023fef18b9b78c8343a4e2f3813ffbcefcb" uuid = "1c621080-faea-4a02-84b6-bbd5e436b8fe" version = "1.0.0" +[[deps.Thermodynamics]] +deps = ["ForwardDiff", "Random", "RootSolvers"] +git-tree-sha1 = "d100d20d7d1a251e84ffc80606a83316f14c2aac" +uuid = "b60c26fb-14c3-4610-9d3e-2d17fe7ff00c" +version = "1.2.2" +weakdeps = ["ClimaParams"] + + [deps.Thermodynamics.extensions] + CreateParametersExt = "ClimaParams" + +[[deps.TiffImages]] +deps = ["CodecZstd", "ColorTypes", "DataStructures", "DocStringExtensions", "FileIO", "FixedPointNumbers", "IndirectArrays", "Inflate", "Mmap", "OffsetArrays", "PkgVersion", "PrecompileTools", "ProgressMeter", "SIMD", "UUIDs"] +git-tree-sha1 = "9ca5f1f2d42f80df4b8c9f6ab5a64f438bbd9976" +uuid = "731e570b-9d59-4bfa-96dc-6df516fadf69" +version = "0.11.9" + [[deps.TranscodingStreams]] git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" version = "0.11.3" +[[deps.TriplotBase]] +git-tree-sha1 = "4d4ed7f294cda19382ff7de4c137d24d16adc89b" +uuid = "981d1d27-644d-49a2-9326-4793e63143c3" +version = "0.1.0" + [[deps.URIs]] git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a" uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" @@ -427,32 +1743,303 @@ version = "1.6.1" [[deps.UUIDs]] deps = ["Random", "SHA"] uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" [[deps.Unicode]] uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.UnicodeFun]] +deps = ["REPL"] +git-tree-sha1 = "53915e50200959667e78a92a418594b428dffddf" +uuid = "1cfade01-22cf-5700-b092-accc4b62d6e1" +version = "0.4.1" + +[[deps.Unitful]] +deps = ["Dates", "LinearAlgebra", "Random"] +git-tree-sha1 = "57e1b2c9de4bd6f40ecb9de4ac1797b81970d008" +uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" +version = "1.28.0" + + [deps.Unitful.extensions] + ConstructionBaseUnitfulExt = "ConstructionBase" + ForwardDiffExt = "ForwardDiff" + InverseFunctionsUnitfulExt = "InverseFunctions" + LatexifyExt = ["Latexify", "LaTeXStrings"] + NaNMathExt = "NaNMath" + PrintfExt = "Printf" + + [deps.Unitful.weakdeps] + ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" + LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" + Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" + NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" + Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.WebP]] +deps = ["CEnum", "ColorTypes", "FileIO", "FixedPointNumbers", "ImageCore", "libwebp_jll"] +git-tree-sha1 = "aa1ca3c47f119fbdae8770c29820e5e6119b83f2" +uuid = "e3aaa7dc-3e4b-44e0-be63-ffb868ccd7c1" +version = "0.1.3" + +[[deps.WoodburyMatrices]] +deps = ["LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "248a7031b3da79a127f14e5dc5f417e26f9f6db7" +uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" +version = "1.1.0" + +[[deps.XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] +git-tree-sha1 = "80d3930c6347cfce7ccf96bd3bafdf079d9c0390" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.13.9+0" + +[[deps.XZ_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "b29c22e245d092b8b4e8d3c09ad7baa586d9f573" +uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800" +version = "5.8.3+0" + +[[deps.Xorg_libX11_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] +git-tree-sha1 = "808090ede1d41644447dd5cbafced4731c56bd2f" +uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +version = "1.8.13+0" + +[[deps.Xorg_libXau_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "aa1261ebbac3ccc8d16558ae6799524c450ed16b" +uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +version = "1.0.13+0" + +[[deps.Xorg_libXdmcp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "52858d64353db33a56e13c341d7bf44cd0d7b309" +uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +version = "1.1.6+0" + +[[deps.Xorg_libXext_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "1a4a26870bf1e5d26cd585e38038d399d7e65706" +uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +version = "1.3.8+0" + +[[deps.Xorg_libXfixes_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "75e00946e43621e09d431d9b95818ee751e6b2ef" +uuid = "d091e8ba-531a-589c-9de9-94069b037ed8" +version = "6.0.2+0" + +[[deps.Xorg_libXrender_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll"] +git-tree-sha1 = "7ed9347888fac59a618302ee38216dd0379c480d" +uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +version = "0.9.12+0" + +[[deps.Xorg_libpciaccess_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "58972370b81423fc546c56a60ed1a009450177c3" +uuid = "a65dc6b1-eb27-53a1-bb3e-dea574b5389e" +version = "0.19.0+0" + +[[deps.Xorg_libxcb_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libXau_jll", "Xorg_libXdmcp_jll"] +git-tree-sha1 = "bfcaf7ec088eaba362093393fe11aa141fa15422" +uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +version = "1.17.1+0" + +[[deps.Xorg_xtrans_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a63799ff68005991f9d9491b6e95bd3478d783cb" +uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +version = "1.6.0+0" [[deps.YAML]] deps = ["Base64", "Dates", "Printf", "StringEncodings"] -git-tree-sha1 = "2f58ac39f64b41fb812340347525be3b590cce3b" +git-tree-sha1 = "a1c0c7585346251353cddede21f180b96388c403" uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" -version = "0.4.14" +version = "0.4.16" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.13+1" +version = "1.3.1+2" + +[[deps.Zstd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.7+1" + +[[deps.aws_c_auth_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_http_jll", "aws_c_sdkutils_jll"] +git-tree-sha1 = "8cab83c96af80a1be968251ce1a0548a7545484d" +uuid = "2b3700d1-4306-52e2-a478-c162f0c514be" +version = "0.9.6+0" + +[[deps.aws_c_cal_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "22c0f42f4a1f0dc5dcfa8fd267c4ac407c455e7a" +uuid = "70f11efc-bab2-57f1-b0f3-22aad4e67c4b" +version = "0.9.13+0" + +[[deps.aws_c_common_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a759cb9bf456ad792cc7898a81ae333cce9ef02a" +uuid = "73048d1d-b8c4-5092-a58d-866c5e8d1e50" +version = "0.12.6+0" + +[[deps.aws_c_compression_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "7910c72f45f44afd297c39fe43b99c56d5ed22ec" +uuid = "73a04cd5-f3d7-5bac-9290-e8adb709f224" +version = "0.3.2+0" + +[[deps.aws_c_http_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_compression_jll", "aws_c_io_jll"] +git-tree-sha1 = "e358d5a001ef7afbd4f8c5225322512819cda2f2" +uuid = "3254fc65-9028-534d-aa9d-d76d128babc6" +version = "0.10.13+0" + +[[deps.aws_c_io_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_common_jll", "s2n_tls_jll"] +git-tree-sha1 = "7e481d474b2087ee8bbf55b81bf9119f21e396d9" +uuid = "13c41daa-f319-5298-b5eb-5754e0170d52" +version = "0.26.3+0" + +[[deps.aws_c_s3_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_auth_jll", "aws_c_common_jll", "aws_c_http_jll", "aws_checksums_jll", "s2n_tls_jll"] +git-tree-sha1 = "3e9917ab25114feba657e71be41cad068b9f6595" +uuid = "bd1f34fb-993f-5903-a121-aaf302eed6d4" +version = "0.11.5+0" + +[[deps.aws_c_sdkutils_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "c43dfba2c1ab9ea9f02f2c80e86fa16f6460244e" +uuid = "1282aa60-004d-510b-9f52-12498d409daa" +version = "0.2.4+1" + +[[deps.aws_checksums_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "2570c8e23f4771a087b12a47edcaaa670ac05a01" +uuid = "b2a88e68-78e7-5e94-8c20-c02986ec140e" +version = "0.2.10+0" + +[[deps.dlfcn_win32_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e141d67ffe550eadfb5af1bdbdaf138031e4805f" +uuid = "c4b69c83-5512-53e3-94e6-de98773c479f" +version = "1.4.2+0" + +[[deps.isoband_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "51b5eeb3f98367157a7a12a1fb0aa5328946c03c" +uuid = "9a68df92-36a6-505f-a73e-abb412b6bfb4" +version = "0.2.3+0" + +[[deps.libaec_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "60f4792734488db6f42e2c7699f1d4594780bd03" +uuid = "477f73a3-ac25-53e9-8cc3-50b2fa2566f0" +version = "1.1.7+0" + +[[deps.libaom_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "850b06095ee71f0135d644ffd8a52850699581ed" +uuid = "a4ae2306-e953-59d6-aa16-d00cac43593b" +version = "3.13.3+0" + +[[deps.libass_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "125eedcb0a4a0bba65b657251ce1d27c8714e9d6" +uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" +version = "0.17.4+0" [[deps.libblastrampoline_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.11.0+0" +version = "5.15.0+0" + +[[deps.libdrm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libpciaccess_jll"] +git-tree-sha1 = "63aac0bcb0b582e11bad965cef4a689905456c03" +uuid = "8e53e030-5e6c-5a89-a30b-be5b7263a166" +version = "2.4.125+1" + +[[deps.libfdk_aac_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "646634dd19587a56ee2f1199563ec056c5f228df" +uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" +version = "2.0.4+0" + +[[deps.libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"] +git-tree-sha1 = "e51150d5ab85cee6fc36726850f0e627ad2e4aba" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.58+0" + +[[deps.libsixel_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "libpng_jll"] +git-tree-sha1 = "c1733e347283df07689d71d61e14be986e49e47a" +uuid = "075b6546-f08a-558a-be8f-8157d0f608a5" +version = "1.10.5+0" + +[[deps.libva_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libX11_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "libdrm_jll"] +git-tree-sha1 = "7dbf96baae3310fe2fa0df0ccbb3c6288d5816c9" +uuid = "9a156e7d-b971-5f62-b2c9-67348b8fb97c" +version = "2.23.0+0" + +[[deps.libvorbis_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll"] +git-tree-sha1 = "11e1772e7f3cc987e9d3de991dd4f6b2602663a5" +uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" +version = "1.3.8+0" + +[[deps.libwebp_jll]] +deps = ["Artifacts", "Giflib_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libglvnd_jll", "Libtiff_jll", "libpng_jll"] +git-tree-sha1 = "4e4282c4d846e11dce56d74fa8040130b7a95cb3" +uuid = "c5f90fcd-3b7e-5836-afba-fc50a0988cb2" +version = "1.6.0+0" + +[[deps.libzip_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "OpenSSL_jll", "XZ_jll", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "86addc139bca85fdf9e7741e10977c45785727b7" +uuid = "337d8026-41b4-5cde-a456-74a10e5b31d1" +version = "1.11.3+0" + +[[deps.mpif_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "TOML"] +git-tree-sha1 = "a8083ee0737c243c8f40a4ba86a0956997facb73" +uuid = "9aeb927a-4695-514f-a259-621a69f20ec0" +version = "0.1.7+0" [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" -version = "1.52.0+1" +version = "1.64.0+1" [[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.4.0+2" +version = "17.7.0+0" + +[[deps.s2n_tls_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "64ae051c6f03044eb7d98027d1b552b4e21e650c" +uuid = "cddc5d3d-934d-5d3a-9747-62fc12ea3f48" +version = "1.7.3+0" + +[[deps.x264_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" +uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" +version = "10164.0.1+0" + +[[deps.x265_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e7b67590c14d487e734dcb925924c5dc43ec85f3" +uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" +version = "4.1.0+0" diff --git a/docs/Project.toml b/docs/Project.toml index 5efff43c0..5c36bb575 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,11 @@ [deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d" +ClimaParams = "5c42b081-d73a-476f-9059-fd94b934656c" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" RRTMGP = "a01a1ee8-cea4-48fc-987c-fc7878d79da1" +Thermodynamics = "b60c26fb-14c3-4610-9d3e-2d17fe7ff00c" diff --git a/docs/bibliography.bib b/docs/bibliography.bib index f68c49773..590e79c92 100644 --- a/docs/bibliography.bib +++ b/docs/bibliography.bib @@ -2,24 +2,30 @@ # Last author name (titlecase), followed by # (no characters in-between) the year. -@article{frierson2007, - title={The dynamics of idealized convection schemes and their effect on the zonally averaged tropical circulation}, - author={Frierson, Dargan MW}, +@techreport{anderson1986, + title={{AFGL} atmospheric constituent profiles (0--120 km)}, + author={Anderson, Gail P and Clough, Shepard A and Kneizys, Francis X and Chetwynd, James H and Shettle, Eric P}, + institution={Air Force Geophysics Laboratory}, + number={AFGL-TR-86-0110}, + year={1986}, + url={https://apps.dtic.mil/sti/citations/ADA175173} +} + +@article{frierson2006, + title={A gray-radiation aquaplanet moist {GCM}. {Part I}: Static stability and eddy scale}, + author={Frierson, D. M. W. and Held, I. M. and Zurita-Gotor, P.}, journal={Journal of the Atmospheric Sciences}, - volume={64}, - number={6}, - pages={1959--1976}, - year={2007}, - publisher={American Meteorological Society}, - doi={10.1175/JAS3935.1} + volume={63}, + pages={2548--2566}, + year={2006}, + doi={10.1175/JAS3753.1} } @article{joseph1976, title={The delta-Eddington approximation for radiative flux transfer}, author={Joseph, J H and Wiscombe, WJ and Weinman, JA}, - journal={Journal of Atmospheric Sciences}, + journal={Journal of the Atmospheric Sciences}, volume={33}, - number={12}, pages={2452--2459}, year={1976}, doi={10.1175/1520-0469(1976)033<2452:TDEAFR>2.0.CO;2} @@ -28,9 +34,8 @@ @article{joseph1976 @article{meador1980, title={Two-stream approximations to radiative transfer in planetary atmospheres: A unified description of existing methods and a new improvement}, author={Meador, WE and Weaver, WR}, - journal={Journal of Atmospheric Sciences}, + journal={Journal of the Atmospheric Sciences}, volume={37}, - number={3}, pages={630--643}, year={1980}, doi={10.1175/1520-0469(1980)037<0630:TSATRT>2.0.CO;2} @@ -41,7 +46,6 @@ @article{ogorman2008 author={O’Gorman, Paul A and Schneider, Tapio}, journal={Journal of Climate}, volume={21}, - number={15}, pages={3815--3832}, year={2008}, doi={10.1175/2007JCLI2065.1} @@ -52,10 +56,8 @@ @article{pincus2019 author={Pincus, Robert and Mlawer, Eli J and Delamere, Jennifer S}, journal={Journal of Advances in Modeling Earth Systems}, volume={11}, - number={10}, pages={3074--3089}, year={2019}, - publisher={Wiley Online Library}, doi={10.1029/2019MS001621} } @@ -64,10 +66,8 @@ @article{schneider2004 author={Schneider, Tapio}, journal={Journal of the Atmospheric Sciences}, volume={61}, - number={12}, pages={1317--1340}, year={2004}, - publisher={American Meteorological Society}, doi={10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2} } @@ -76,7 +76,6 @@ @article{shonk2008 author={Shonk, Jonathan KP and Hogan, Robin J}, journal={Journal of Climate}, volume={21}, - number={11}, pages={2352--2370}, year={2008}, doi={10.1175/2007JCLI1940.1} @@ -87,29 +86,36 @@ @article{toon1989 author={Toon, Owen B and McKay, CP and Ackerman, TP and Santhanam, K}, journal={Journal of Geophysical Research: Atmospheres}, volume={94}, - number={D13}, pages={16287--16301}, year={1989}, - publisher={Wiley Online Library}, doi={10.1029/JD094iD13p16287} } +@article{ukkonen2024, + title={Twelve times faster yet accurate: A new state-of-the-art in radiation schemes via performance and spectral optimization}, + author={Ukkonen, Peter and Hogan, Robin J}, + journal={Journal of Advances in Modeling Earth Systems}, + volume={16}, + pages={e2023MS003932}, + year={2024}, + doi={10.1029/2023MS003932} +} + @article{yang2013, title={Spectrally consistent scattering, absorption, and polarization properties of atmospheric ice crystals at wavelengths from 0.2 to 100 $\mu$ m}, author={Yang, Ping and Bi, Lei and Baum, Bryan A and Liou, Kuo-Nan and Kattawar, George W and Mishchenko, Michael I and Cole, Benjamin}, - journal={Journal of the atmospheric sciences}, + journal={Journal of the Atmospheric Sciences}, volume={70}, - number={1}, pages={330--347}, year={2013}, doi={10.1175/JAS-D-12-039.1} } + @article{clough1992, title={Line-by-line calculations of atmospheric fluxes and cooling rates: Application to water vapor}, author={Clough, Shepard A and Iacono, Michael J and Moncet, Jean-Luc}, journal={Journal of Geophysical Research: Atmospheres}, volume={97}, - number={D14}, pages={15761--15785}, year={1992}, doi={10.1029/92JD01419} @@ -120,20 +126,18 @@ @article{fu1997 author={Fu, Qiang and Liou, KN and Cribb, MC and Charlock, TP and Grossman, A}, journal={Journal of the Atmospheric Sciences}, volume={54}, - number={24}, pages={2799--2812}, year={1997}, doi={10.1175/1520-0469(1997)054<2799:MSPITI>2.0.CO;2} } -@article{hogan2023, +@article{hogan2024, title={What are the optimum discrete angles to use in thermal-infrared radiative transfer calculations?}, author={Hogan, Robin J}, journal={Quarterly Journal of the Royal Meteorological Society}, volume={150}, - number={758}, pages={318--333}, - year={2023}, + year={2024}, doi={10.1002/qj.4598} } @@ -141,7 +145,76 @@ @article{zdunkowski1980 title={An investigation of the structure of typical two-stream methods for the calculation of solar fluxes and heating rates in clouds}, author={Zdunkowski, Wilford G and Welch, Ronald M and Korb, G}, journal={Contributions to Atmospheric Physics}, - volume={53}, pages={147--166}, year={1980} } + +@article{lacis1991, + title={A description of the correlated k distribution method for modeling nongray gaseous absorption, thermal emission, and multiple scattering in vertically inhomogeneous atmospheres}, + author={Lacis, Andrew A and Oinas, Valdar}, + journal={Journal of Geophysical Research: Atmospheres}, + volume={96}, + pages={9027--9063}, + year={1991}, + doi={10.1029/90JD01945} +} + +@article{manabe1964, + title={Thermal equilibrium of the atmosphere with a convective adjustment}, + author={Manabe, Syukuro and Strickler, Robert F}, + journal={Journal of the Atmospheric Sciences}, + volume={21}, + pages={361--385}, + year={1964}, + doi={10.1175/1520-0469(1964)021<0361:TEOTAW>2.0.CO;2} +} + +@article{manabe1967, + title={Thermal equilibrium of the atmosphere with a given distribution of relative humidity}, + author={Manabe, Syukuro and Wetherald, Richard T}, + journal={Journal of the Atmospheric Sciences}, + volume={24}, + pages={241--259}, + year={1967}, + doi={10.1175/1520-0469(1967)024<0241:TEOTAW>2.0.CO;2} +} + +@article{kluft2019, + title={Re-examining the first climate models: Climate sensitivity of a modern radiative-convective equilibrium model}, + author={Kluft, Lukas and Dacie, Sally and Buehler, Stefan A and Schmidt, Hauke and Stevens, Bjorn}, + journal={Journal of Climate}, + volume={32}, + pages={8111--8125}, + year={2019}, + doi={10.1175/JCLI-D-18-0774.1} +} + +@article{kluft2021, + title={Temperature-dependence of the clear-sky feedback in radiative-convective equilibrium}, + author={Kluft, Lukas and Dacie, Sally and Brath, Manfred and Buehler, Stefan A and Stevens, Bjorn}, + journal={Geophysical Research Letters}, + volume={48}, + pages={e2021GL094649}, + year={2021}, + doi={10.1029/2021GL094649} +} + +@article{romps2021, + title={The Rankine-Kirchhoff approximations for moist thermodynamics}, + author={Romps, David M}, + journal={Quarterly Journal of the Royal Meteorological Society}, + volume={147}, + pages={3493--3497}, + year={2021}, + doi={10.1002/qj.4154} +} + +@article{cronin2014, + title={On the choice of average solar zenith angle}, + author={Cronin, Timothy W}, + journal={Journal of the Atmospheric Sciences}, + volume={71}, + pages={2994--3003}, + year={2014}, + doi={10.1175/JAS-D-13-0392.1} +} diff --git a/docs/make.jl b/docs/make.jl index 6b6db3155..5789b57e9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,9 +2,25 @@ using Documenter using LaTeXStrings using RRTMGP using DocumenterCitations +import Literate bib = CitationBibliography(joinpath(@__DIR__, "bibliography.bib")) +# Generate the tutorial pages from the Literate.jl scripts in docs/tutorials. +# The generated markdown contains `@example` blocks, so Documenter executes +# the tutorials during the docs build (they cannot silently rot). +const tutorials_src = joinpath(@__DIR__, "tutorials") +const tutorials_out = joinpath(@__DIR__, "src", "tutorials") +for script in readdir(tutorials_src) + endswith(script, ".jl") || continue + Literate.markdown( + joinpath(tutorials_src, script), + tutorials_out; + documenter = true, + execute = false, + ) +end + makedocs(; plugins = [bib], sitename = "RRTMGP.jl", @@ -24,12 +40,25 @@ makedocs(; modules = [RRTMGP], pages = Any[ "Home" => "index.md", - "Functional core" => "functional_core.md", - "RTE" => "RTE.md", - "Optics" => "Optics.md", - "Example" => "Example.md", - "Fortran and paper concordance" => "concordance.md", - "API" => Any[ + "Tutorials" => Any[ + "A first radiation calculation" => "tutorials/getting_started.md", + "Radiative-convective equilibrium" => "tutorials/manabe_rce.md", + ], + "How-to guides" => Any[ + "Drive RRTMGP from a host model" => "howto/host_model.md", + "Run on GPUs" => "howto/gpu.md", + "Cache the lookup tables" => "howto/lookup_cache.md", + "Get per-band (spectral) fluxes" => "howto/spectral_fluxes.md", + "Run the validated test problems" => "Example.md", + ], + "Explanation" => Any[ + "Functional core" => "functional_core.md", + "RTE" => "RTE.md", + "Optics" => "Optics.md", + "Float32 and Float64" => "precision.md", + "Fortran and paper concordance" => "concordance.md", + ], + "Reference" => Any[ "API" => "api.md" "Getter contract" => "getters.md" "Angular Discretization" => "optics/AngularDiscretizations.md" diff --git a/docs/src/ArtifactPaths.md b/docs/src/ArtifactPaths.md index 1c81f4c2e..5e510f3c3 100644 --- a/docs/src/ArtifactPaths.md +++ b/docs/src/ArtifactPaths.md @@ -1,6 +1,6 @@ # ArtifactPaths -This module provides the file paths for loading the lookup tables and input data for the tests. These artifacts are obtained from "Pincus, R., Mlawer, E. J., Delamere, J., Iacono, M. J., & Pernak, R. (2023). RRTMGP data (Version 1.7) [Data set](https://github.com/earth-system-radiation/rrtmgp-data) +This module provides the file paths for loading the lookup tables and input data for the tests. These artifacts are obtained from Pincus, R., Mlawer, E. J., Delamere, J., Iacono, M. J., & Pernak, R. (2023), [RRTMGP data (Version 1.9)](https://github.com/earth-system-radiation/rrtmgp-data). ```@meta CurrentModule = RRTMGP.ArtifactPaths diff --git a/docs/src/Example.md b/docs/src/Example.md index 7d4f6c94f..bd7a02d3b 100644 --- a/docs/src/Example.md +++ b/docs/src/Example.md @@ -1,33 +1,44 @@ # Example -The quickest way to run RRTMGP is the standalone gray-atmosphere one-liner, which -needs no NetCDF data: +The quickest way to run RRTMGP is the standalone gray-atmosphere one-liner, +which needs no NetCDF data. It builds its column internally, with temperatures +on the analytic semi-gray radiative-equilibrium profile of +[schneider2004](@cite), ``T(p) = T_t\,[1 + d_0\,(p/p_0)^α]^{1/4}``, where +``T_t = 200`` K is the temperature at the top of the atmosphere and the +optical depth ``d_0`` follows from a latitude-dependent radiative-equilibrium +surface temperature (a single column defaults to the equator): -```julia -julia> using RRTMGP +```@example example +using RRTMGP -julia> out = RRTMGP.solve_gray(Float64; nlay = 60, ncol = 1); +out = RRTMGP.solve_gray(Float64; nlay = 60, ncol = 1); -julia> out.net # net flux at each level [W/m²], (nlev, ncol) -julia> out.heating_rate # heating rate at each layer [K/s], (nlay, ncol) +out.net[end, 1] # net flux at the top of the atmosphere [W/m²] ``` -To assemble the pieces yourself — the atmospheric state, the RTE workspaces, and -`solve_lw!`/`solve_sw!` — see [The functional core](@ref), which walks through the -same gray problem step by step and sketches the clear-sky (lookup-table) path. - -Beyond that, the test suite doubles as a set of complete, validated examples. -Each driver below builds its states from reference NetCDF data (downloaded -automatically as artifacts), runs the solvers, and compares against reference -results. Run them from the repository root with the `test` project -(`julia --project=test`). +To assemble the pieces yourself (the atmospheric state, the RTE workspaces, and +`solve_lw!`/`solve_sw!`), see [The functional core](@ref), which walks through +the gray problem and sketches the clear-sky path. + +Beyond that, the test suite doubles as a set of complete, validated examples, +run from the repository root with the `test` project (`julia --project=test`). +The gray driver checks the solvers against analytic solutions. The clear-sky +and all-sky drivers build their states from standardized NetCDF inputs and +compare the computed fluxes against the official results of the Fortran +reference implementation +[rte-rrtmgp](https://github.com/earth-system-radiation/rte-rrtmgp) for the +RFMIP clear-sky and all-sky cases, distributed through the +[rrtmgp-data](https://github.com/earth-system-radiation/rrtmgp-data) +repository and downloaded automatically as artifacts (see +`test/reference_files.jl`). This is the same source of truth the Fortran +implementation validates against, so the tolerances are directly comparable. ## Gray radiation `test/gray_atm.jl` exercises longwave and shortwave gray radiation with both the -non-scattering and two-stream solvers. For longwave-only gray radiation an -analytical radiative-equilibrium solution exists; `gray_atmos_lw_equil` integrates -to equilibrium and compares against it: +non-scattering and two-stream solvers. For longwave-only gray radiation, an +analytical radiative-equilibrium solution exists; `gray_atmos_lw_equil` +integrates to equilibrium and compares against it: ```julia julia> include("test/gray_atm_utils.jl"); @@ -48,15 +59,16 @@ julia> gray_atmos_sw_test(ClimaComms.context(), TwoStreamSWRTE, Float64, 1); Test Passed ``` -Here is the vertical profile of the downward shortwave radiative flux (`flux_dn_dir`): +Here is the vertical profile of the downward shortwave radiative flux +(`flux_dn_dir`): ![](assets/gray_sw_flux_dn.png) ## Gas optics (clear sky) `test/clear_sky.jl` runs RRTMGP for the RFMIP clear-sky atmosphere states and -compares the results to reference data (running the file drives both longwave -solver types and the two-stream shortwave solver at `ncol = 250`): +compares the results to the reference fluxes (running the file drives both +longwave solver types and the two-stream shortwave solver at `ncol = 250`): ```julia julia> include("test/clear_sky.jl") @@ -70,8 +82,9 @@ Here are the vertical profiles of downward longwave (`flux_dn_lw`) and shortwave ## Cloud and aerosol optics (all sky) `test/all_sky_with_aerosols.jl` runs RRTMGP for all-sky atmosphere states with -idealized clouds (uniform condensate and particle size in the troposphere, McICA -cloud sampling) plus MERRA aerosols, and compares the results to reference data: +idealized clouds (uniform condensate and particle size in the troposphere, +McICA cloud sampling) plus MERRA aerosols, and compares the results to the +reference fluxes: ```julia julia> include("test/all_sky_with_aerosols.jl") @@ -81,3 +94,12 @@ Here are the vertical profiles of downward longwave (`flux_dn_lw`) and shortwave (`flux_dn_sw`) fluxes for the first column: ![](assets/all_sky_lw_flux_dn.png) ![](assets/all_sky_sw_flux_dn.png) + +## Related datasets + +The [rte-examples](https://github.com/earth-system-radiation/rte-examples) +repository provides standardized *input* datasets (RFMIP, CKDMIP evaluation +profiles, an idealized RCE profile) without committed reference fluxes; since +its problems overlap the RFMIP cases already covered here, RRTMGP.jl does not +vendor it. Its idealized RCE setup is realized instead as the +[radiative-convective equilibrium tutorial](tutorials/manabe_rce.md). diff --git a/docs/src/Optics.md b/docs/src/Optics.md index be03c47c1..0a9af1a2f 100644 --- a/docs/src/Optics.md +++ b/docs/src/Optics.md @@ -11,44 +11,36 @@ Two options are currently supported for computing the optical depth for longwave ```math \begin{align} -d(\phi, p) = d_0(\phi) \left(\frac{p}{p_0}\right)^\alpha +d(\phi, p) = d_0(\phi) \left(\frac{p}{p_0}\right)^\alpha . \end{align} ``` -Here, ``\alpha`` is ratio of the pressure scale height to the partial-pressure scale height of the infrared absorber (``\alpha=3.5`` for water vapor); ``\phi`` is the latitude; ``p`` is the pressure, and ``p_0 = 1000~\mathrm{hPa}`` is the reference pressure. The optical depth at ``\phi`` and ``p_0``, ``d_0``, is calculated from the latitude-dependent radiative equilibrium temperature ``T_s(\phi)``: +Here, ``\alpha`` is ratio of the pressure scale height to the partial-pressure scale height of the infrared absorber (``\alpha=3.5`` for water vapor); ``\phi`` is the latitude; ``p`` is the pressure, and ``p_0`` is the surface pressure. The optical depth at ``\phi`` and ``p_0``, ``d_0``, is calculated from the latitude-dependent radiative equilibrium temperature ``T_s(\phi)``: ```math \begin{align} -T_s(\phi) = T_e + \Delta T \left(\frac{1}{3} - \sin^2\phi\right) \\ +T_s(\phi) = T_e + \Delta T \left(\frac{1}{3} - \sin^2\phi\right), \\ d_0(\phi) = \left(\frac{T_s(\phi)}{T_t}\right)^4 - 1. \end{align} ``` Here, ``T_e`` is the global-mean surface temperature in radiative equilibrium, ``T_t`` is the temperature at the top of the atmosphere, and ``\Delta T`` is the equator-to-pole temperature difference in radiative equilibrium. The default values are ``T_e = 300~\mathrm{K}``, ``T_t = 200~\mathrm{K}``, and ``\Delta T = 60~\mathrm{K}``. -The optical thickness of an atmosphere layer (the differential optical depth) of pressure thickness ``\Delta p`` is -```math -\begin{align} -\tau(\phi, p) = \alpha d_0(\phi) \left(\frac{p}{p_0}\right)^\alpha \frac{\Delta p}{p}. -\end{align} -``` - - -The optical depth (``d``) for longwave radiation, with the "GrayOpticalThicknessOGorman2008" option, follows [ogorman2008](@cite): +The optical depth (``d``) for longwave radiation, with the "GrayOpticalThicknessOGorman2008" option, follows [frierson2006](@cite) and [ogorman2008](@cite): ```math \begin{align} -d(\phi, p) = \alpha \left[f_l \sigma + (1 - f_l) \sigma^4 \right] \left[ \tau_e + (\tau_p - \tau_e) \sin^2\phi \right] +d(\phi, p) = \alpha \left[f_l \sigma + (1 - f_l) \sigma^4 \right] \left[ \tau_e + (\tau_p - \tau_e) \sin^2\phi \right], \end{align} ``` -where ``f_l = 0.2``, ``\sigma = p / p_0`` is pressure p normalized by -surface pressure ``p_0``, ``\phi`` is latitude, and the longwave optical thicknesses at the equator and at the pole are $\tau_e = 7.2$ and $\tau_p = 1.8$, respectively. $\alpha$ is a scaling factor. +where ``f_l = 0.2``, ``\sigma = p / p_0`` is pressure ``p`` normalized by +surface pressure ``p_0``, ``\phi`` is latitude, and the longwave optical thicknesses at the equator and at the pole are ``\tau_e = 7.2`` and ``\tau_p = 1.8``, respectively. The prefactor ``\alpha`` (default 1) rescales the total longwave optical depth; [ogorman2008](@citet) vary it to simulate a range of climates. This ``\alpha`` is unrelated to the exponent ``\alpha`` of the Schneider option above — each matches the ``α`` field of its own parameter struct. The optical thickness of an atmosphere layer (the differential optical depth) of pressure thickness ``\Delta p`` is ```math \begin{align} -\tau(\phi, p) = (\alpha * \frac{\Delta p}{p}) \left[f_l \sigma + 4 (1 - f_l) \sigma^4 \right] \left[ \tau_e + (\tau_p - \tau_e) \sin^2\phi \right] +\tau(\phi, p) = \alpha \frac{\Delta p}{p} \left[f_l \sigma + 4 (1 - f_l) \sigma^4 \right] \left[ \tau_e + (\tau_p - \tau_e) \sin^2\phi \right]. \end{align} ``` @@ -58,60 +50,170 @@ The source function for longwave radiation is calculated as ``S = \sigma T^4 / \ Two options are currently supported for computing the optical depth for shortwave radiation. The optical depth (``d``) for shortwave radiation, with the "GrayOpticalThicknessSchneider2004" option, is 0. -The optical depth (``d``) for shortwave radiation, with the "GrayOpticalThicknessOGorman2008" option, follows [frierson2007](@cite) and [ogorman2008](@cite): +The optical depth (``d``) for shortwave radiation, with the "GrayOpticalThicknessOGorman2008" option, follows [ogorman2008](@cite): ```math \begin{align} -d(p) = \tau_0 \left(\frac{p}{p_0}\right)^2 +d(p) = \tau_0 \left(\frac{p}{p_0}\right)^2 , \end{align} ``` -where ``p`` is the pressure of the atmosphere layer and ``p_0 = 1000~\mathrm{hPa}`` is the reference pressure. The default value for ``\tau_0`` is 0.22. - -The differential optical thickness of an atmosphere layer of pressure thickness ``\Delta p`` is - -```math -\begin{align} -\tau(p) = 2 \tau_0 \frac{p}{p_0} \frac{\Delta p}{p_0}. -\end{align} -``` +where ``p`` is the pressure of the atmosphere layer and ``p_0`` is the surface pressure. The default value for ``\tau_0`` is 0.22. The single scattering albedo and asymmetry parameter are zero. ## Gas optics -RRTMGP calculates gas optics using a correlated-k method, in which the integration over frequency ``\nu`` of a complex line spectrum is replaced by an integration over a much smoother function of a new variable ``g``. The integral is approximated by the sum over ``G`` quadrature points (g-points). The mapping from ``\nu`` to ``g`` is computed for a set of longwave and shortwave bands. Within each band, the absorption is dominated by no more two gases (major species). The bands are non-overlapping, contiguous, and span the frequencies of radiation emitted by the Earth or Sun. For each band, the total optical thickness (``\tau``) includes contributions from major species (``\tau_\mathrm{major}``), minor species (``\tau_\mathrm{minor}``), and Rayleigh scattering (``\tau_\mathrm{rayleigh}``) for shortwave bands. - -`compute_τ_ssa_lw_src!` calculates the optical properties for each g-point in the longwave and shortwave bands, as well as source functions for the longwave bands. The input atmospheric conditions include pressure (``p``), temperature (``T``), and volume mixing ratios of gases (``\chi``). The relationship between the volume mixing ratio and the mass fraction (``q``) of a gas is ``\chi = q / q_d * M_d / M``, where ``q_d = 1-q_t`` is the dry air mass fraction (total specific humidity ``q_t``), ``M_d`` is the molar mass of the dry air, and `M` is the molar mass of the gas. - -For the major species, the absorption coefficient is computed by linearly interpolating the tabulated values in ``\log(p)``, ``T``, and relative abundances of the two major species (``\eta``) calculated from the volume mixing ratios. The contributions from minor species and Rayleigh scattering are treated with less detail. For each minor species and Rayleigh scattering, a representative pressure is chosen, and the absorption coefficient is computed by linearly interpolating the tabulated values in ``T`` and ``\eta``. For the shortwave bands, the single scattering albedo is calculated as ``\omega_0 = \tau_\mathrm{rayleigh} / \tau``, and the asymmetry parameter is zero. +At any single frequency, gaseous absorption obeys Beer's law, and the transfer +equations of the [RTE page](RTE.md) apply directly. The difficulty is spectral: +across an absorption band, the monochromatic absorption coefficient +``k_\nu`` sweeps over orders of magnitude through thousands of lines, so the +band-averaged transmission over an absorber path ``u``, +```math +\bar{t}(u) = \frac{1}{\Delta\nu} \int_{\Delta\nu} e^{-k_\nu u}\, d\nu, +``` +cannot be represented by any single mean absorption coefficient — the exponential +weights weak and strong lines differently at every path length. The +``k``-distribution method [lacis1991](@cite) replaces the integral over +frequency by an integral over the cumulative distribution of absorption +strength: with ``g(k)`` the fraction of the band where the absorption +coefficient is below ``k``, reordering the spectrum by strength turns the +ragged ``k_\nu`` into a smooth, monotonic ``k(g)`` on ``g \in [0, 1]``, and +```math +\bar{t}(u) = \int_0^1 e^{-k(g)\,u}\, dg \approx \sum_{j=1}^{G} \omega_j\, e^{-k(g_j)\, u}, +``` +a quadrature over a handful of *g-points*. The *correlated*-``k`` assumption is +that the frequency-to-``g`` ordering is the same at all pressures and +temperatures along the path, so a single ``g`` coordinate serves the whole +column, and each g-point behaves like one monochromatic radiative-transfer +problem. RRTMGP's tables carry 16 g-points in each of 16 longwave and 14 +shortwave bands (256 and 224 in total); the bands are non-overlapping, +contiguous, and span the thermal and solar spectra [pincus2019](@cite). The RTE +is solved once per g-point and column, and fluxes simply add over g-points — +the quadrature weights ``\omega_j`` enter through the per-g-point source terms +described below. + +`compute_optical_props!` computes each g-point's optical properties from the +pressure ``p``, temperature ``T``, and gas volume mixing ratios ``\chi`` of the +atmospheric state. (The volume mixing ratio relates to the mass fraction ``q`` +of a gas by ``\chi = q / q_d \cdot M_d / M``, where ``q_d = 1 - q_t`` is the +dry-air mass fraction, ``M_d`` the molar mass of dry air, and ``M`` that of the +gas.) The optical thickness of a layer splits into +```math +\tau_g = \tau_\mathrm{major} + \tau_\mathrm{minor} + \tau_\mathrm{rayleigh}, +``` +with the Rayleigh term present in the shortwave only. -For longwave bands, the Planck fraction (defined as the fraction of the band-integrated Planck energy associated with each g-point) is computed by linearly interpolating the tabulated values in ``\log(p)``, ``T``, and ``\eta``. The band-integrated Planck function is uniquely determined by temperature, and is computed by linearly interpolating the tabulated values in ``T``. The shortwave source function for each g-point is assumed to be constant. +**Major species.** Within each band, absorption is dominated by at most two +gases. Their relative abundance enters through the binary mixing parameter +```math +\eta = \frac{\chi_1}{\chi_1 + r\,\chi_2}, +``` +where ``r`` is the ratio of the two abundances in the reference atmosphere used +to build the tables, so ``\eta`` runs from 0 (only species 2) to 1 (only +species 1). The absorption coefficient is tabulated on a +``(\eta, \log p, T)`` grid and interpolated trilinearly +(`compute_interp_frac_temp`/`_press`/`_η`); ``\tau_\mathrm{major}`` is the +interpolated coefficient times the column amount of the binary mixture. + +**Minor species and Rayleigh scattering.** These contributions are linear in +the respective column amounts, with coefficients tabulated against +``(\eta, T)`` at a representative pressure — minor gases with additional +density and scaling-gas corrections where the spectroscopy requires them +(continua), and Rayleigh scattering in proportion to the column amount of air, +water vapor included. In the shortwave, +the single scattering albedo of the gas optics is +``\omega_0 = \tau_\mathrm{rayleigh} / \tau_g``, and the asymmetry parameter is +zero (Rayleigh scattering is nearly symmetric fore–aft). + +**Source terms.** In the longwave, each g-point emits the fraction +``f_g(\eta, p, T)`` — the *Planck fraction*, interpolated like the absorption +coefficient — of the band-integrated Planck emission ``B_\mathrm{band}(T)``, +which is tabulated against temperature at 1 K resolution; the fractions of a +band sum to one, so summing g-points recovers the band's blackbody emission. In +the shortwave, each g-point carries a fixed fraction of the total solar +irradiance, scaled to the incident flux the host prescribes at the top of the +atmosphere. These per-g-point sources are where the quadrature weights of the +``k``-distribution live, which is why fluxes add over g-points without further +weighting. ### Lookup tables -The lookup tables for the spectral map between ``\nu`` and ``g`` are stored in netcdf files [here](https://caltech.box.com/shared/static/wbtrwp44dyn08g7mozjf4fcyrexwbe6a.gz). Longwave and shortwave lookup tables are stored in `clearsky_lw.nc` and `clearsky_sw.nc`, respectively. `LookUpLW` and `LookUpSW` read the lookup tables. The tabulated information includes the spectral discretization (bands), the major and minor gases considered in each band, and tabulated data of absorption coefficients, Planck fraction, and band-integrated Planck function. The absorption coefficients and Planck fraction are computed at pressures ``1 \mathrm{Pa} ≤ p ≤ 109663 \mathrm{Pa}`` in increments of ``\log(p/1 \mathrm{Pa}) = 0.2``, temperatures ``160 \mathrm{K} ≤ T ≤ 355 \mathrm{K}`` in ``15 \mathrm{K}`` increments, and ``0 ≤ \eta ≤ 1`` in ``1/8`` increments. The band-integrated Plack function is computed at temperatures ``160 \mathrm{K} ≤ T ≤ 355 \mathrm{K}`` in ``1 \mathrm{K}`` increments. A more detailed description of the spectral structure of the lookup tables can be found in `Appendix A` in [pincus2019](@cite). +The lookup tables for the spectral map between ``\nu`` and ``g`` are NetCDF files from the +[rrtmgp-data](https://github.com/earth-system-radiation/rrtmgp-data) repository (v1.7), +downloaded automatically as Julia artifacts. The longwave and shortwave lookup tables are +`rrtmgp-gas-lw-g256.nc` and `rrtmgp-gas-sw-g224.nc`, respectively (see +`RRTMGP.ArtifactPaths`). `LookUpLW` and `LookUpSW` read the lookup tables. The tabulated information includes the spectral discretization (bands), the major and minor gases considered in each band, and tabulated data of absorption coefficients, Planck fraction, and band-integrated Planck function. The absorption coefficients and Planck fraction are computed at pressures ``1\,\mathrm{Pa} \le p \le 109663\,\mathrm{Pa}`` in increments of ``\log(p/1\,\mathrm{Pa}) = 0.2``, temperatures ``160\,\mathrm{K} \le T \le 355\,\mathrm{K}`` in ``15\,\mathrm{K}`` increments, and ``0 \le \eta \le 1`` in ``1/8`` increments. The band-integrated Planck function is computed at temperatures ``160\,\mathrm{K} \le T \le 355\,\mathrm{K}`` in ``1\,\mathrm{K}`` increments. A more detailed description of the spectral structure of the lookup tables can be found in `Appendix A` in [pincus2019](@cite). !!! note - Absorption by both major and minor species is treated separately in the upper and lower atmosphere, defined as pressures above and below the pressure defined in `press_ref_trop` in the lookup tables (~9948 Pa). This corresponds to element 13 in array `press_ref`. There is a single large table of absorption coefficients. Table elements 1:13 in the pressure dimension correspond to pressures >= `press_ref_trop`, while table elements 14:60 correspond to pressures <= `press_ref_trop`. Elements 13 and 14 refer to the same pressure but for the lower and upper atmosphere, respectively. This is why the the absorption coefficients lookup table has a pressure dimension of 60, but there are only 59 reference pressure levels in `press_ref`. + Absorption by both major and minor species is treated separately in the upper and lower atmosphere, defined as pressures above and below the pressure defined in `press_ref_trop` in the lookup tables (~9948 Pa). This corresponds to element 13 in array `press_ref`. There is a single large table of absorption coefficients. Table elements 1:13 in the pressure dimension correspond to pressures >= `press_ref_trop`, while table elements 14:60 correspond to pressures <= `press_ref_trop`. Elements 13 and 14 refer to the same pressure but for the lower and upper atmosphere, respectively. This is why the absorption coefficients lookup table has a pressure dimension of 60, but there are only 59 reference pressure levels in `press_ref`. ## Cloud optics -`compute_cld_props` computes the optical properties of clouds, which is a combination of contributions from liquid and ice particles. The optical thickness (``\tau``), single scattering albedo (``\omega_0``), and asymmetry parameter (``g``) for clouds are: +`compute_lookup_cld_liq_props` and `compute_lookup_cld_ice_props` compute the optical properties of liquid and ice cloud particles, whose combination gives the cloud optical properties. The optical thickness (``\tau``), single scattering albedo (``\omega_0``), and asymmetry parameter (``g``) for clouds are: ```math \begin{align} -\tau_c &= \tau_l + \tau_i \\ -\omega_{0c} &= \frac{\tau_l \omega_{0l} + \tau_i \omega_{0i}}{\tau_l + \tau_i} \\ -g_c &= \frac{\tau_l \omega_{0l} g_l + \tau_i \omega_{0i} g_i}{\tau_l \omega_{0l} + \tau_i \omega_{0i}} \\ +\tau_c &= \tau_l + \tau_i, \\ +\omega_{0c} &= \frac{\tau_l \omega_{0l} + \tau_i \omega_{0i}}{\tau_l + \tau_i}, \\ +g_c &= \frac{\tau_l \omega_{0l} g_l + \tau_i \omega_{0i} g_i}{\tau_l \omega_{0l} + \tau_i \omega_{0i}}, \end{align} ``` -where subscripts c, l, and i indicate cloud, liquid, and ice, respectively. The liquid cloud and ice cloud optical thicknesses are calculated by multiplying the cloud liquid and ice extinction coefficients (``\mu``) by the cloud liquid (lwp) and ice water paths (iwp), respectively. That is, ``\tau_l = \mu_l \mathrm{lwp}`` and ``\tau_i = \mu_i \mathrm{iwp}``. The extinction coefficient, single scattering albedo, and asymmetry parameter are computed by linearly interpolating the tabulated values in the liquid and ice particle effective radius. For ice partices, the optical properties also depend on the surface roughness, following [yang2013](@cite). The optical properties for shortwave bands are delta-scaled to reduce the biases in calculating radiative fluxes for highly asymmetric phase functions ([joseph1976](@cite)). +where subscripts c, l, and i indicate cloud, liquid, and ice, respectively. The liquid cloud and ice cloud optical thicknesses are calculated by multiplying the cloud liquid and ice extinction coefficients (``\mu``) by the cloud liquid (lwp) and ice water paths (iwp), respectively. That is, ``\tau_l = \mu_l \mathrm{lwp}`` and ``\tau_i = \mu_i \mathrm{iwp}``. The extinction coefficient, single scattering albedo, and asymmetry parameter are computed by linearly interpolating the tabulated values in the liquid and ice particle effective radius. For ice particles, the optical properties also depend on the surface roughness, following [yang2013](@cite). The optical properties for shortwave bands are delta-scaled to reduce the biases in calculating radiative fluxes for highly asymmetric phase functions ([joseph1976](@cite)). -`add_cloud_optics_2stream` adds cloud optics to the gas optics for each g-point that sees cloud (the cloud mask is described below). +`add_cloud_optics_2stream!` adds cloud optics to the gas optics for each g-point that sees cloud (the cloud mask is described below). ### Lookup tables -The lookup tables for cloud optics are stored in netcdf files [here](https://caltech.box.com/shared/static/wbtrwp44dyn08g7mozjf4fcyrexwbe6a.gz). Longwave and shortwave lookup tables are stored in `cloudysky_lw.nc` and `cloudysky_sw.nc`, respectively. `LookUpCld` reads the lookup tables. The tabulated information includes tabulated data of extinction coefficients, single scattering albedo, and asymmetry parameter for liquid and ice particles. The optical properties for liquid particles are computed at radius ``2.5 \mathrm{\mu m} ≤ r_l ≤ 21.5 \mathrm{\mu m}`` in increments of ``1 \mathrm{\mu m}``. The optical properties for ice particles are computed at radius ``10 \mathrm{\mu m} ≤ r_i ≤ 180 \mathrm{\mu m}`` in increments of ``10 \mathrm{\mu m}``. +The lookup tables for cloud optics are NetCDF files from the same +[rrtmgp-data](https://github.com/earth-system-radiation/rrtmgp-data) artifacts. The +longwave and shortwave lookup tables are `rrtmgp-clouds-lw-bnd.nc` and +`rrtmgp-clouds-sw-bnd.nc`, respectively. `LookUpCld` reads the lookup tables. The tabulated information includes tabulated data of extinction coefficients, single scattering albedo, and asymmetry parameter for liquid and ice particles. The optical properties for liquid particles are computed at radius ``2.5\,\mathrm{\mu m} \le r_l \le 21.5\,\mathrm{\mu m}`` in increments of ``1\,\mathrm{\mu m}``. The optical properties for ice particles are computed at radius ``10\,\mathrm{\mu m} \le r_i \le 180\,\mathrm{\mu m}`` in increments of ``10\,\mathrm{\mu m}``. ### Cloud overlap method -Climate models predict grid-mean cloud fraction and cloud condensates, and the vertical structure of clouds needs to be prescribed using some overlap assumptions for calculating radiative fluxes. The Monte Carlo independent column approximation (McICA) allows us to have fractional cloud areas, by randomly assigning some wavelengths to see cloud and other wavelengths to see no cloud. `build_cloud_mask!` builds McICA-sampled cloud masks from cloud fraction. We use the maximum-random overlap method, which maximizes the cloud overlap between adjacent layers but randomly distributes clouds at different altitudes separated by clear sky. The random numbers are generated for each layer and each g-point. For a certain layer (layer 1), the g-points that have been assigned a random number larger than one minus the cloud fraction see cloud, and the other g-points see clear-sky. The random numbers are recalculated for the layer below (layer 2). If the g-point sees cloud in layer 1, its random number in layer 2 is changed to the one in layer 1. If the g-point sees clear-sky in layer 1, its random number in layer 2 is multiplied by one minus the cloud fraction of layer 1. This ensures that the random numbers in layer 2 are randomly distributed in the range [0, 1]. \ No newline at end of file +Climate models predict grid-mean cloud fraction and cloud condensates, and the vertical structure of clouds needs to be prescribed using some overlap assumptions for calculating radiative fluxes. The Monte Carlo independent column approximation (McICA) allows us to have fractional cloud areas, by randomly assigning some wavelengths to see cloud and other wavelengths to see no cloud. `build_cloud_mask!` builds McICA-sampled cloud masks from cloud fraction. We use the maximum-random overlap method, which maximizes the cloud overlap between adjacent layers but randomly distributes clouds at different altitudes separated by clear sky. The random numbers are generated for each layer and each g-point. For a certain layer (layer 1), the g-points that have been assigned a random number larger than one minus the cloud fraction see cloud, and the other g-points see clear-sky. The random numbers are recalculated for the layer below (layer 2). If the g-point sees cloud in layer 1, its random number in layer 2 is changed to the one in layer 1. If the g-point sees clear-sky in layer 1, its random number in layer 2 is multiplied by one minus the cloud fraction of layer 1. This ensures that the random numbers in layer 2 are randomly distributed in the range [0, 1]. + +Formally, proceeding downward for each g-point, layer ``k`` sees cloud where its random number ``R_k`` exceeds one minus the layer's cloud fraction, ``R_k > 1 - \mathrm{CF}_k``, with +```math +R_k = +\begin{cases} +R_{k-1} & \text{if the g-point sees cloud in layer } k-1,\\ +u_k \, (1 - \mathrm{CF}_{k-1}) & \text{otherwise}, +\end{cases} +``` +where ``u_k \sim U(0, 1)`` is an independent uniform draw. Three properties follow. Each layer is cloudy with probability ``\mathrm{CF}_k``, so the sampled mask reproduces the prescribed cloud fractions in expectation. Vertically contiguous cloudy layers overlap maximally: a g-point that is cloudy in one layer stays cloudy in the next wherever the fractions allow, so a contiguous cloudy block has total cloud cover ``\max_k \mathrm{CF}_k``. Cloudy blocks separated by clear air are uncorrelated. Because every g-point carries an independent subcolumn, the broadband flux averages over the samples: the McICA estimate is unbiased, and its sampling noise largely cancels in the spectral integration. The all-sky methods' `reset_rng_seed` option reseeds the generator on each [`update_fluxes!`](@ref RRTMGP.update_fluxes!) call to make the sampling reproducible (see the reproducibility caveat in [How to run on GPUs](howto/gpu.md)). + +## Aerosol optics + +`add_aerosol_optics_1scalar!` and `add_aerosol_optics_2stream!` add aerosol +extinction to the gas (and cloud) optical properties, using the MERRA aerosol +tables shipped with the rrtmgp-data artifacts +(`rrtmgp-aerosols-merra-lw.nc`/`-sw.nc`, read by `LookUpAerosolMerra`). For +each species and band, the tables give the optical properties per unit column +mass of aerosol; the host supplies the per-species column mass densities and +particle sizes (the [`aerosol_column_mass_density`](@ref RRTMGP.aerosol_column_mass_density) +and [`aerosol_radius`](@ref RRTMGP.aerosol_radius) getters), and layers +without aerosol mass are skipped through a precomputed mask. + +The MERRA set comprises 15 species: dust and sea salt in five size bins each, +sulfate, and black and organic carbon each in a hydrophilic and a hydrophobic +variant. Dust and sea salt select their size bin from the supplied particle +radius. The hygroscopic species — sea salt, sulfate, and the hydrophilic +carbon variants — are additionally interpolated in relative humidity, which +enters from the atmospheric state; keeping `layer_relative_humidity` current +is a host responsibility (see +[Driving RRTMGP from a host model](howto/host_model.md)). Dust and the +hydrophobic carbon variants are humidity-independent. Internally, the species +occupy fixed positions in the aerosol state arrays: hosts should map their +species through [`aerosol_index_map`](@ref RRTMGP.aerosol_index_map) (or +[`aerosol_names`](@ref RRTMGP.aerosol_names)) rather than assume an ordering, +and the named getters above handle this lookup. + +What each solver family receives mirrors the treatment of gas optics. The +non-scattering longwave path adds only the aerosol absorption optical depth +``\tau (1 - \omega_0)``, while the two-stream path folds the full +``(\tau, \omega_0, g)`` of each species into the running optical properties +by the same ``\tau``-weighted mixing rules as for clouds, delta-scaled in the +shortwave. In the shortwave band containing 550 nm, the summed extinction and +scattering aerosol optical depths are retained as diagnostics +(`aod_sw_extinction`, `aod_sw_scattering`), giving the standard AOD at +negligible extra cost. \ No newline at end of file diff --git a/docs/src/RTE.md b/docs/src/RTE.md index d10541f92..f1d298ab3 100644 --- a/docs/src/RTE.md +++ b/docs/src/RTE.md @@ -1,74 +1,173 @@ -# Radiatve Transfer Equations - -## Radiation heating rate - -Radiative fluxes enter the thermal energy equation as follows: - +# Radiative transfer + +The RTE solvers integrate the radiative transfer equation through a +plane-parallel, horizontally homogeneous column and return the upward and +downward radiative fluxes and the heating rate. This page states the equations +they solve and how they solve them; the optical properties that enter as +coefficients are described under [Optics](@ref). + +## The radiative transfer equation + +Radiation is described by the spectral radiance ``I_\lambda``, the power per +unit area, solid angle, and wavelength flowing in a given direction ``\Omega``. +Along a ray, the medium changes the radiance by three processes: extinction +(absorption plus scattering out of the beam), thermal emission, and scattering +into the beam from all other directions. In terms of the extinction optical +depth ``\tau_\lambda`` measured along the ray, the radiative transfer equation +reads ```math -\begin{align} -\partial_t (\rho e) = - \nabla \cdot F_{net} -\end{align} +\frac{dI_\lambda(\Omega)}{d\tau_\lambda} = +(1 - \omega_{0,\lambda})\, B_\lambda(T) - I_\lambda(\Omega) ++ \omega_{0,\lambda} \int_{4\pi} P_\lambda(\Omega', \Omega)\, +I_\lambda(\Omega')\, d\Omega', ``` - -where ``F_{net} = \sum_j (F^{+}_j - F^{-}_j)``, and ``F^{+}_j`` and ``F^{-}_j`` denote the sum of shortwave and longwave upward and downward fluxes for spectral interval j, respectively. - -## RTE solver - -`solve_lw!` and `solve_sw!` solve the radiative transfor equation for longwave and shortwave radiation. - -!!! note - We have both no scattering and two stream solvers in the code. Only two stream solver is documented here. - -## Computing fluxes through a vertically layered atmosphere - -`rte_lw_2stream!` and `rte_sw_2stream!` compute longwave and shortwave radiative fluxes through a vertically layered atmosphere. The equations for upward and downward fluxes are calculated following [shonk2008](@cite): - +where ``B_\lambda(T)`` is the Planck function at the local temperature, +``\omega_{0,\lambda}`` is the single-scattering albedo — the fraction of the +extinction due to scattering — and ``P_\lambda(\Omega', \Omega)`` is the phase +function, the probability of scattering from direction ``\Omega'`` into +``\Omega``. The emission term ``B_\lambda`` follows from Kirchhoff's law under +local thermodynamic equilibrium (absorptivity = emissivity at every +wavelength), which is assumed to hold throughout the atmosphere RRTMGP models. The scattering integral couples all directions, which is what makes +the general equation expensive; in the atmosphere, it matters for clouds and +aerosols in both the longwave and shortwave and for Rayleigh scattering in the +shortwave. The two-stream approximation below reduces the angular coupling to +two hemispheric fluxes. + +For longwave transfer in clear skies, scattering is negligible +(``\omega_{0,\lambda} \to 0``), and the equation reduces to the *Schwarzschild +equation*, ```math -\begin{align} -F^{+}_i = \alpha_i F^{-}_i + G_i \\ -F^{-}_i = \beta_i (T_i F^{-}_{i+1} + R_i G_i + S^{-}_i) + F^{-}_{i, dir} \\ -\alpha_{i+1} = R_i + T_i^2 \beta_i \alpha_i \\ -G_{i+1} = S^{+}_i + T_i \beta_i (G_i + \alpha_i S^{-}_i) \\ -\end{align} +\frac{dI_\lambda}{d\tau_\lambda} = B_\lambda(T) - I_\lambda, ``` +with ``\tau_\lambda`` now the absorption optical depth. -where ``\beta_k = 1 / (1 - \alpha_k R_k)``. ``T`` and ``R`` are transmittance and reflectance, and ``S^{+}`` and ``S^{-}`` represent upward and downward source functions. ``F^-_{dir}`` is the downward direct shortwave flux. The subscript ``i`` represents the level. - -## Computing transmittance, reflectance, and total source functions - -### Longwave - -`lw_2stream_coeffs` calculates transmittance (``T``), reflectance (``R``), and source functions (``S^+`` and ``S^-``) from optical properties for longwave radiation. Transmittance and reflectance are calculated following [meador1980](@cite): +## Two-stream approximation +Resolving the full angular dependence of ``I_\lambda`` is too costly for a +climate model, so the flux calculation collapses the radiation field into an +upward flux ``F_\lambda^\uparrow`` and a downward flux ``F_\lambda^\downarrow``. +In the plane-parallel column, the vertical optical depth ``\widehat{\tau}_\lambda`` +increases downward from zero at the top of the atmosphere. Neglecting +scattering, the two streams obey ```math -\begin{align} -T = \frac{2 k e^{-k \tau}}{k + \gamma_1 + (k - \gamma_1) e^{-2 k \tau}} \\ -R = \frac{\gamma_2 (1 - e^{-2 k \tau})}{k + \gamma_1 + (k - \gamma_1) e^{-2 k \tau}} -\end{align} +\frac{dF_\lambda^\uparrow}{d\widehat{\tau}_\lambda} = D\,(F_\lambda^\uparrow - \pi B_\lambda), +\qquad +\frac{dF_\lambda^\downarrow}{d\widehat{\tau}_\lambda} = D\,(\pi B_\lambda - F_\lambda^\downarrow), ``` - -where ``\tau`` is the optical depth, and ``k = \sqrt{\gamma_1^2 - \gamma_2^2}``. ``\gamma_1`` and ``\gamma_2`` are coupling coefficients in the two-stream approximation that describe how isotropic the phase function is, and are determined by the optical properties (single scattering albedo and asymmetry parameter). - -``S^+`` and ``S^-`` are calculated from optical properties and Planck source functions following [toon1989](@cite). - -### Shortwave -`sw_2stream_coeffs` calculates diffuse transmittance (``T``), diffuse reflectance (``R``), direct transmittance (``T_{dir}``), and direct reflectance (``R_{dir}``) from optical properties for shortwave radiation. ``T`` and ``R`` are calculated following the same equations as for the longwave radiation. ``T_{dir}`` and ``R_{dir}`` is calculated following [meador1980](@cite): - +where the *diffusivity factor* ``D`` accounts for the slantwise paths of a +diffuse field: replacing the range of zenith angles by a single effective secant +``D`` closes the hemispheric integral that relates flux to radiance. With +scattering, the streams couple through a layer reflectance, and RRTMGP forms the +two-stream transmittance and reflectance from the optical thickness, single +scattering albedo, and asymmetry parameter following [meador1980](@citet). + +## Angular discretization + +The longwave no-scattering solver does better than a single diffusivity angle: +it integrates the Schwarzschild equation along a small set of discrete zenith +angles and sums the results with Gauss quadrature weights, so the hemispheric +flux is ``F_\lambda = \sum_i w_i\, I_\lambda(\mu_i)``. RRTMGP uses the +Gauss-Jacobi-5 nodes of [hogan2024](@citet) with one to four angles +([`AngularDiscretization`](@ref RRTMGP.AngularDiscretizations.AngularDiscretization)). +The default single angle has secant ``D \approx 1.64``, close to Elsasser's +classic diffusivity factor of ``1.66``, which the two-stream longwave solver +adopts following [fu1997](@citet); adding angles improves the accuracy of the +angular integral. + +## Radiative heating rate + +The fluxes feed back on temperature through the divergence of the net upward +flux ``F^{\mathrm{net}} = F^\uparrow - F^\downarrow``. A layer warms where the +net flux converges and cools where it diverges: ```math -\begin{align} -T_{dir} = - A [(1 + k \mu) (\alpha_1 + k \gamma_4) e^{-\tau/\mu} - (1 - k \mu) (\alpha_1 - k \gamma_4) e^{-2k\tau} e^{-\tau/\mu} - 2k (\gamma_4 + \alpha_1 \mu) e^{-\tau/\mu}] \\ -R_{dir} = A [(1 - k \mu) (\alpha_2 + k \gamma_3) - (1 + k \mu) (\alpha_2 - k \gamma_3) e^{-2 k \tau} - 2 (k \gamma_3 - \alpha_2 k \mu) e^{-k \tau} e^{-\tau/\mu}] -\end{align} +\rho\, c_p \frac{\partial T}{\partial t} = -\frac{dF^{\mathrm{net}}}{dz}. ``` - -where ``A = \omega_0 / (1 - k^2 \mu^2) / [k (1 + e^{-2 k \tau}) + \gamma_1 (1 - e^{-2 k \tau})]``. ``\omega_0`` is the single scattering albedo. ``\mu`` is the cosine of solar zenith angle. ``\gamma_3``, and ``\gamma_4`` are coefficients in the two-stream approximation and determined by the optical properties. They are constrained to ``\gamma_3 + \gamma_4 = 1`` by energy conservation. - -The direct downward flux (``F^{-}_{dir}``) and source functions (``S^+`` and ``S^-``) for each level are calculated from ``T_{dir}`` and ``R_{dir}``: - +Because the temperature tendency scales as ``1/\rho``, a given flux divergence +warms thin, high-altitude air far more than dense air near the surface — one +reason ozone's shortwave absorption heats the stratosphere so effectively. +[`heating_rate`](@ref RRTMGP.heating_rate) returns ``\partial T/\partial t`` in +K/s. + +## How the equations are solved + +The continuous equations are solved by discretizing the column into layers of +uniform optical properties, within which they admit exact solutions. The +resulting sweeps run independently for every spectral quadrature point and +column, which is what makes the solvers parallelize well. + +The no-scattering longwave solver integrates the Schwarzschild equation along +each quadrature angle exactly: crossing a layer of vertical optical depth +``\widehat{\tau}`` multiplies the radiance by the slant-path transmittance +``t = e^{-D\widehat{\tau}}`` and adds the layer's own emission, +```math +I_{\mathrm{out}} = t\, I_{\mathrm{in}} + S, +``` +where the source ``S`` follows from a Planck function that varies linearly in +optical depth across the layer ([clough1992](@citet)). One downward and one +upward sweep of this update per quadrature angle yields the fluxes. + +In the two-stream solver, the coupled equations for ``F^\uparrow`` and +``F^\downarrow`` within a layer have exponential solutions ``e^{\pm k\tau}``, +which reduce each layer ``i`` to a diffuse reflectance ``R_i``, a transmittance +``T_i``, and upward and downward source terms ``S_i^{\pm}`` +([meador1980](@citet); [toon1989](@citet) for the thermal source). The layers +are then coupled by the adding method of [shonk2008](@citet). With ``\alpha_i`` +the albedo of the atmosphere–surface system below level ``i`` and ``G_i`` its +upward emission, a first pass climbs from the surface, +```math +\begin{aligned} +\alpha_{i+1} &= R_i + T_i^2 \beta_i \alpha_i,\\ +G_{i+1} &= S_i^{+} + T_i \beta_i \left(G_i + \alpha_i S_i^{-}\right), +\end{aligned} +``` +where ``\beta_i = (1 - \alpha_i R_i)^{-1}`` sums the infinite series of +reflections between layer ``i`` and the medium below it. A second pass descends +from the top-of-atmosphere boundary condition, computing the downwelling flux ```math -\begin{align} -F^-_{dir, i} = e^{-\tau/\mu} F^-_{dir, i+1} \\ -S^+_i = R_{dir, i} F^-_{dir, i+1} \\ -S^-_i = T_{dir, i} F^-_{dir, i+1} -\end{align} +F_i^{\downarrow} = \beta_i \left(T_i F_{i+1}^{\downarrow} + R_i G_i + +S_i^{-}\right) + F_{i,\mathrm{dir}}^{\downarrow}, ``` +where the last term is the direct solar beam, attenuated by Beer's law, and +from it the upwelling flux ``F_i^{\uparrow} = \alpha_i F_i^{\downarrow} + +G_i``. The result is the discrete counterpart of the integral solution of the +transfer equation: the flux at each level accumulates every layer's emission, +transmitted and reflected through the layers between the emitting layer and +that level. + +## No-scattering and two-stream solvers + +RRTMGP provides two solver families, distinguished by whether they represent +scattering: + +- The **no-scattering solver** carries the optical thickness ``\tau`` alone + ([`OneScalar`](@ref RRTMGP.Optics.OneScalar) optics). It integrates absorption + and emission over the Gauss angles above and is used for clear-sky longwave + transfer, where scattering is negligible; in the shortwave, it reduces to + Beer's-law extinction of the direct solar beam. +- The **two-stream solver** carries the optical thickness, single scattering + albedo, and asymmetry parameter ([`TwoStream`](@ref RRTMGP.Optics.TwoStream) + optics) and represents multiple scattering. It is required wherever scattering + is not negligible: clouds and aerosols in both bands, and Rayleigh scattering + in the shortwave. + +Both families exist for the longwave and the shortwave +([`NoScatLWRTE`](@ref RRTMGP.RTE.NoScatLWRTE) / [`TwoStreamLWRTE`](@ref RRTMGP.RTE.TwoStreamLWRTE) +and [`NoScatSWRTE`](@ref RRTMGP.RTE.NoScatSWRTE) / [`TwoStreamSWRTE`](@ref RRTMGP.RTE.TwoStreamSWRTE)), +and [`solve_lw!`](@ref RRTMGP.RTESolver.solve_lw!) / [`solve_sw!`](@ref RRTMGP.RTESolver.solve_sw!) +dispatch on the workspace type. + +## Boundary conditions + +The sweep needs conditions at both ends of the column. At the surface, the +longwave upwelling radiance is the surface Planck emission scaled by the surface +emissivity, and the shortwave reflection is set by the direct and diffuse +albedos; the incident solar flux and the cosine of the solar zenith angle enter +at the top. Any prescribed incident diffuse flux at the top defaults to zero, as does the downwelling longwave flux at the top. + +RRTMGP can also insert an **isothermal boundary layer**: an extra layer between +the host model's top and the minimum pressure of the gas-optics tables, held at +the model-top temperature, that represents the radiative effect of the +atmosphere above the model lid. It is enabled through +[`RRTMGPGridParams`](@ref RRTMGP.RRTMGPGridParams), and every getter masks it +off so the host reads and writes arrays sized to its own grid (see +[The getter contract](@ref)). diff --git a/docs/src/api.md b/docs/src/api.md index dc82c7dd9..049482157 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -2,8 +2,8 @@ RRTMGP has an API for creating various types of solvers, and accessing data passed to it. Hosts read and write a solver's data through named getters; their -uniform layout/domain-masking/writability contract — the RRTMGP–ClimaCore -decoupling mechanism — is spelled out under [The getter contract](@ref). +uniform layout/domain-masking/writability contract (the RRTMGP–ClimaCore +decoupling mechanism) is spelled out under [The getter contract](@ref). ```@meta CurrentModule = RRTMGP diff --git a/docs/src/assets/logo.png b/docs/src/assets/logo.png deleted file mode 100644 index 7a1c546bd..000000000 Binary files a/docs/src/assets/logo.png and /dev/null differ diff --git a/docs/src/assets/logo.svg b/docs/src/assets/logo.svg new file mode 100644 index 000000000..29c3e2fe1 --- /dev/null +++ b/docs/src/assets/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/concordance.md b/docs/src/concordance.md index 4d8488f45..e4cb2f2c0 100644 --- a/docs/src/concordance.md +++ b/docs/src/concordance.md @@ -2,8 +2,8 @@ RRTMGP.jl is a Julia implementation of the algorithms in the reference Fortran package [rte-rrtmgp](https://github.com/earth-system-radiation/rte-rrtmgp) -[pincus2019](@cite). If you know the Fortran code — or the papers it -implements — the tables below map its `mo_*` modules, `ty_*` derived types, and +[pincus2019](@cite). If you know the Fortran code (or the papers it +implements), the tables below map its `mo_*` modules, `ty_*` derived types, and kernel subroutines to their RRTMGP.jl counterparts, with the papers whose equations each kernel implements. @@ -20,7 +20,7 @@ and ``μ₀`` (cosine of the solar zenith angle). | RRTMGP.jl | Fortran rte-rrtmgp | Papers / symbols | |:----------|:-------------------|:-----------------| | `AtmosphericState` (layer/level ``p``, ``T``; `vmr`; `col_dry`) | the `gas_optics()` arguments `play`, `plev`, `tlay`, `tlev`, `col_dry` | — | -| `GrayAtmosphericState` | — (no gray path in the Fortran code) | [schneider2004](@cite), [ogorman2008](@cite) | +| `GrayAtmosphericState` | (no gray path in the Fortran code) | [schneider2004](@cite), [frierson2006](@cite), [ogorman2008](@cite) | | `VolumeMixingRatios.Vmr` / `VmrGM` | `mo_gas_concentrations`: `ty_gas_concs` (`set_vmr!`) | — | | `Optics.OneScalar` (``τ`` only) | `mo_optical_props`: `ty_optical_props_1scl` | — | | `Optics.TwoStream` (``τ``, ``ω₀``, ``g``) | `mo_optical_props`: `ty_optical_props_2str` | — | @@ -30,7 +30,7 @@ and ``μ₀`` (cosine of the solar zenith angle). | `Fluxes.FluxBand` (`spectral_fluxes = true`) | `extensions/mo_fluxes_byband`: `ty_fluxes_byband` | — | | `LwBCs` (`sfc_emis`, `inc_flux`) | the `rte_lw` arguments `sfc_emis`, `inc_flux` | ``ε_{\mathrm{sfc}}`` | | `SwBCs` (`cos_zenith`, `toa_flux`, `sfc_alb_direct`, `inc_flux_diffuse`, `sfc_alb_diffuse`) | the `rte_sw` arguments `mu0`, `inc_flux`, `sfc_alb_dir`, `sfc_alb_dif`, `inc_flux_dif` | ``μ₀``, ``α`` | -| `AngularDiscretization` (`gauss_Ds`, `gauss_wts`) | the `gauss_Ds`/`gauss_wts` tables in `mo_rte_solver_kernels` (`n_gauss_angles`) | Gauss–Jacobi-5 quadrature, [hogan2023](@cite) Table 1 | +| `AngularDiscretization` (`gauss_Ds`, `gauss_wts`) | the `gauss_Ds`/`gauss_wts` tables in `mo_rte_solver_kernels` (`n_gauss_angles`) | Gauss–Jacobi-5 quadrature, [hogan2024](@cite) Table 1 | | `LookupBundle`, `lookup_tables`, `LookUpLW`/`LookUpSW` | `mo_load_coefficients` loading a k-distribution into `mo_gas_optics_rrtmgp`: `ty_gas_optics_rrtmgp` | correlated-``k`` distribution, [pincus2019](@cite) | | `LookUpCld` | `mo_load_cloud_coefficients` → `mo_cloud_optics_rrtmgp`: `ty_cloud_optics_rrtmgp` (LUT path) | — | | `LookUpAerosolMerra` | `extensions/mo_aerosol_optics_rrtmgp_merra`: `ty_aerosol_optics_rrtmgp_merra` | MERRA-2 aerosol types | @@ -52,14 +52,14 @@ RRTMGP.jl equivalents live in `src/rte/`, split by band and solver | `lw_noscat_source_up` / `lw_noscat_source_dn` | `lw_source_noscat` | linear-in-``τ`` Planck source, [clough1992](@cite) Eq. 13 | | `rte_lw_noscat_one_angle!` (transport sweep) | `lw_transport_noscat` | ``T = e^{-Dτ}`` at secant ``D`` | | `rte_lw_2stream_solve!` → `lw_2stream_gpt_col!` | `lw_solver_2stream` | — | -| `lw_2stream_coeffs` | `lw_two_stream` | ``γ₁``, ``γ₂``, ``k = \sqrt{(γ₁-γ₂)(γ₁+γ₂)}``, ``R_{\mathrm{dif}}``/``T_{\mathrm{dif}}`` — [meador1980](@cite) with the diffusivity ``D = 1.66`` of [fu1997](@cite) | +| `lw_2stream_coeffs` | `lw_two_stream` | ``γ₁``, ``γ₂``, ``k = \sqrt{(γ₁-γ₂)(γ₁+γ₂)}``, ``R_{\mathrm{dif}}``/``T_{\mathrm{dif}}`` [meador1980](@cite) with the diffusivity ``D = 1.66`` of [fu1997](@cite) | | longwave source part of `rte_lw_2stream!` | `lw_source_2str` | linear-in-``τ`` source for scattering atmospheres, [toon1989](@cite) | | `rte_sw_noscat_solve!` → `sw_noscat_gpt_col!` (`rte_sw_noscat!`) | `sw_solver_noscat` | Beer–Lambert direct beam, ``e^{-τ/μ₀}`` | | `rte_sw_2stream_solve!` → `sw_2stream_gpt_col!` | `sw_solver_2stream` | — | | `sw_2stream_coeffs` | `sw_two_stream` + `sw_source_2str` (merged as `sw_dif_and_source` in later versions) | ``γ₁``–``γ₄`` of the Zdunkowski PIFM scheme [zdunkowski1980](@cite); ``R_{\mathrm{dir}}``/``T_{\mathrm{dir}}`` from [meador1980](@cite) Eqs. 14–18 | | direct-beam prepass inside `rte_sw_2stream!` | the direct-beam accumulation inside `sw_solver_2stream` | ``F^↓_{\mathrm{dir}} = μ₀ F_0 e^{-τ_{\mathrm{cum}}/μ₀}`` | | adding pass inside `rte_lw_2stream!` / `rte_sw_2stream!` | `adding` | interface albedo recursion, [shonk2008](@cite) | -| `Optics.delta_scale` | `mo_optical_props_kernels`: `delta_scale_2str_kernel` | δ-scaling, [joseph1976](@cite) (RRTMGP.jl uses the algebraically exact forms, e.g. ``g' = g/(1+g)``) | +| `Optics.delta_scale` | `mo_optical_props_kernels`: `delta_scale_2str_kernel` | δ-scaling, [joseph1976](@cite) (RRTMGP.jl uses the algebraically exact forms, e.g., ``g' = g/(1+g)``) | | `Optics.increment_2stream` | `mo_optical_props_kernels`: `increment_2stream_by_2stream` | — | | `Fluxes.compute_net_flux!` | `mo_fluxes_broadband_kernels`: `net_broadband` | ``F_{\mathrm{net}} = F^↑ - F^↓`` | @@ -78,7 +78,7 @@ The Fortran gas-optics kernels live in | `compute_τ_rayleigh` | `compute_tau_rayleigh` | Rayleigh scattering ``τ`` | | `interp1d_equispaced`, `interp2d`, `interp3d` | `interpolate1D`, `interpolate2D`, `interpolate3D` | — | | `Optics.compute_col_gas!` | `ty_gas_optics_rrtmgp`: `get_col_dry` | dry-air column amount [molecules cm⁻²] | -| `Optics.compute_relative_humidity!` | — (host responsibility in Fortran) | feeds the MERRA aerosol lookup | +| `Optics.compute_relative_humidity!` | (host responsibility in Fortran) | feeds the MERRA aerosol lookup | ## Cloud and aerosol optics @@ -90,7 +90,7 @@ The Fortran gas-optics kernels live in ## RRTMGP.jl-only layers -These have no Fortran counterpart; the closest analog is the example driver +These represent Julia-specific additions; the closest analog is the example driver programs (`examples/rfmip-clear-sky`, `examples/all-sky`), which each user of the Fortran code re-writes. @@ -98,6 +98,6 @@ the Fortran code re-writes. |:----------|:-----| | `RRTMGPSolver`, `update_fluxes!`, `prepare_atmosphere!`, the named getters | Layer-2 host convenience: construct once, drive the functional core each radiation step (see [The getter contract](@ref)) | | `standard_atmosphere`, `solve(profile)`, `RadiationOutput`, `solve_gray` | Layer-3 standalone front door (teaching, single-column experiments) | -| `GrayAtmosphere` module and the gray optics kernels | analytic gray-atmosphere radiation [schneider2004](@cite), [ogorman2008](@cite) | +| `GrayAtmosphere` module and the gray optics kernels | analytic gray-atmosphere radiation [schneider2004](@cite), [frierson2006](@cite), [ogorman2008](@cite) | | `Numerics` module (`k_min`, `τ_thresh`, `resonance_window`, `μ₀_min`) | documented numerical guard constants (hard-coded literals in the Fortran kernels) | | `LookupBundle` caching via `save_lookup_tables` / `load_lookup_tables` | NetCDF-free lookup reuse | diff --git a/docs/src/functional_core.md b/docs/src/functional_core.md index c17213eb3..948ce3c2c 100644 --- a/docs/src/functional_core.md +++ b/docs/src/functional_core.md @@ -1,45 +1,51 @@ # The functional core -RRTMGP's API is layered, and this page documents its center: a small **functional core** that -you can drive directly — in a class, in a standalone single-column experiment, or as the -foundation the host-convenience [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) is built on. The -core needs no ClimaCore; a single column is simply `ncol = 1`. +RRTMGP's API is layered, and this page documents its center: a **functional +core** that you can use in a class, in a standalone single-column experiment, +or as the foundation the [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) is built +upon. If you want results before concepts, start with the +[tutorials](tutorials/getting_started.md) and return here for what `solve` +assembles internally. -## The model: three inputs and one verb +## The model: three inputs and one action -A radiative-transfer solve is split into explicitly-owned pieces: +A radiative-transfer solve is split into separately-owned pieces: -1. **Immutable lookup tables** — the correlated-k gas/cloud/aerosol optics data, loaded once - and shared across columns and timesteps. (Gray radiation needs none.) -2. **A caller-owned atmospheric state** — [`AtmosphericState`](@ref +1. **Immutable lookup tables**: the correlated-k gas/cloud/aerosol optics data, + loaded once and shared across columns and timesteps. (Gray radiation uses + analytic formulas.) +2. **A caller-owned atmospheric state**: [`AtmosphericState`](@ref RRTMGP.AtmosphericStates.AtmosphericState) (or [`GrayAtmosphericState`](@ref - RRTMGP.AtmosphericStates.GrayAtmosphericState)) holding the inputs — layer/level pressure - and temperature, gas concentrations, cloud and aerosol properties — as plain device arrays - laid out `(nlay, ncol)` at layer centers and `(nlev, ncol)` at level faces. -3. **A preallocated per-band workspace** — one of [`NoScatLWRTE`](@ref RRTMGP.RTE.NoScatLWRTE) / - [`TwoStreamLWRTE`](@ref RRTMGP.RTE.TwoStreamLWRTE) / [`NoScatSWRTE`](@ref - RRTMGP.RTE.NoScatSWRTE) / [`TwoStreamSWRTE`](@ref RRTMGP.RTE.TwoStreamSWRTE) — which owns the - optics scratch and the output flux buffers, so a solve allocates nothing. - -The verb is [`solve_lw!`](@ref RRTMGP.RTESolver.solve_lw!) / [`solve_sw!`](@ref -RRTMGP.RTESolver.solve_sw!). It reads the state, writes the workspace's flux buffers in place, -and you read the results straight out: + RRTMGP.AtmosphericStates.GrayAtmosphericState)) holding the inputs + (layer/level pressure and temperature, gas concentrations, cloud and aerosol + properties) as device arrays laid out `(nlay, ncol)` at layer centers and + `(nlev, ncol)` at level faces. +3. **A preallocated per-band workspace**: one of [`NoScatLWRTE`](@ref + RRTMGP.RTE.NoScatLWRTE) / [`TwoStreamLWRTE`](@ref RRTMGP.RTE.TwoStreamLWRTE) + / [`NoScatSWRTE`](@ref RRTMGP.RTE.NoScatSWRTE) / [`TwoStreamSWRTE`](@ref + RRTMGP.RTE.TwoStreamSWRTE) which owns the optics scratch and the output + flux buffers, operating in place. + +The action is a single function call: [`solve_lw!`](@ref RRTMGP.RTESolver.solve_lw!) / +[`solve_sw!`](@ref RRTMGP.RTESolver.solve_sw!) reads the state and writes the +workspace's flux buffers in place, and you can read the results: ```julia solve_lw!(slv_lw, state, lookup_lw) # or solve_lw!(slv_lw, state) for gray F = slv_lw.flux.flux_net # (nlev, ncol) [W/m²] ``` -This is what RRTMGP's own tests drive, and what [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) -wraps behind [`update_fluxes!`](@ref RRTMGP.update_fluxes!) and the named getters. +This is what RRTMGP's own tests drive, and what [`RRTMGPSolver`](@ref +RRTMGP.RRTMGPSolver) wraps behind [`update_fluxes!`](@ref RRTMGP.update_fluxes!) +and the named getters. -## Gray radiation (no lookup tables) +## Gray radiation -The gray path needs no NetCDF data, so it runs in a fresh session after `using RRTMGP`. It -builds an analytic gray profile, longwave and shortwave two-stream workspaces, solves, and +The gray path uses analytic formulas and runs after `using RRTMGP`. It +builds a gray profile, longwave and shortwave two-stream workspaces, solves, and reads the net fluxes: -```julia +```@example functional_core using RRTMGP using RRTMGP.RTE: TwoStreamLWRTE, TwoStreamSWRTE using RRTMGP.RTESolver: solve_lw!, solve_sw! @@ -71,21 +77,23 @@ slv_sw = TwoStreamSWRTE(grid_params; cos_zenith, toa_flux, sfc_alb_direct = albedo, inc_flux_diffuse = nothing, sfc_alb_diffuse = copy(albedo)) -# the verb: solve, then read the fluxes out of the workspaces +# solve, then read the fluxes out of the workspaces solve_lw!(slv_lw, as) solve_sw!(slv_sw, as) lw_net = slv_lw.flux.flux_net # (nlev, ncol) [W/m²] sw_net = slv_sw.flux.flux_net net = lw_net .+ sw_net +net[end, 1] # net flux at the top of the atmosphere [W/m²] ``` -The one-liner [`solve_gray`](@ref RRTMGP.solve_gray) wraps precisely this. +The one-liner [`solve_gray`](@ref RRTMGP.solve_gray) wraps this. ## Clear-sky and all-sky radiation (with lookup tables) -The spectral path has the same shape; the differences are that the state is built from data -and the lookup tables are passed to the solve. Loading the tables needs `NCDatasets`: +The spectral path has the same shape as the gray path; the differences are that +the state is built from data and the lookup tables are passed to the solve. +Loading the tables needs `NCDatasets`: ```julia using NCDatasets, RRTMGP @@ -102,7 +110,7 @@ lookup_sw, _ = NCDataset(sw_lookup_file) do ds end # build the AtmosphericState from your data (pressures, temperatures, VMRs, ...) -as = ... # e.g. setup_clear_sky_as in test/read_clear_sky.jl +as = ... # e.g., setup_clear_sky_as in test/read_clear_sky.jl grid_params = RRTMGP.RRTMGPGridParams(FT; context, domain_nlay = nlay, ncol) slv_lw = TwoStreamLWRTE(grid_params; params, sfc_emis, inc_flux = nothing) @@ -118,24 +126,28 @@ lw_net = slv_lw.flux.flux_net See the full clear-sky driver in [`test/clear_sky_utils.jl`](https://github.com/CliMA/RRTMGP.jl/blob/main/test/clear_sky_utils.jl) -for the complete state construction, and the all-sky/aerosol drivers alongside it for the -cloud and aerosol lookups. +for the complete state construction, and the all-sky/aerosol drivers alongside +it for the cloud and aerosol lookups. ## Optional grid adaptation -`solve_*!` assumes a complete, valid state. If a host supplies only layer-center values, or -wants the isothermal boundary layer, or must clip unphysical inputs, RRTMGP provides -separable, in-place helpers — [`interpolate_levels!`](@ref RRTMGP.interpolate_levels!), -[`add_isothermal_boundary_layer!`](@ref RRTMGP.add_isothermal_boundary_layer!), -[`clip!`](@ref RRTMGP.clip!), and [`update_concentrations!`](@ref -RRTMGP.update_concentrations!) — each usable on its own and none required by `solve_*!`. -(Relative humidity is a host responsibility; see [`update_concentrations!`](@ref -RRTMGP.update_concentrations!).) +`solve_*!` assumes a complete, valid state. If a host supplies only +layer-center values, or wants the isothermal boundary layer (an extra buffer +layer above the model top; see [Boundary conditions](RTE.md) on the RTE page), +or must clip unphysical inputs, RRTMGP provides separable, in-place helpers: [`interpolate_levels!`](@ref +RRTMGP.interpolate_levels!), [`add_isothermal_boundary_layer!`](@ref +RRTMGP.add_isothermal_boundary_layer!), [`clip!`](@ref RRTMGP.clip!), and +[`update_concentrations!`](@ref RRTMGP.update_concentrations!), each usable +independently. (Relative humidity is a host responsibility; see +[`update_concentrations!`](@ref RRTMGP.update_concentrations!).) ## From the core to the host aggregate -For a host that runs radiation every step, [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) bundles -the two workspaces, the state, and the lookups, and drives the whole pipeline with one -allocation-free [`update_fluxes!`](@ref RRTMGP.update_fluxes!). Inputs and outputs are then -exchanged through named getters following the uniform contract on [The getter contract](@ref) -page. See the [API](@ref) reference for the full solver and getter listing. +For a host that runs radiation every step or every ``n`` steps (as is common +in atmosphere models because of the relatively large computational expense of +radiation), [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) bundles the two +workspaces, the state, and the lookups, and drives the pipeline with +[`update_fluxes!`](@ref RRTMGP.update_fluxes!). +Inputs and outputs are exchanged through named getters following the uniform +contract on [The getter contract](@ref) page. See the [API](@ref) reference for +the solver and getter listing. diff --git a/docs/src/getters.md b/docs/src/getters.md index 74c24ccac..4a634cdc3 100644 --- a/docs/src/getters.md +++ b/docs/src/getters.md @@ -4,39 +4,38 @@ CurrentModule = RRTMGP ``` -A [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) owns its device buffers (state, fluxes, -scratch). Hosts do not touch those buffers directly; they read and write them through -**named getters**. This getter set is the entire data-exchange interface — it is what lets -RRTMGP stay free of ClimaCore, since every input and output is a plain device array in a +A [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver) owns its device buffers (state, +fluxes, scratch). Hosts read and write them through **named getters**. This +getter set forms the data-exchange interface, operating on device arrays in a documented layout. ## The uniform contract -Every getter that carries a vertical dimension returns a **writable view into the solver's -own buffer**, following one invariant: - -- **Layout: physical dimension first, `ncol` last.** Layer-centered quantities are - `(nlay, ncol)`, level/face quantities are `(nlev, ncol)` (with `nlev = nlay + 1`), per-band - fluxes are `(nlev, ncol, nbnd)`, and surface/column quantities are `(ncol,)` — except the - spectral surface properties (emissivity, albedos), which are `(nbnd, ncol)` (hosts commonly - broadcast one value across the band dimension). Vertical (or layer) index first. -- **Domain-masked.** When the solver carries an internal isothermal boundary layer - (`isothermal_boundary_layer = true`), the getters exclude that extra top layer/level, so the - returned arrays are sized to the *physical* domain (`domain_nlay` / `domain_nlay + 1`) and - line up with the host's grid. Without a boundary layer the mask is a no-op. Either way the - getter returns a single concrete `view` type — this is what keeps - [`update_fluxes!`](@ref RRTMGP.update_fluxes!) allocation-free and type-stable. -- **A view, not a copy.** Writing into an input getter mutates the solver's buffer in place; - reading an output getter reads the buffer directly. `ncol` is the flattened column count - (`Nij·Nij·Nh` for a GCM, `1` standalone) — the solver stores it, never infers it. Three - getters are deliberate exceptions: `volume_mixing_ratio` for a well-mixed gas (with the - global-mean `VmrGM` storage) returns a host scalar copied off the device (not a view — see - below); the `spectral_*_flux_net` getters allocate a fresh array (`up − dn` is not stored); - and [`heating_rate`](@ref RRTMGP.heating_rate) computes its result on demand into a fresh - array. - -Internal code that needs the full, boundary-extended buffer (rather than the domain view) uses -the raw struct fields (e.g. `solver.net_flux_buffer`) or `parent(getter(solver))`. +Every getter that carries a vertical dimension returns a **writable view into +the solver's own buffer**, following one invariant: + +- **Layout: physical dimension first, `ncol` last.** Layer-centered quantities + are `(nlay, ncol)`, level/face quantities are `(nlev, ncol)` (with + `nlev = nlay + 1`), per-band fluxes are `(nlev, ncol, nbnd)`, and + surface/column quantities are `(ncol,)` (except the spectral surface + properties like emissivity and albedos, which are `(nbnd, ncol)`). +- **Domain-masked.** When the solver carries an internal isothermal boundary + layer (`isothermal_boundary_layer = true`), the getters exclude that extra + top layer/level, so the returned arrays are sized to the physical domain + (`domain_nlay` / `domain_nlay + 1`) and line up with the host's grid. The + getter returns a concrete `view` type, keeping [`update_fluxes!`](@ref + RRTMGP.update_fluxes!) type-stable. +- **A view, not a copy.** Writing into an input getter mutates the solver's + buffer in place; reading an output getter reads the buffer. `ncol` is the + flattened column count (`Nij·Nij·Nh` for a GCM, `1` standalone). Two getters + have different behavior: `volume_mixing_ratio` for a well-mixed gas (with the + global-mean `VmrGM` storage) returns a host scalar copied off the device, and + [`heating_rate`](@ref RRTMGP.heating_rate) computes its result on demand into + a fresh array. + +Internal code that needs the full, boundary-extended buffer (rather than the +domain view) uses the raw struct fields (e.g., `solver.net_flux_buffer`) or +`parent(getter(solver))`. ## Writing inputs and reading outputs @@ -51,39 +50,42 @@ RRTMGP.update_fluxes!(solver, seed) # runs the radiation update F = RRTMGP.net_flux(solver) # (nlev, ncol) view of the result ``` -With ClimaCore this is the zero-transpose bridge: `array2field(getter(solver), space)` wraps an -**input** view as a `Field` (writes flow straight into the solver's buffer), and -`field2array(dst) .= getter(solver)` copies an **output** into a destination field. On the GPU -the returned views are strided `SubArray`s of device arrays; to bring one to the CPU use -`Array(getter(solver))` (or the element-wise `.=` copy above) — never index it -element-by-element. +With ClimaCore, this forms the bridge: `array2field(getter(solver), space)` +wraps an input view as a `Field` (writes flow into the solver's buffer), and +`field2array(dst) .= getter(solver)` copies an output into a destination field. +On the GPU, the returned views are strided `SubArray`s of device arrays; to +bring one to the CPU, use `Array(getter(solver))` (or the element-wise `.=` +copy above). ## Responsibilities: what the host provides vs what RRTMGP derives -The host **writes** the inputs through the getters before calling `update_fluxes!`: +The host **writes** the inputs through the getters before calling +`update_fluxes!`: -- layer/level pressure and temperature, gas volume mixing ratios, cloud properties (effective - radii, water paths, fraction), and aerosol properties; -- surface and solar boundary conditions (`surface_temperature`, `surface_emissivity`, the - albedos, `cos_zenith`, `toa_flux`); -- for the deep-atmosphere GCM path, `deep_atmosphere_inverse_scaling` (computed from the host's - geometry). +- layer/level pressure and temperature, gas volume mixing ratios, cloud + properties (effective radii, water paths, fraction), and aerosol properties; +- surface and solar boundary conditions (`surface_temperature`, + `surface_emissivity`, the albedos, `cos_zenith`, `toa_flux`); +- for the deep-atmosphere GCM path, `deep_atmosphere_inverse_scaling` (computed + from the host's geometry). -RRTMGP **derives** the rest inside `update_fluxes!`: the optional preparation cascade (level -values from centers, the isothermal boundary layer, input clipping, and the dry-air column -amount via `compute_col_gas!`), then the optics and RTE solve, then the net-flux combine. +RRTMGP **derives** the rest inside `update_fluxes!`: the optional preparation +cascade (level values from centers, the isothermal boundary layer, input +clipping, and the dry-air column amount via `compute_col_gas!`), then the +optics and RTE solve, then the net-flux combine. Two things are **deliberately the host's job**: -- **Relative humidity** — the cascade does not recompute it; call `compute_relative_humidity!` - yourself if your gas optics need an up-to-date `layer_relative_humidity`. -- **Idealized profiles** (idealized water vapor or clouds) — a host modeling choice, applied - before the solve. +- **Relative humidity**: call `compute_relative_humidity!` if your gas optics + need an up-to-date `layer_relative_humidity`. +- **Idealized profiles** (idealized water vapor or clouds): a host modeling + choice, applied before the solve. ## Getter reference -Getters marked with a link have full docstrings; the rest follow the contract above. All -vertical/layer getters are domain-masked views into solver-owned buffers. +Getters marked with a link have full docstrings; the rest follow the contract +above. All vertical/layer getters are domain-masked views into solver-owned +buffers. ### Layer-center state — inputs, `(nlay, ncol)` @@ -121,13 +123,14 @@ vertical/layer getters are domain-masked views into solver-owned buffers. | `sw_flux_up`, `sw_flux_dn`, `sw_flux_net` | shortwave up / down / net | | `sw_direct_flux_dn` | shortwave direct-beam downward | | `net_flux` | combined longwave + shortwave net flux | -| [`heating_rate`](@ref RRTMGP.heating_rate) | radiative heating rate [K/s], `(nlay, ncol)` — computed on demand (allocates; not a view) | +| [`heating_rate`](@ref RRTMGP.heating_rate) | radiative heating rate [K/s], `(nlay, ncol)` (computed on demand into a fresh array) | -The clear-sky counterparts (`AllSkyRadiationWithClearSkyDiagnostics` only) mirror these: -`clear_lw_flux_up`/`clear_lw_flux_dn`/`clear_lw_flux`, `clear_sw_flux_up`/`clear_sw_flux_dn`/ -`clear_sw_direct_flux_dn`/`clear_sw_flux`, and `clear_net_flux`. Per-band fluxes +The clear-sky counterparts (`AllSkyRadiationWithClearSkyDiagnostics` only) +mirror these: `clear_lw_flux_up`/`clear_lw_flux_dn`/`clear_lw_flux`, +`clear_sw_flux_up`/`clear_sw_flux_dn`/`clear_sw_direct_flux_dn`/ +`clear_sw_flux`, and `clear_net_flux`. Per-band fluxes (`spectral_{lw,sw}_flux_{up,dn,net}`, `(nlev, ncol, nbnd)`) are covered on the -[API](@ref) page; there the `_up`/`_dn` variants are views while the `_net` variants allocate. +[API](@ref) page; all six are views into the retained per-band buffers. ### Clouds and aerosols @@ -144,7 +147,7 @@ The clear-sky counterparts (`AllSkyRadiationWithClearSkyDiagnostics` only) mirro | Getter | Quantity | |---|---| -| [`volume_mixing_ratio`](@ref RRTMGP.volume_mixing_ratio)`(s, name)` | gas volume mixing ratio — a `(nlay, ncol)` view for `"h2o"`/`"o3"`; a **host scalar** (copied off the device, not a view) for well-mixed gases with the default global-mean (`VmrGM`) storage. With full per-layer `Vmr` storage, every gas is a `(nlay, ncol)` view | +| [`volume_mixing_ratio`](@ref RRTMGP.volume_mixing_ratio)`(s, name)` | gas volume mixing ratio: a `(nlay, ncol)` view for `"h2o"`/`"o3"`; a host scalar for well-mixed gases with the default global-mean (`VmrGM`) storage. With full per-layer `Vmr` storage, every gas is a `(nlay, ncol)` view | | `center_z`, `face_z` | layer-center / level altitudes [m], or `nothing` if not provided | | `deep_atmosphere_inverse_scaling` | deep-atmosphere flux scaling `(nlev, ncol)`, or `nothing` | | [`radiation_method`](@ref RRTMGP.radiation_method) | the solver's radiation method | diff --git a/docs/src/howto/gpu.md b/docs/src/howto/gpu.md new file mode 100644 index 000000000..188fa6e32 --- /dev/null +++ b/docs/src/howto/gpu.md @@ -0,0 +1,124 @@ +# How to run on GPUs + +RRTMGP.jl runs the same code on CPUs and CUDA GPUs; the device is selected +through [ClimaComms](https://github.com/CliMA/ClimaComms.jl), and every array +the solver owns lives on that device. + +## Select the device + +Set the device before constructing anything, either with an environment +variable: + +``` +CLIMACOMMS_DEVICE=CUDA julia --project my_script.jl +``` + +or explicitly in code: + +```julia +import ClimaComms +ClimaComms.@import_required_backends # loads CUDA.jl when a GPU is requested + +context = ClimaComms.context() # honors CLIMACOMMS_DEVICE +device = ClimaComms.device(context) +DA = ClimaComms.array_type(device) # Array or CuArray +``` + +Pass `context` to [`RRTMGPGridParams`](@ref RRTMGP.RRTMGPGridParams) (or to +`solve_gray`/`solve`) and build your input arrays with `DA{FT}(...)`; from +there, everything (states, workspaces, solves) stays on the GPU. + +## Reading results without scalar indexing + +Getters return device-array views. Indexing them elementwise from the host +(`net_flux(solver)[1, 1]`) would trigger CUDA scalar indexing; instead +materialize what you need: + +```julia +F = Array(RRTMGP.net_flux(solver)) # one device→host copy +``` + +Broadcasted writes into getters (`layer_temperature(solver) .= T_dev`) are +device-side and safe. Two getters do not return views: +`volume_mixing_ratio(solver, name)` for well-mixed gases returns a host +scalar, and `heating_rate(solver)` allocates a fresh device array; read them +outside hot loops. + +## Checkpointing and device transfer + +`RRTMGPSolver` supports [Adapt](https://github.com/JuliaGPU/Adapt.jl), so +`Adapt.adapt(Array, solver)` produces a host copy (e.g., for serialization) and +`Adapt.adapt(CuArray, solver)` moves it back. RRTMGP's custom `adapt_structure` +methods preserve the internal view topology (scratch views into packed +buffers), so an adapted solver keeps the zero-allocation property. + +## Reproducibility caveat: McICA cloud sampling + +With partial cloud fractions, the all-sky methods sample cloud overlap +stochastically (McICA) using the global RNG. On a single CPU thread, +`update_fluxes!(solver, seed)` with `reset_rng_seed = true` makes results +reproducible. In multi-threaded settings and on the GPU (at least on CUDA 6.x), +the per-thread RNG state produces statistically but not deterministically +reproducible results for partially cloudy columns (overcast and clear columns +remain deterministic). + +## Parallelization strategy + +Each column and g-point is an independent monochromatic transfer problem; only +the vertical sweeps within a column are sequential. RRTMGP parallelizes over +this independence, with the same per-(g-point, column) kernel bodies shared by +both backends: + +- On the GPU, each CUDA thread owns one column and loops over its g-points, + accumulating the column's broadband fluxes thread-locally — no atomics and no + synchronization between threads. Parallelism therefore scales with `ncol`, + and a GPU pays off with many columns, the regime the benchmarks below target. +- On the CPU, the loop order is inverted: for each g-point, the columns are + distributed across threads (`ClimaComms.@threaded`), which keeps that + g-point's lookup-table slices hot in cache while sweeping the columns. With + a single-threaded context, the same code runs serially. + +## Performance expectations + +CI benchmarks the kernels on GPUs (see `perf/benchmark_ratchet.jl` +and the Buildkite "Benchmarks" group). Indicative behavior: + +- Work scales with `ncol × nlay × ngpt`; the g-point loop dominates. +- `update_fluxes!` allocates nothing on either device; data movement is the + host's choice (the getters return views). +- `Float32` roughly halves memory traffic; see + [Float32 and Float64](../precision.md) for the accuracy achieved at single + precision and the numerics behind it. + +## GPU versus CPU performance + +Wall times per full radiation step (`solve_lw!` + `solve_sw!`) comparing the +multi-threaded CPU strategy against the GPU strategy across single (`Float32`) and double (`Float64`) precision. + +### Benchmark problem dimensions + +Every configuration evaluated by the benchmark (see [`perf/benchmark_ratchet.jl`](https://github.com/CliMA/RRTMGP.jl/blob/main/perf/benchmark_ratchet.jl)) operates on an identical, uniform spatial grid representative of a high-resolution atmospheric model: + +- **Columns (`ncol`)**: **`86,400` horizontal columns** (`30² elements × 6 panels × 4² quadrature points`). +- **Vertical structure**: **`63` layers (`64` levels)** across every test profile (`5.53 × 10⁶` uniform 3D spatial coordinates, `ncol × nlev`), aligned with GPU warp sizes and memory boundaries. +- **Spectral quadrature points (`ngpt`)**: **`480` spectral g-points** (`256` longwave + `224` shortwave) across all problems, resulting in approximately `2.65 × 10⁹` total cell evaluations per `solve_lw!` + `solve_sw!` step. + +### Single precision (`Float32`) — Primary target + +At `Float32` precision, memory traffic across lookup-table interpolations is halved compared to `Float64`, enabling large GPU speedups: + +| Configuration | CPU (Intel Xeon, 12 threads) | GPU (NVIDIA A100 40GB) | Speedup | +|---|---|---|---| +| Clear-sky (two-stream) | 77.95 s | 1.24 s | 63.1× | +| All-sky, McICA clouds | 98.42 s | 1.82 s | 54.1× | +| All-sky with aerosols | 99.39 s | 1.95 s | 51.0× | + +### Double precision (`Float64`) + +For reference, running the identical uniform benchmark sweep in double precision (`Float64`) yields: + +| Configuration | CPU (Intel Xeon, 12 threads) | GPU (NVIDIA A100 40GB) | Speedup | +|---|---|---|---| +| Clear-sky (two-stream) | 79.64 s | 1.70 s | 46.8× | +| All-sky, McICA clouds | 101.60 s | 2.43 s | 41.8× | +| All-sky with aerosols | 106.33 s | 2.59 s | 41.1× | diff --git a/docs/src/howto/host_model.md b/docs/src/howto/host_model.md new file mode 100644 index 000000000..74729bc0c --- /dev/null +++ b/docs/src/howto/host_model.md @@ -0,0 +1,191 @@ +# How to drive RRTMGP from a host model + +This guide walks through embedding RRTMGP.jl in a host model (a GCM, a +single-column model, or any code with its own time loop): construct the solver +once, then exchange data through the [getter contract](../getters.md) and call +[`update_fluxes!`](@ref RRTMGP.update_fluxes!) every radiation step. The +[Functional core](../functional_core.md) page explains what these pieces are; +this page shows the order in which to assemble them. + +## 1. Construct the grid parameters + +[`RRTMGPGridParams`](@ref RRTMGP.RRTMGPGridParams) fixes the float type, the +compute device (via [ClimaComms](https://github.com/CliMA/ClimaComms.jl)), and +the grid size: + +```julia +import ClimaComms +ClimaComms.@import_required_backends +using RRTMGP + +context = ClimaComms.context() +grid_params = RRTMGP.RRTMGPGridParams( + Float64; + context, + domain_nlay = 63, # your model's layer count + ncol = ncolumns, + isothermal_boundary_layer = true, # add a buffer layer to the model top +) +``` + +`domain_nlay` is the number of physical layers. With +`isothermal_boundary_layer = true`, RRTMGP internally adds one extra layer +between your model top and the pressure minimum of the gas-optics tables, and +every getter masks it off (you read and write arrays sized to your domain). + +## 2. Choose a radiation method and build the lookup tables + +The method selects the physics and which lookup tables are loaded (load +NCDatasets first; the spectral methods live in a package extension): + +```julia +using NCDatasets + +method = RRTMGP.AllSkyRadiationWithClearSkyDiagnostics( + true, # aerosol_radiation + false, # reset_rng_seed (McICA reproducibility) +) +lookups = RRTMGP.lookup_tables(grid_params, method) +``` + +Build the tables once and pass them to every solver you construct; see +[How to cache the lookup tables](lookup_cache.md) to skip the NetCDF read +across sessions. + +## 3. Build the state and boundary conditions + +The host owns the [`AtmosphericState`](@ref RRTMGP.AtmosphericStates.AtmosphericState) +(pressures, temperatures, mixing ratios, optional +[`CloudState`](@ref RRTMGP.AtmosphericStates.CloudState) and +[`AerosolState`](@ref RRTMGP.AtmosphericStates.AerosolState)) and the boundary +conditions ([`LwBCs`](@ref RRTMGP.BCs.LwBCs), [`SwBCs`](@ref RRTMGP.BCs.SwBCs)). +All arrays live on the device described by `grid_params`, with the vertical +dimension first and columns last. `test/read_all_sky_with_aerosols.jl` is a +worked example of filling these from data. + +The boundary conditions close the column at both ends. At the top, the +insolation enters through the incident solar flux `toa_flux` [W/m²] and the +cosine of the solar zenith angle `cos_zenith`, both `(ncol,)`; the host +computes them from its solar and orbital code. The CliMA package +[Insolation.jl](https://github.com/CliMA/Insolation.jl) provides both as +functions of latitude, longitude, and time, from the orbital parameters. At the bottom, the longwave surface +emissivity `sfc_emis` and the shortwave direct and diffuse albedos are +`(nbnd, ncol)`, i.e., **per band**, so the surface can be spectrally selective — +vegetation, for example, absorbs strongly in the visible and reflects in the +near-infrared, and snow does the reverse. For a spectrally flat surface, fill +the band dimension with one value: + +```julia +FT = Float64 +DA = ClimaComms.array_type(ClimaComms.device(context)) +(; nbnd_lw, nbnd_sw) = lookups + +sfc_emis = fill!(DA{FT}(undef, nbnd_lw, ncolumns), FT(0.98)) +bcs_lw = RRTMGP.BCs.LwBCs(sfc_emis, nothing) # nothing: no prescribed + # incident longwave flux + +cos_zenith = DA{FT}(undef, ncolumns) # filled each step (below) +toa_flux = DA{FT}(undef, ncolumns) +alb_direct = fill!(DA{FT}(undef, nbnd_sw, ncolumns), FT(0.06)) +alb_diffuse = copy(alb_direct) +bcs_sw = RRTMGP.BCs.SwBCs(cos_zenith, toa_flux, alb_direct, nothing, alb_diffuse) +``` + +The two `nothing` slots are prescribed incident fluxes at the top (longwave and +diffuse shortwave), used for offline intercomparison cases; a host model +normally leaves them off, so the top boundary sees only the direct solar beam. +After construction, the host updates all of these through the getters — +`cos_zenith(solver)`, `toa_flux(solver)`, `surface_emissivity(solver)`, +`direct_sw_surface_albedo(solver)`, `diffuse_sw_surface_albedo(solver)` — as in +step 5, so a time-varying sun or a changing surface needs no reconstruction. + +## 4. Construct the solver + +```julia +solver = RRTMGP.RRTMGPSolver( + grid_params, method, params, bcs_lw, bcs_sw, as; + lookups, + interpolation = RRTMGP.ArithmeticMean(), # fill level values from layers + # center_z, face_z, # required for BestFit/HydrostaticBottom + # deep_atmosphere_inverse_scaling, # (nlev, ncol) metric factor +) +``` + +If your model provides level (face) pressures and temperatures directly, keep +the default `interpolation = NoInterpolation()`. Otherwise pick an +[`AbstractInterpolation`](@ref RRTMGP.AbstractInterpolation) and +[`AbstractBottomExtrapolation`](@ref RRTMGP.AbstractBottomExtrapolation); +z-based schemes need `center_z`/`face_z` at construction. + +## 5. The radiation step + +Write the current model state through the getters (they are writable views +into solver-owned device memory), then solve: + +```julia +RRTMGP.layer_temperature(solver) .= T_from_host +RRTMGP.surface_temperature(solver) .= T_sfc +RRTMGP.volume_mixing_ratio(solver, "h2o") .= vmr_h2o +RRTMGP.cos_zenith(solver) .= μ₀ + +RRTMGP.update_fluxes!(solver) # allocation-free; returns nothing + +F_net = RRTMGP.net_flux(solver) # (nlev, ncol) domain-masked view +hr = RRTMGP.heating_rate(solver) # allocates; prefer net-flux divergence in hot loops +``` + +`update_fluxes!` runs the full cascade: level interpolation, boundary-layer +fill, clipping to the lookup tables' valid range, column amounts, longwave and +shortwave solves, and the combined net flux. To inspect the prepared state +without solving, call [`prepare_atmosphere!`](@ref RRTMGP.prepare_atmosphere!). + +Two host responsibilities to remember (details in the +[getter contract](../getters.md)): + +- **Relative humidity** feeds RH-dependent aerosol optics; update + `layer_relative_humidity(solver)` (or call `compute_relative_humidity!`) if + needed. +- **Reading well-mixed gases** via `volume_mixing_ratio(solver, "co2")` + returns a host scalar; do this during setup. + +## 6. Per-band (spectral) fluxes + +The getters above return broadband fluxes. Components that need the spectral +decomposition — an atmospheric-chemistry scheme whose photolysis rates depend +on the ultraviolet and visible fluxes, or a land model that partitions the +radiation absorbed by a canopy into photosynthetically active (visible) and +near-infrared bands — can have the solver retain per-band fluxes. Request them +at construction and read them like the broadband getters, with the band as a +third dimension: + +```julia +solver = RRTMGP.RRTMGPSolver( + grid_params, method, params, bcs_lw, bcs_sw, as; + lookups, + spectral_fluxes = true, +) +RRTMGP.update_fluxes!(solver) + +F_sw = RRTMGP.spectral_sw_flux_dn(solver) # (nlev, ncol, nbnd_sw) view +wn = RRTMGP.sw_band_bounds(solver) # (2, nbnd_sw) wavenumber edges [cm⁻¹] +``` + +The band edges from [`sw_band_bounds`](@ref RRTMGP.sw_band_bounds) / +[`lw_band_bounds`](@ref RRTMGP.lw_band_bounds) identify each band's spectral +interval, so the host can sum the surface downwelling flux over the bands its +chemistry or canopy scheme needs. See +[How to get per-band (spectral) fluxes](spectral_fluxes.md) for the full +recipe and the supported configurations. + +## 7. Debugging: opt-in input validation + +During development, enable range checks on every solve: + +```julia +RRTMGP.check_values[] = true # validate_inputs runs inside update_fluxes! +``` + +Out-of-range inputs (negative pressures, `cos_zenith` outside [-1, 1], +albedos outside [0, 1], negative mixing ratios, NaNs) raise an error naming +the offending field. The checks reduce over device arrays, so keep the toggle +off in production to maintain peak performance. diff --git a/docs/src/howto/lookup_cache.md b/docs/src/howto/lookup_cache.md new file mode 100644 index 000000000..e814d9606 --- /dev/null +++ b/docs/src/howto/lookup_cache.md @@ -0,0 +1,43 @@ +# How to cache the lookup tables + +The spectral (non-gray) radiation methods need the RRTMGP gas/cloud/aerosol +lookup tables, which are parsed from NetCDF artifacts. Caching the tables avoids +re-reading the NetCDF files within a session and allows running without +NCDatasets in constrained environments. + +## Reuse within a session + +[`lookup_tables`](@ref RRTMGP.lookup_tables) returns a typed +[`LookupBundle`](@ref RRTMGP.LookupBundle); build it once and pass it to every +solver (and to every [`solve`](@ref RRTMGP.solve) call): + +```julia +using RRTMGP, NCDatasets + +lookups = RRTMGP.lookup_tables(grid_params, method) +solver_a = RRTMGP.RRTMGPSolver(grid_params, method, params, bcs_lw, bcs_sw, as_a; lookups) +out = RRTMGP.solve(profile; lookups) +``` + +## Cache across sessions + +[`save_lookup_tables`](@ref RRTMGP.save_lookup_tables) serializes a bundle to +disk; [`load_lookup_tables`](@ref RRTMGP.load_lookup_tables) restores it onto +the device described by your grid parameters: + +```julia +# once, in an environment with NCDatasets: +using RRTMGP, NCDatasets +lookups = RRTMGP.lookup_tables(grid_params, RRTMGP.ClearSkyRadiation(false)) +RRTMGP.save_lookup_tables("clearsky_lookups.jls", lookups) + +# later, e.g., in a classroom environment with only RRTMGP: +using RRTMGP +lookups = RRTMGP.load_lookup_tables("clearsky_lookups.jls", grid_params) +out = RRTMGP.solve(profile; lookups) +``` + +!!! warning "A cache, not an interchange format" + The file uses Julia's `Serialization` stdlib and is tied to the Julia + version and package layout that wrote it. Regenerate it when environments + change; the NetCDF artifacts remain the source of truth. diff --git a/docs/src/howto/spectral_fluxes.md b/docs/src/howto/spectral_fluxes.md new file mode 100644 index 000000000..372550ae3 --- /dev/null +++ b/docs/src/howto/spectral_fluxes.md @@ -0,0 +1,56 @@ +# How to get per-band (spectral) fluxes + +By default, RRTMGP retains only broadband fluxes. For applications that need +the spectral decomposition, the solver can retain per-band fluxes. Examples +include atmospheric chemistry, where photolysis rates depend on the +ultraviolet and visible fluxes; land models, where the radiation a canopy +absorbs divides into photosynthetically active (visible) and near-infrared +bands with very different vegetation albedos; band-by-band forcing +diagnostics; and satellite-channel proxies. + +## Request per-band fluxes at construction + +```julia +solver = RRTMGP.RRTMGPSolver( + grid_params, method, params, bcs_lw, bcs_sw, as; + lookups, + spectral_fluxes = true, +) +RRTMGP.update_fluxes!(solver) +``` + +This is supported for the spectral (non-gray) methods with two-stream optics +in both bands; other configurations raise an informative error at +construction. The per-band buffers add `(nlev, ncol, n_bnd)` arrays per band +set, so they are opt-in. + +## Read them + +```julia +F_lw = RRTMGP.spectral_lw_flux_up(solver) # (nlev, ncol, nbnd_lw) view +F_sw = RRTMGP.spectral_sw_flux_dn(solver) # (nlev, ncol, nbnd_sw) view +``` + +Band `b`'s slice `F_lw[:, :, b]` has the same layout as the broadband getters, +and the bands sum to the broadband flux: + +```julia +sum(RRTMGP.spectral_lw_flux_up(solver); dims = 3) ≈ RRTMGP.lw_flux_up(solver) +``` + +The `spectral_*_flux_net` getters are views into a retained per-band net-flux +buffer, updated on every solve like the `up`/`dn` buffers. + +## Identify the bands + +[`lw_band_bounds`](@ref RRTMGP.lw_band_bounds) and +[`sw_band_bounds`](@ref RRTMGP.sw_band_bounds) return the `(2, n_bnd)` +wavenumber edges (cm⁻¹) of each band: + +```julia +wn = RRTMGP.lw_band_bounds(solver) +wn[:, 1] # lower/upper wavenumber of longwave band 1 +``` + +The RRTMGP longwave tables have 16 bands and the shortwave tables 14, +following [pincus2019](@citet). diff --git a/docs/src/index.md b/docs/src/index.md index d2762522d..6a341aee4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,14 +1,74 @@ # RRTMGP.jl -Julia implementation of [RTE-RRTMGP](https://github.com/earth-system-radiation/rte-rrtmgp). +RRTMGP.jl is a Julia implementation of the radiative transfer solver RTE +(Radiative Transfer for Energetics) and the RRTMGP (RRTM for General +circulation model applications—Parallel) correlated-``k`` gas optics +([pincus2019](@citet)), based on the reference Fortran implementation +[rte-rrtmgp](https://github.com/earth-system-radiation/rte-rrtmgp). It +computes longwave and shortwave fluxes and heating rates for clear, cloudy, +and aerosol-laden atmospheres, as well as an analytic gray-radiation mode for +idealized studies, on CPUs and GPUs. It is the radiation scheme of the +[CliMA](https://clima.caltech.edu/) Earth System Model. + +## Quick start + +Build an idealized atmospheric column, then solve it: gray radiation uses +analytic formulas and needs no input data, while the full gas optics loads +NCDatasets to download and parse the lookup tables: + +```@example index +using RRTMGP +profile = RRTMGP.standard_atmosphere(Float64; kind = :tropical) +out = RRTMGP.solve(profile; method = RRTMGP.GrayRadiation()) # gray optics +out.heating_rate[1, 1] # heating rate at bottom layer [K/s] +``` + +```@example index +using NCDatasets # activates the gas optics +out = RRTMGP.solve(profile) # clear-sky correlated-k +out.lw_up[end, 1] # OLR [W/m²] +``` ## Code structure -RRTMGP is fundamentally split into two parts: +RRTMGP is split into two parts: + +- **Optics** computes optical properties and source functions given + atmospheric conditions (pressure, temperature, gas concentrations, clouds, + aerosols). +- **RTE** computes radiative fluxes given optical properties and source + functions. + +On top of this functional core sits the [`RRTMGPSolver`](@ref RRTMGP.RRTMGPSolver), +which host models drive through the [getter contract](getters.md), and a standalone +front door (`solve_gray`, `standard_atmosphere`, `solve`) for single-column work. + +## How the documentation is organized - - **Optics** computes optical properties and source functions given atmospheric conditions (e.g., pressure, temperature, gas concentrations). - - **RTE** computes radiative fluxes given optical properties and source functions. +- **Tutorials** (learning-oriented, runnable end to end): + [A first radiation calculation](tutorials/getting_started.md) and + [Radiative-convective equilibrium](tutorials/manabe_rce.md) (Manabe's + classic climate-sensitivity experiment). Start here if you want to learn + about radiative transfer and how to run this code. +- **How-to guides** (task-oriented recipes): + [driving RRTMGP from a host model](howto/host_model.md), + [running on GPUs](howto/gpu.md), + [caching the lookup tables](howto/lookup_cache.md), + [per-band fluxes](howto/spectral_fluxes.md), and + [the validated test problems](Example.md). Start here if you are wiring + RRTMGP into a climate model. +- **Explanation** (the concepts and the math): + [the functional core](functional_core.md), the [RTE solvers](RTE.md), the + [optics](Optics.md), and the + [Fortran and paper concordance](concordance.md) for readers coming from + rte-rrtmgp. +- **Reference**: the [API](api.md) and the [getter contract](getters.md). - ## Authors +## Authors -`RRTMGP.jl` is being developed by the [Climate Modeling Alliance](https://clima.caltech.edu/). +`RRTMGP.jl` is being developed by the +[Climate Modeling Alliance](https://clima.caltech.edu/). It is based on +RTE+RRTMGP and its reference +[Fortran implementation](https://github.com/earth-system-radiation/rte-rrtmgp), +developed by Robert Pincus, Eli Mlawer, and Jennifer Delamere +([pincus2019](@citet)). diff --git a/docs/src/optics/AtmosphericStates.md b/docs/src/optics/AtmosphericStates.md index f7c5662f1..fa5083d3c 100644 --- a/docs/src/optics/AtmosphericStates.md +++ b/docs/src/optics/AtmosphericStates.md @@ -5,10 +5,15 @@ CurrentModule = RRTMGP.AtmosphericStates ``` ```@docs +AbstractAtmosphericState AtmosphericState GrayAtmosphericState CloudState AerosolState +AbstractCloudMask +MaxRandomOverlap +AbstractGrayOpticalThickness GrayOpticalThicknessSchneider2004 GrayOpticalThicknessOGorman2008 +setup_gray_as_pr_grid ``` diff --git a/docs/src/optics/Fluxes.md b/docs/src/optics/Fluxes.md index f2cc81065..514eb5e09 100644 --- a/docs/src/optics/Fluxes.md +++ b/docs/src/optics/Fluxes.md @@ -5,6 +5,7 @@ CurrentModule = RRTMGP.Fluxes ``` ```@docs +AbstractFlux FluxLW FluxSW set_flux_to_zero! diff --git a/docs/src/precision.md b/docs/src/precision.md new file mode 100644 index 000000000..63d260b05 --- /dev/null +++ b/docs/src/precision.md @@ -0,0 +1,78 @@ +# Float32 and Float64 + +Every RRTMGP.jl kernel is generic in the working precision: the float type +chosen at construction (through [`RRTMGPGridParams`](@ref RRTMGP.RRTMGPGridParams), +[`solve`](@ref RRTMGP.solve), or [`solve_gray`](@ref RRTMGP.solve_gray)) flows +through the lookup tables, the optics, and the RTE solve. `Float64` is the +reference; `Float32` halves the memory traffic of the bandwidth-bound kernels +and is the precision at which the CliMA atmosphere runs radiation on GPUs. + +## Accuracy achieved at Float32 + +A consistency harness (`test/float32_consistency.jl`) solves the same gray, +clear-sky, and cloudy problems at both precisions, with the `Float32` states +rounded from the `Float64` ones, and pins the end-to-end difference — input +rounding, gas and cloud optics, and the RTE solve together. The measured +maximum broadband flux differences are: + +- every longwave path sits at its interpolation-noise floor of about + 2–5 × 10⁻⁴ W/m², roughly 25–90 times better than before the + single-precision program described below; +- shortwave differences are about 1–2 × 10⁻² W/m² in clear skies and about + 5 × 10⁻² W/m² with clouds. These were verified to be per-g-point + interpolation and coefficient noise rather than accumulation error: + repeating the runs with full `Float64` broadband accumulation left them + unchanged, so compensated summation was deliberately not added. + +For scale, these differences are orders of magnitude below both typical flux +magnitudes (hundreds of W/m²) and the accuracy of the correlated-``k`` +approximation itself; the Fortran reference (rte-rrtmgp) accepts an absolute +flux error of 0.35 W/m² in its own validation. CI locks them in with ratcheting thresholds +(gray 10⁻³ W/m² and 10⁻⁸ K/s for the heating rate; longwave 10⁻³ W/m²; +shortwave 3 × 10⁻² clear and 1.2 × 10⁻¹ W/m² cloudy) that are tightened as +numerics fixes land and never loosened. The reference comparisons against the +official rte-rrtmgp results also run at both precisions. + +## How: the single-precision numerics + +The `Float32` accuracy rests on evaluating exact reformulations instead of +cancellation-prone ones, and on switching to series expansions where +cancellation is unavoidable. The measures, roughly in the order radiation +flows: + +- **Gas optics.** The interpolation fraction in the binary species parameter + ``η`` is computed relative to its clamped table cell, so it stays continuous + at ``η = 1`` — a value `Float32` rounds to far more often than `Float64`. +- **Longwave, no scattering.** The linear-in-``τ`` source factor + ``(1 - t)/\tau - t`` ([clough1992](@citet), Eq. 13) loses + ``\sim\!\mathrm{eps}/\tau^2`` to cancellation for thin layers; it switches to + its third-order series at ``\tau = \mathrm{eps}^{1/4}``, where the two error + curves cross. +- **Two-stream coefficients.** The diffusion eigenvalue + ``k = \sqrt{(\gamma_1-\gamma_2)(\gamma_1+\gamma_2)}`` uses exact + ``\gamma_1 - \gamma_2`` identities rather than subtracting near-equal + numbers, with ``k^2`` floored at ``\sqrt{\mathrm{eps}}``; thin-layer factors + ``1 - e^{-k\tau}`` are evaluated with `expm1`. +- **Longwave two-stream source.** The [toon1989](@citet) source terms are + factored so that no term divides by ``\tau`` and no small difference of + near-equal quantities appears (exact ``1 \mp R_{\mathrm{dif}} - + T_{\mathrm{dif}}`` factorizations). +- **Shortwave direct beam.** The direct reflectance and transmittance of + [meador1980](@citet) (Eqs. 14–15) have a removable singularity at + ``k\mu_0 = 1``; ``k\mu_0`` is nudged off resonance symmetrically within a + ``\sqrt{\mathrm{eps}}`` window, keeping numerator and denominator mutually + consistent. An energy-conservation clamp then bounds the direct reflectance + and transmittance by the energy the unscattered beam leaves behind, + following [ukkonen2024](@citet); and the direct-beam profile itself is built + by accumulating optical depth downward rather than by repeated + multiplication. +- **Delta scaling.** The ``f = g^2`` similarity scaling of cloud and aerosol + optics ([joseph1976](@citet)) is evaluated in exact, non-cancelling forms + (for example ``g' = g/(1+g)`` instead of ``(g - g^2)/(1 - g^2)``). + +All the guard constants involved (`k_min`, `τ_thresh`, `resonance_window`, +`μ₀_min`) live in the `RRTMGP.Numerics` module, each with the derivation of +its value, and all scale with `eps(FT)` — so the kernels behave consistently +at either precision, and the same policy would extend to other float types. +Where a threshold matches the Fortran reference (rte-rrtmgp), the docstring +says so. diff --git a/docs/tutorials/getting_started.jl b/docs/tutorials/getting_started.jl new file mode 100644 index 000000000..f7d47a267 --- /dev/null +++ b/docs/tutorials/getting_started.jl @@ -0,0 +1,187 @@ +# # A first radiation calculation +# +# This tutorial computes radiative fluxes and heating rates with RRTMGP.jl in +# a few lines, first for a gray (single-band) atmosphere and then with the +# full RRTMGP correlated-``k`` gas optics. The gray model is analytic, and the +# gas-optics lookup tables download automatically. + +using RRTMGP +using CairoMakie + +# ## An idealized atmospheric column +# +# Radiation needs an atmospheric state to act on: temperatures, pressures, and +# composition. Each [`standard_atmosphere`](@ref RRTMGP.standard_atmosphere) is +# an analytic clear-sky column, loosely following the Air Force Geophysics Laboratory +# reference climatology ([anderson1986](@citet)): a two-segment temperature profile +# with constant tropospheric lapse rate up to the tropopause, then a warming +# stratosphere, hydrostatic pressure, water vapor decaying exponentially from +# the surface with a floor in the stratosphere, an ozone layer peaked near +# 30 km, and present-day well-mixed gases (CO₂, CH₄, N₂O, …). The three +# `kind`s — `:tropical`, `:midlatitude_summer`, and `:subarctic_winter` — +# differ in surface temperature, tropopause height, and moisture, from warm and +# moist to cold and dry. It needs no input data: + +profile = RRTMGP.standard_atmosphere(Float64; kind = :tropical, nlay = 60); + +# ## Gray radiation +# +# The gray atmosphere replaces the full spectral dependence of gas optics by a +# single prescribed longwave optical-thickness profile, the workhorse of +# idealized-climate studies. RRTMGP's default follows [frierson2006](@citet) and +# [ogorman2008](@citet): the +# vertical longwave optical depth blends a linear and a quartic dependence on the +# normalized pressure ``σ = p/p_s``, +# ```math +# \widehat{\tau}(σ) \propto f_\ell\,σ + (1 - f_\ell)\,σ^4, +# ``` +# so it stays finite aloft and thickens toward the surface. Because the gray +# equations are analytically tractable, the semi-gray column also has a +# closed-form radiative-equilibrium solution, ``T(\widehat{\tau}) = T_e\,[(1 + +# D\widehat{\tau})/2]^{1/4}`` with diffusivity factor ``D``; RRTMGP's test suite +# integrates the gray longwave solver to equilibrium and checks the computed +# temperatures against the analytic radiative-equilibrium balance ``\sigma T^4 = +# (F^\uparrow + F^\downarrow)/2``. Solving the column with gray optics takes one +# call (no lookup tables are needed; the temperatures are the profile's): + +out = RRTMGP.solve(profile; method = RRTMGP.GrayRadiation()); + +# The result is a [`RadiationOutput`](@ref RRTMGP.RadiationOutput) whose +# fields are `(nlev, ncol)` fluxes in W/m² plus the heating rate in K/s. +# Plot the longwave and shortwave net fluxes and the heating rate against +# pressure: + +p_lev = Array(RRTMGP.level_pressure(out.solver)) ./ 100 # [hPa] +p_lay = Array(RRTMGP.layer_pressure(out.solver)) ./ 100 + +fig = Figure(size = (700, 400)) +ax1 = Axis( + fig[1, 1]; + xlabel = "flux [W/m²]", + ylabel = "pressure [hPa]", + yreversed = true, + limits = (nothing, nothing, nothing, 1000), +) +lines!(ax1, Array(out.lw_net)[:, 1], p_lev[:, 1]; label = "longwave net") +lines!(ax1, Array(out.sw_net)[:, 1], p_lev[:, 1]; label = "shortwave net") +lines!(ax1, Array(out.net)[:, 1], p_lev[:, 1]; label = "total net") +axislegend(ax1; position = :rb, framevisible = false) +ax2 = Axis( + fig[1, 2]; + xlabel = "heating rate [K/day]", + yreversed = true, + limits = (nothing, nothing, nothing, 1000), +) +lines!(ax2, 86400 .* Array(out.heating_rate)[:, 1], p_lay[:, 1]) +fig + +# ## Configuring the gray optical depth +# +# The optical-thickness model is a keyword argument. The [schneider2004](@citet) +# form exposes a single pressure exponent ``α`` through ``\widehat{\tau}(p) = d_0\, +# (p/p_s)^α``, which controls how the absorber is distributed with height: +# ``α = 1`` for a well-mixed absorber (constant mixing ratio, no pressure +# broadening), larger ``α`` for one concentrated near the surface. Change it in +# one line: + +using RRTMGP.AtmosphericStates: GrayOpticalThicknessSchneider2004 + +out_wellmixed = RRTMGP.solve( + profile; + method = RRTMGP.GrayRadiation(), + optical_thickness = GrayOpticalThicknessSchneider2004(Float64; α = 1.0), +); +println("surface net flux, α = 1: ", + round(Array(out_wellmixed.net)[1, 1]; digits = 1), " W/m²") + +# ([`solve_gray`](@ref RRTMGP.solve_gray) is a related one-liner that builds an +# analytic, latitude-dependent gray column internally instead of taking a +# profile.) + +# The longwave net flux converges toward the outgoing longwave radiation at +# the top; the corresponding heating rate is the radiative cooling that +# convection balances in the troposphere. +# +# ## Clear-sky gas optics +# +# For the real gas optics, load NCDatasets (which activates RRTMGP's +# lookup-table support, downloading the tables on first use) and solve the +# same column with the full correlated-``k`` treatment: + +using NCDatasets + +out_cs = RRTMGP.solve(profile); + +# `out_cs.solver.lookups` holds the parsed lookup tables; pass them back via +# `solve(profile; lookups)` to skip the NetCDF read when solving many +# profiles. The outgoing longwave radiation (OLR) is the upwelling longwave +# flux at the top level: + +olr(kind, lookups) = Array( + RRTMGP.solve( + RRTMGP.standard_atmosphere(Float64; kind); + lookups, + ).lw_up, +)[end, 1] + +lookups = out_cs.solver.lookups +for kind in (:tropical, :midlatitude_summer, :subarctic_winter) + println(rpad(kind, 22), round(olr(kind, lookups); digits = 1), " W/m²") +end + +# Warm, moist columns emit more (though not proportionally to ``σT_s^4``, +# because water vapor absorbs part of the surface emission). +# +# ## The effect of doubling CO₂ +# +# Because the profile carries its well-mixed gases as a dictionary, changing +# the CO₂ concentration is one line. Comparing the OLR at fixed temperature +# quantifies the *instantaneous radiative forcing* at the top of the +# atmosphere: + +profile_2x = RRTMGP.standard_atmosphere(Float64; kind = :tropical, nlay = 60) +profile_2x.well_mixed_vmr["co2"] = 2 * profile.well_mixed_vmr["co2"] +out_2x = RRTMGP.solve(profile_2x; lookups) + +ΔOLR = Array(out_2x.lw_up)[end, 1] - Array(out_cs.lw_up)[end, 1] +println("ΔOLR from doubling CO₂: $(round(ΔOLR; digits = 2)) W/m²") + +# Doubling CO₂ reduces the OLR by a few W/m², creating the energy imbalance that +# forces the climate to warm. How much the surface must warm to restore +# balance is the subject of the +# [radiative-convective equilibrium tutorial](manabe_rce.md), which turns +# this experiment into Manabe's classic climate-sensitivity calculation. +# +# ## The clear-sky heating rate +# +# The clear-sky heating-rate profile shows the stratospheric heating by ozone +# absorption of shortwave radiation: + +hr = 86400 .* Array(out_cs.heating_rate) # [K/day] +p_cs = Array(RRTMGP.layer_pressure(out_cs.solver)) ./ 100 + +fig2 = Figure(size = (400, 400)) +ax3 = Axis( + fig2[1, 1]; + xlabel = "heating rate [K/day]", + ylabel = "pressure [hPa]", + yscale = log10, + yreversed = true, + limits = (nothing, nothing, nothing, 1000), + title = "Clear-sky radiative heating (tropical)", +) +lines!(ax3, hr[:, 1], p_cs[:, 1]) +vlines!(ax3, [0.0]; color = :gray, linestyle = :dash) +fig2 + +# ## Where to go from here +# +# - The [radiative-convective equilibrium tutorial](manabe_rce.md) builds a +# single-column climate model from these pieces. +# - The [Functional core](../functional_core.md) page shows what `solve` and +# `solve_gray` assemble internally (the explicit states and +# `solve_lw!`/`solve_sw!` kernels). +# - The [getter contract](../getters.md) documents how a host model exchanges +# data with an `RRTMGPSolver` in place. +# - [How to get per-band (spectral) fluxes](../howto/spectral_fluxes.md) shows +# how to retain the band-by-band fluxes behind these broadband results. diff --git a/docs/tutorials/manabe_rce.jl b/docs/tutorials/manabe_rce.jl new file mode 100644 index 000000000..a7bdb840e --- /dev/null +++ b/docs/tutorials/manabe_rce.jl @@ -0,0 +1,495 @@ +# # Radiative-convective equilibrium (Manabe's classic experiment) +# +# This tutorial reproduces the classic radiative-convective equilibrium (RCE) +# calculations of [manabe1964](@citet) and [manabe1967](@citet) with RRTMGP's +# clear-sky gas optics: a single atmospheric column with +# +# - a **prescribed tropospheric lapse rate** (convective adjustment to +# 6.5 K/km), +# - a **prescribed relative-humidity profile** (water vapor follows the +# temperature), and +# - a stratosphere in **radiative equilibrium**. +# +# The same setup (hard convective adjustment plus fixed relative humidity) +# is the default configuration of the single-column model +# [konrad](https://github.com/atmtools/konrad), and [kluft2019](@citet) showed +# that it reproduces Manabe's headline result: an equilibrium climate +# sensitivity of about 2 K per doubling of CO₂ when relative humidity is held +# fixed. +# +# From this one setup, we reproduce Manabe's classic results: the +# radiative-versus-radiative-convective comparison, the decomposition of the +# greenhouse effect into its individual absorbers (water vapor, CO₂, ozone), +# the dependence of the equilibrium on the assumed lapse rate, and the surface +# warming under CO₂ increases with the water-vapor feedback isolated by +# contrasting fixed relative and fixed absolute humidity. +# +# Along the way, this tutorial demonstrates the host-model workflow of +# RRTMGP.jl: build a solver once, then mutate its state through the +# [getter contract](../getters.md) and call +# [`update_fluxes!`](@ref RRTMGP.update_fluxes!) inside a time loop. + +using RRTMGP +using NCDatasets # activates RRTMGP's lookup-table extension +import Thermodynamics as TD +import ClimaParams # brings in the CliMA default thermodynamic constants +using CairoMakie + +# ## The column +# +# Start from the idealized midlatitude-summer standard atmosphere on 60 layers +# up to 45 km. Its pressure grid, ozone profile, and well-mixed gases +# (CO₂ = 420 ppmv, etc.) stay fixed; temperature and water vapor will be +# overwritten by the RCE iteration. We keep a copy of the standard-atmosphere +# temperature to overlay later as the "observed" reference. + +const FT = Float64 +nlay = 60 +profile = RRTMGP.standard_atmosphere(FT; kind = :midlatitude_summer, nlay); +T_obs = copy(profile.t_lay[:, 1]); + +# Like Manabe and collaborators, we force the column with the **global-mean +# insolation** ``S_0/4 \approx 341`` W/m² (the solar constant spread over the sphere). +# Because the mean solar zenith angle sets the shortwave absorption path length +# [cronin2014](@cite), we use a representative zenith angle of 47.9° +# (``\cos θ \approx 0.67``) with an incident flux of 509 W/m², so that +# ``509 \times \cos(47.9^\circ) \approx 341`` W/m². The surface albedo is set to 0.3, higher +# than a bare surface, to stand in for the shortwave reflection of the clouds +# this clear-sky column omits: + +insolation = (; cos_zenith = cosd(47.9), toa_flux = 509.0, surface_albedo = 0.3); + +# Both RRTMGP and [Thermodynamics.jl](https://github.com/CliMA/Thermodynamics.jl) +# read their physical constants from +# [ClimaParams.jl](https://github.com/CliMA/ClimaParams.jl), the single source of +# truth for CliMA parameters, so the constants used across the radiation and +# thermodynamics calculations here are consistent. We build both parameter sets +# from it and take the gravitational acceleration, the dry-air gas constant, and +# the dry-air heat capacity from them: + +params = RRTMGP.Parameters.RRTMGPParameters(FT) +td_params = TD.Parameters.ThermodynamicsParameters(FT) +R_d = TD.Parameters.R_d(td_params) +cp_d = TD.Parameters.cp_d(td_params) +g = RRTMGP.Parameters.grav(params) # gravitational acceleration [m/s²] + +# The iteration below marches the layer temperatures, so we ask the solver to +# rebuild the level (cell-face) temperatures and pressures from the layer +# (cell-center) values on every [`update_fluxes!`](@ref RRTMGP.update_fluxes!) call: +# `interpolation = ArithmeticMean()` averages the adjacent layers on interior +# faces and extrapolates linearly at the boundary faces. We collect this, the +# parameters, and the insolation into one configuration shared by every column +# in this tutorial. One call then builds the clear-sky solver (downloading the +# RRTMGP lookup tables on first use) and solves the initial state; its state is +# exposed as writable views that the iteration overwrites in place: + +setup = (; params, insolation..., interpolation = RRTMGP.ArithmeticMean()) +out = RRTMGP.solve(profile; setup...) +solver = out.solver; + +# ## Fixed relative humidity +# +# Manabe and Wetherald prescribed the relative-humidity profile +# ``h(p) = 0.77\,(p/p_s - 0.02)/(1 - 0.02)``, so the specific humidity rises +# and falls with the temperature (the water-vapor feedback in its simplest +# form). The saturation vapor pressure ``e^*(T)`` comes from [Thermodynamics.jl](https://github.com/CliMA/Thermodynamics.jl), +# which integrates the Clausius-Clapeyron relation under the Rankine-Kirchhoff +# (constant-heat-capacity) approximation [romps2021](@cite), the same +# formulation the CliMA model uses. From the vapor partial pressure +# ``e = h\,e^*`` the water-vapor volume mixing ratio (moles of vapor per mole +# of dry air, RRTMGP's convention) is ``e/(p - e)``. We floor it at the +# stratospheric minimum Manabe used (a vmr of ≈ 4.8 × 10⁻⁶). + +e_sat(T) = TD.saturation_vapor_pressure(td_params, T, TD.Liquid()) # [Pa] +rel_hum(p, p_sfc) = max(0.77 * (p / p_sfc - 0.02) / (1 - 0.02), 0.0) +const vmr_h2o_min = 4.8e-6 + +function set_humidity!(solver) + T = RRTMGP.layer_temperature(solver) + p = RRTMGP.layer_pressure(solver) + vmr = RRTMGP.volume_mixing_ratio(solver, "h2o") + p_sfc = RRTMGP.level_pressure(solver)[1, 1] + e = rel_hum.(p, p_sfc) .* e_sat.(T) # vapor partial pressure [Pa] + @. vmr = max(e / (p - e), vmr_h2o_min) + ## Alternatively, Thermodynamics.jl can do both steps in one go: + ## q = TD.q_vap_from_RH.(td_params, p, T, rel_hum.(p, p_sfc), Ref(TD.Liquid())) + ## @. vmr = max(TD.vol_vapor_mixing_ratio(td_params, q), vmr_h2o_min) + return nothing +end; + +# ## Convective adjustment +# +# The steepest lapse rate a dry column sustains is the dry adiabatic value +# ``Γ_d = g/c_p``, calculated from the Thermodynamics.jl constants: + +Γ_dry = g / cp_d +println("dry adiabatic lapse rate g/cₚ = $(round(1000 * Γ_dry; digits = 1)) K/km") + +# Moist convection and large-scale eddies hold the observed troposphere to a +# gentler value, so we impose a critical lapse rate of 6.5 K/km. Wherever +# radiative cooling pulls the lapse rate above it, convection is assumed to +# instantaneously restore the critical profile ``T(z) = T_s - Γ z``, anchored +# at a trial surface temperature ``T_s`` (hard adjustment). The lapse rate is +# defined per unit height, yet no altitudes are needed to apply it: +# combining ``dT/dz = -Γ`` with hydrostatic balance and the ideal-gas law gives +# ``dT/T = (Γ R_d / g)\, dp/p``, so in the solver's native pressure coordinate, +# the critical profile is the power law +# ```math +# T_c(p) = T_s \left(\frac{p}{p_s}\right)^{Γ R_d / g}, +# ``` +# which reduces to the dry adiabat ``T \propto p^{R_d/c_p}`` for ``Γ = g/c_p``. +# Layers whose radiative-equilibrium temperature is warmer than ``T_c`` (the +# stratosphere) are left untouched, allowing the tropopause to emerge from the +# calculation. +# +# Although convection should only redistribute energy, not create or destroy +# it, convective adjustment by itself does not conserve column energy. We +# enforce energy conservation in equilibrium by adjusting the surface +# temperature (an unknown of the problem): each equilibration below holds a +# trial ``T_s`` fixed, and the trial value is then adjusted until the net +# radiative flux at the top of the atmosphere vanishes, so the energy balance +# determines ``T_s``. + +const Γ_crit = 6.5e-3 # critical lapse rate [K/m] + +function convective_adjustment!(solver, T_sfc; Γ = Γ_crit) + T = RRTMGP.layer_temperature(solver) + p = RRTMGP.layer_pressure(solver) + p_sfc = RRTMGP.level_pressure(solver)[1, 1] + @. T = max(T, T_sfc * (p / p_sfc)^(Γ * R_d / g)) # critical profile T_c(p) + return nothing +end; + +# ## The RCE iteration +# +# Each step applies the convective adjustment, updates the water vapor to the +# fixed relative humidity, solves the radiative transfer, and marches the +# temperature with the radiative heating rate. Level (cell-face) temperatures +# are rebuilt from the layer (cell-center) temperatures inside +# `update_fluxes!`, by the `ArithmeticMean` interpolation requested at +# construction. The ground temperature enters separately, through the +# surface-emission boundary condition (`surface_temperature`); keeping the two +# distinct lets the air at the bottom face be colder than the ground, which +# pure radiative equilibrium demands (the `UseSurfaceTempAtBottom` +# extrapolation option would pin the bottom face to the ground temperature and +# spuriously chill the lowest layer in the radiative-equilibrium experiment). +# The column is equilibrated when the largest temperature change per step is +# negligible. + +# Marching the layer temperatures independently excites a grid-scale +# (two-layer) oscillation in the stratosphere: a sawtooth in the layer +# temperatures maps to nearly unchanged level temperatures, so it barely +# perturbs the level fluxes that would otherwise damp it, and it survives no +# matter how small the time step. We remove it with a light three-point +# (`1-2-1`) filter applied to the interior layers each step — a weak numerical +# diffusion. Its damping is scale-selective: each step multiplies a mode of +# wavelength ``L`` by ``1 - 4ν\sin^2(πΔz/L)``, so the two-layer mode shrinks by +# ``1 - 4ν`` per step while smooth structure, which the filter perturbs only in +# proportion to its curvature, is nearly untouched. The main visible cost is a +# slight rounding of the tropopause kink; the balanced surface temperature +# shifts by less than 0.1 K. + +function smooth!(T, ν) + nlay = size(T, 1) + prev = T[1, 1] + for k in 2:(nlay - 1) + cur = T[k, 1] + T[k, 1] = cur + ν * (prev - 2cur + T[k + 1, 1]) + prev = cur + end + return nothing +end + +# One iteration serves every experiment: `convection = false` gives pure +# radiative equilibrium, `humidity = false` holds the water vapor constant +# (used below to remove water vapor entirely and to freeze it for the +# fixed-absolute-humidity experiments), and the `Γ` keyword changes the +# critical lapse rate. The adjustment and humidity are synced before each +# solve, including on the final (converged) step, so the reported fluxes are +# consistent with the temperatures. + +function equilibrate!( + solver, + T_sfc; + convection = true, + humidity = true, + Γ = Γ_crit, + dt = 8 * 3600.0, + maxsteps = 5000, + tol = 1e-4, + ν = 0.1, +) + RRTMGP.surface_temperature(solver) .= T_sfc + T = RRTMGP.layer_temperature(solver) + for _ in 1:maxsteps + convection && convective_adjustment!(solver, T_sfc; Γ) + humidity && set_humidity!(solver) + RRTMGP.update_fluxes!(solver) + dT = clamp.(dt .* Array(RRTMGP.heating_rate(solver)), -2, 2) + maximum(abs, dT) < tol && break + T .+= dT + smooth!(T, ν) + end + return nothing +end; + +# Run it. The net flux at the top of the atmosphere (`net = up - down`) +# measures the column's energy imbalance; at the true equilibrium surface +# temperature it vanishes. + +toa_imbalance(solver) = RRTMGP.net_flux(solver)[end, 1] + +T_sfc = FT(288) +equilibrate!(solver, T_sfc) +N = toa_imbalance(solver) +println("TOA imbalance at Tₛ = $(T_sfc) K: $(round(N; digits = 2)) W/m²") + +# ## Radiative equilibrium versus radiative-convective equilibrium +# +# Repeating the iteration without the convective adjustment reproduces +# Manabe and Strickler's famous comparison: pure radiative equilibrium produces +# high surface temperatures and low upper-troposphere temperatures, while the +# convective adjustment brings the profile into agreement with observations +# (here, the midlatitude-summer standard atmosphere we started from). Near the +# surface, the radiative-equilibrium temperature falls steeply — far more +# steeply than any adiabat — and the air at the bottom face remains several +# kelvin colder than the 288 K ground: the classic surface discontinuity of +# radiative equilibrium. Both features are convectively unstable; the +# turbulent heat fluxes that the convective adjustment stands in for would +# erase them, which is what the RCE profile shows. + +T_rce = copy(Array(RRTMGP.layer_temperature(solver))) +p_lay = copy(Array(RRTMGP.layer_pressure(solver))) + +solver_re = RRTMGP.solve(profile; lookups = solver.lookups, setup...).solver +equilibrate!(solver_re, T_sfc; convection = false) +T_re = copy(Array(RRTMGP.layer_temperature(solver_re))); + +fig = Figure(size = (500, 500)) +ax = Axis( + fig[1, 1]; + xlabel = "temperature [K]", + ylabel = "pressure [hPa]", + yscale = log10, + yreversed = true, + limits = (nothing, nothing, nothing, 1000), + title = "Radiative vs. radiative-convective equilibrium", +) +lines!(ax, T_obs, p_lay[:, 1] ./ 100; color = :gray, linestyle = :dot, label = "standard atmosphere") +lines!(ax, T_re[:, 1], p_lay[:, 1] ./ 100; label = "radiative equilibrium") +lines!(ax, T_rce[:, 1], p_lay[:, 1] ./ 100; label = "RCE (Γ = 6.5 K/km)") +axislegend(ax; position = :rt, framevisible = false, backgroundcolor = :white) +fig + +# ## Climate sensitivity at fixed relative humidity +# +# Manabe and Wetherald's headline experiment: find the surface temperature +# that balances the column (zero net flux at the top of the atmosphere) for +# the present CO₂ concentration and for increased CO₂, keeping the relative +# humidity fixed. The difference is the equilibrium climate sensitivity of +# the radiative-convective column. +# +# A secant iteration on the TOA imbalance converges in a few equilibrations +# (each restarted from the previous equilibrium, so they are fast): + +function balanced_surface_temperature!(solver, T1, T2; humidity = true, Γ = Γ_crit, tol = 5e-3) + eq(T) = (equilibrate!(solver, T; humidity, Γ); toa_imbalance(solver)) + N1 = eq(T1) + N2 = eq(T2) + while abs(N2) > tol && abs(T2 - T1) > 1e-3 + Tn = T2 - N2 * (T2 - T1) / (N2 - N1) + T1, N1 = T2, N2 + T2 = Tn + N2 = eq(T2) + end + return T2 +end + +Ts_1x = balanced_surface_temperature!(solver, FT(285), FT(290)) +T_1x = copy(Array(RRTMGP.layer_temperature(solver))) +vmr_1x = copy(Array(RRTMGP.volume_mixing_ratio(solver, "h2o"))) # reference vapor, for fixed AH +println("equilibrium Tₛ (1 × CO₂): $(round(Ts_1x; digits = 2)) K") + +# The reference tropopause — the highest layer still on the critical profile — +# sits near 10 km. On that profile, height and temperature are interchangeable, +# ``z = (T_s - T)/Γ``, so the height follows from the temperature of the +# highest adjusted layer, again with no altitude reconstruction: + +function tropopause_height(solver, T_sfc; Γ = Γ_crit) + T = Array(RRTMGP.layer_temperature(solver))[:, 1] + p = Array(RRTMGP.layer_pressure(solver))[:, 1] + p_sfc = Array(RRTMGP.level_pressure(solver))[1, 1] + k = findlast(abs.(T .- T_sfc .* (p ./ p_sfc) .^ (Γ * R_d / g)) .< 0.5) + return (T_sfc - T[k]) / Γ +end +println("tropopause height ≈ $(round(tropopause_height(solver, Ts_1x) / 1000; digits = 1)) km") + +# Now scale the CO₂ and re-balance. `balanced_co2` builds a fresh column with a +# scaled CO₂ concentration, warm-starts it from the 1 × CO₂ equilibrium, and +# finds its balanced surface temperature. With `fixed_rh = false`, the water +# vapor is frozen at its reference (1 × CO₂) values (**fixed absolute +# humidity**) which removes the water-vapor feedback and isolates the direct +# effect of CO₂. + +co2_1x = profile.well_mixed_vmr["co2"] + +function balanced_co2(factor; fixed_rh = true, bracket = (Ts_1x, Ts_1x + 3)) + prof = RRTMGP.standard_atmosphere(FT; kind = :midlatitude_summer, nlay) + prof.well_mixed_vmr["co2"] = factor * co2_1x + slv = RRTMGP.solve(prof; lookups = solver.lookups, setup...).solver + RRTMGP.layer_temperature(slv) .= T_1x # warm start + fixed_rh || (RRTMGP.volume_mixing_ratio(slv, "h2o") .= vmr_1x) # freeze reference vapor + Ts = balanced_surface_temperature!(slv, bracket...; humidity = fixed_rh) + return Ts, copy(Array(RRTMGP.layer_temperature(slv))) +end + +Ts_2x_rh, T_2x_rh = balanced_co2(2) +Ts_4x_rh, T_4x_rh = balanced_co2(4; bracket = (Ts_1x + 3, Ts_1x + 7)) +Ts_2x_ah, T_2x_ah = balanced_co2(2; fixed_rh = false, bracket = (Ts_1x, Ts_1x + 2)) +Ts_4x_ah, T_4x_ah = balanced_co2(4; fixed_rh = false, bracket = (Ts_1x + 1, Ts_1x + 4)); + +# Collect the surface temperatures into a table. Comparing the fixed-RH and +# fixed-AH warming at the same forcing isolates the water-vapor feedback: + +rows = ( + ("reference (1×CO₂, RH)", Ts_1x, nothing), + ("2×CO₂, fixed RH", Ts_2x_rh, Ts_2x_rh - Ts_1x), + ("4×CO₂, fixed RH", Ts_4x_rh, Ts_4x_rh - Ts_1x), + ("2×CO₂, fixed AH", Ts_2x_ah, Ts_2x_ah - Ts_1x), + ("4×CO₂, fixed AH", Ts_4x_ah, Ts_4x_ah - Ts_1x), +) +println(rpad("experiment", 24), lpad("Tₛ [K]", 9), lpad("ΔTₛ [K]", 10)) +for (name, Ts, dT) in rows + dstr = dT === nothing ? "—" : string(round(dT; digits = 1)) + println(rpad(name, 24), lpad(round(Ts; digits = 1), 9), lpad(dstr, 10)) +end + +# With fixed relative humidity and hard adjustment to 6.5 K/km, the equilibrium +# climate sensitivity (the warming per CO₂ doubling) comes out near 2.9 K — +# in the range of the classic calculations: Manabe and Wetherald obtained 2.36 K +# with their radiation scheme, and konrad's re-examination with RRTMG yields +# ≈ 2.1 K ([kluft2019](@citet)). The warming is roughly logarithmic in CO₂, +# growing slightly with each doubling as the warmer column holds more water vapor. +# Freezing the water vapor (fixed AH) roughly halves the warming, so the water-vapor +# feedback about doubles the response. The residual spread among the published +# sensitivities traces to the humidity profile, the adjustment scheme, and the ozone +# treatment; [kluft2021](@citet) dissects these dependencies, which you can reproduce +# by editing a few lines above. +# +# The warmed column shows the classic fingerprint of CO₂-driven change: +# tropospheric warming with stratospheric cooling. This pattern was +# predicted decades before it was observed. Fixed absolute humidity gives +# the same qualitative pattern but a weaker surface warming. + +fig2 = Figure(size = (500, 500)) +ax2 = Axis( + fig2[1, 1]; + xlabel = "temperature [K]", + ylabel = "pressure [hPa]", + yscale = log10, + yreversed = true, + limits = (nothing, nothing, nothing, 1000), + title = "RCE response to CO₂", +) +lines!(ax2, T_1x[:, 1], p_lay[:, 1] ./ 100; label = "1×CO₂ (RH)") +lines!(ax2, T_2x_rh[:, 1], p_lay[:, 1] ./ 100; linestyle = :dash, label = "2×CO₂ (RH)") +lines!(ax2, T_4x_rh[:, 1], p_lay[:, 1] ./ 100; linestyle = :dot, label = "4×CO₂ (RH)") +lines!(ax2, T_4x_ah[:, 1], p_lay[:, 1] ./ 100; linestyle = :dashdot, label = "4×CO₂ (AH)") +axislegend(ax2; position = :rt, framevisible = false, backgroundcolor = :white) +fig2 + +# ## Sensitivity to the critical lapse rate +# +# The assumed critical lapse rate ``Γ_c`` shapes the equilibrium. Re-balancing +# the reference column for lapse rates from a stable moist adiabat (3.0 K/km) to +# the dry adiabat (9.8 K/km) shows that a shallower lapse rate cools the surface +# but lifts the tropopause: the troposphere "stretches" vertically because the +# gentler gradient needs more depth to bridge the surface and stratospheric +# temperatures. All the profiles balance the same absorbed sunlight, so they +# emit the same outgoing longwave radiation to space. + +fig3 = Figure(size = (500, 500)) +ax3 = Axis( + fig3[1, 1]; + xlabel = "temperature [K]", + ylabel = "pressure [hPa]", + yscale = log10, + yreversed = true, + limits = (nothing, nothing, nothing, 1000), + title = "Sensitivity to the critical lapse rate", +) +for Γ_km in (9.8, 6.5, 3.0) + prof = RRTMGP.standard_atmosphere(FT; kind = :midlatitude_summer, nlay) + slv = RRTMGP.solve(prof; lookups = solver.lookups, setup...).solver + Ts = balanced_surface_temperature!(slv, FT(270), FT(292); Γ = Γ_km * 1e-3) + z_top = tropopause_height(slv, Ts; Γ = Γ_km * 1e-3) / 1000 + println("Γ = $(Γ_km) K/km: Tₛ = $(round(Ts; digits = 1)) K, tropopause ≈ $(round(z_top; digits = 1)) km") + T = Array(RRTMGP.layer_temperature(slv)) + lines!(ax3, T[:, 1], p_lay[:, 1] ./ 100; label = "Γ = $(Γ_km) K/km") +end +axislegend(ax3; position = :rt, framevisible = false, backgroundcolor = :white) +fig3 + +# ## Contributions of the individual absorbers +# +# Manabe and Wetherald's other classic diagnostic isolates each greenhouse gas. +# We remove water vapor, carbon dioxide, or ozone one at a time, find the +# column's balanced surface temperature, and compare against the reference +# (all-absorbers) profile computed above. Water vapor is removed by zeroing it +# and holding it there (`humidity = false`); ozone by zeroing its layer profile; +# CO₂ by setting its well-mixed value to zero before the state is built. + +function rce_without(absorber, T1, T2) + prof = RRTMGP.standard_atmosphere(FT; kind = :midlatitude_summer, nlay) + absorber === :co2 && (prof.well_mixed_vmr["co2"] = 0.0) + slv = RRTMGP.solve(prof; lookups = solver.lookups, setup...).solver + absorber === :o3 && (RRTMGP.volume_mixing_ratio(slv, "o3") .= 0) + absorber === :h2o && (RRTMGP.volume_mixing_ratio(slv, "h2o") .= 0) + Ts = balanced_surface_temperature!(slv, T1, T2; humidity = absorber !== :h2o) + return Ts, copy(Array(RRTMGP.layer_temperature(slv))) +end + +## brackets straddle each case's expected surface temperature; the secant +## converges from either side +Ts_no_h2o, T_no_h2o = rce_without(:h2o, FT(255), FT(272)) +Ts_no_co2, T_no_co2 = rce_without(:co2, FT(264), FT(278)) +Ts_no_o3, T_no_o3 = rce_without(:o3, FT(278), FT(288)) + +for (name, Ts) in ( + ("all absorbers", Ts_1x), + ("no H₂O", Ts_no_h2o), + ("no CO₂", Ts_no_co2), + ("no O₃", Ts_no_o3), +) + println("equilibrium Tₛ ($name): $(round(Ts; digits = 1)) K") +end + +# The decomposition reproduces the textbook ordering. Water vapor is the +# largest contributor: removing it drops the surface more than 20 K, below the +# freezing point. Carbon dioxide is the next largest. Ozone affects the surface +# less, but strongly cools the stratosphere it otherwise heats by absorbing +# solar ultraviolet; the mid-atmosphere temperature inversion disappears +# without it. + +fig4 = Figure(size = (500, 500)) +ax4 = Axis( + fig4[1, 1]; + xlabel = "temperature [K]", + ylabel = "pressure [hPa]", + yscale = log10, + yreversed = true, + limits = (nothing, nothing, nothing, 1000), + title = "Contribution of individual absorbers", +) +lines!(ax4, T_1x[:, 1], p_lay[:, 1] ./ 100; label = "all absorbers") +lines!(ax4, T_no_h2o[:, 1], p_lay[:, 1] ./ 100; label = "no H₂O") +lines!(ax4, T_no_co2[:, 1], p_lay[:, 1] ./ 100; label = "no CO₂") +lines!(ax4, T_no_o3[:, 1], p_lay[:, 1] ./ 100; label = "no O₃") +axislegend(ax4; position = :lt, framevisible = false, backgroundcolor = :white) +fig4 + +# ## Where to go from here +# +# - Vary the critical lapse rate or the relative-humidity profile and watch +# the sensitivity respond (cf. [kluft2021](@citet)). +# - Swap `:midlatitude_summer` for `:tropical` or `:subarctic_winter`. diff --git a/perf/benchmark_baselines/nvidia_a100_sxm4_40gb.txt b/perf/benchmark_baselines/nvidia_a100_sxm4_40gb.txt new file mode 100644 index 000000000..96f3cdd44 --- /dev/null +++ b/perf/benchmark_baselines/nvidia_a100_sxm4_40gb.txt @@ -0,0 +1,17 @@ +# RRTMGP DYAMOND GPU benchmark measurement (minimum ns) +# device: NVIDIA A100-SXM4-40GB +# julia: 1.12.6 +# recorded: local run +# regenerate with RRTMGP_BENCH_WRITE_BASELINE=1 +all_sky_lw_Float32 9.76149447e8 +all_sky_lw_Float64 1.331256434e9 +all_sky_sw_Float32 8.43344196e8 +all_sky_sw_Float64 1.097443624e9 +all_sky_with_aerosols_lw_Float32 1.05467407e9 +all_sky_with_aerosols_lw_Float64 1.412722056e9 +all_sky_with_aerosols_sw_Float32 8.94524846e8 +all_sky_with_aerosols_sw_Float64 1.173976877e9 +clear_sky_lw_Float32 7.7122294e8 +clear_sky_lw_Float64 1.049297832e9 +clear_sky_sw_Float32 4.64954059e8 +clear_sky_sw_Float64 6.53718113e8 diff --git a/perf/benchmark_ratchet.jl b/perf/benchmark_ratchet.jl new file mode 100644 index 000000000..ac935c6da --- /dev/null +++ b/perf/benchmark_ratchet.jl @@ -0,0 +1,195 @@ +#= +GPU benchmark ratchet for the DYAMOND-scale solver kernels. + +Measures the minimum wall time of `solve_lw!`/`solve_sw!` at DYAMOND resolution +for the three production configurations (clear sky, all sky, all sky with +aerosols; two-stream longwave and shortwave) in both precisions, and compares +each against a recorded per-GPU baseline: + + perf/benchmark_baselines/.txt + +A measurement slower than baseline × (1 + tolerance) fails its `@test`. The +tolerance (default 20%, chosen for shared-cluster timing noise on the +Buildkite P100s) can be overridden with the `RRTMGP_BENCH_TOL_PCT` environment +variable. + +Baselines: +- If no baseline file exists for this GPU, the script records one and passes; + commit the file (CI uploads it as a Buildkite artifact) to arm the ratchet. +- To re-baseline intentionally (e.g. after an accepted performance change), + run with `RRTMGP_BENCH_WRITE_BASELINE=1` and commit the updated file. + +Usage (needs a CUDA device): + + CLIMACOMMS_DEVICE=CUDA julia --project=test perf/benchmark_ratchet.jl + +This lane is advisory (soft-fail in Buildkite): it flags regressions loudly +without blocking merges, per the CliMA convention that `perf/` is not a gate. +=# + +using Test +using Statistics +using Printf +import ClimaComms +@static pkgversion(ClimaComms) >= v"0.6" && ClimaComms.@import_required_backends + +const context = ClimaComms.context() +const device = ClimaComms.device(context) + +if !(device isa ClimaComms.CUDADevice) + @info "benchmark_ratchet.jl requires a CUDA device; nothing to do." device + exit(0) +end + +using CUDA + +# The high-resolution DYAMOND target column count used by the benchmark scripts +# (30² horizontal elements × 6 panels × 4² quadrature points across 64 levels / 63 layers). +dyamond_ncol(; helems = 30, nq = 4) = Int(helems * helems * 6 * nq * nq) +const dyamond_nlay = 63 + +# Pull in benchmark_clear_sky / benchmark_all_sky / +# benchmark_all_sky_with_aerosols; their sweep loops are guarded behind +# `PROGRAM_FILE == @__FILE__`, so the includes only define functions. +const test_dir = joinpath(dirname(@__DIR__), "test") +include(joinpath(test_dir, "clear_sky_dyamond_gpu_benchmark.jl")) +include(joinpath(test_dir, "cloudy_sky_dyamond_gpu_benchmark.jl")) +include(joinpath(test_dir, "all_sky_with_aerosols_dyamond_gpu_benchmark.jl")) + +# One measured case: production two-stream solvers across identical uniform DYAMOND grids. +# Each `run` returns (trial_lw, trial_sw) from BenchmarkTools. +cases = [ + ( + key = "clear_sky", + run = FT -> benchmark_clear_sky( + context, + TwoStreamLWRTE, + TwoStreamSWRTE, + VmrGM, + FT; + ncol = dyamond_ncol(), + nlay = dyamond_nlay, + ), + ), + ( + key = "all_sky", + run = FT -> benchmark_all_sky( + context, + TwoStreamLWRTE, + TwoStreamSWRTE, + FT; + ncol = dyamond_ncol(), + nlay = dyamond_nlay, + cldfrac = FT(1), + ), + ), + ( + key = "all_sky_with_aerosols", + run = FT -> benchmark_all_sky_with_aerosols( + context, + TwoStreamLWRTE, + TwoStreamSWRTE, + FT; + ncol = dyamond_ncol(), + nlay = dyamond_nlay, + ), + ), +] + +device_key() = + replace(lowercase(CUDA.name(CUDA.device())), r"[^a-z0-9]+" => "_") + +baseline_path(key) = + joinpath(@__DIR__, "benchmark_baselines", string(key, ".txt")) + +# Baseline file format: comment lines start with '#'; data lines are two +# whitespace-separated tokens, "__ ". The key is a +# single token (no embedded spaces) so the round-trip is unambiguous. +function read_baseline(path) + baseline = Dict{String, Float64}() + isfile(path) || return nothing + for line in eachline(path) + line = strip(line) + (isempty(line) || startswith(line, "#")) && continue + name, ns = split(line) + baseline[name] = parse(Float64, ns) + end + return baseline +end + +function write_baseline(path, results; kind = "baseline") + mkpath(dirname(path)) + open(path, "w") do io + println(io, "# RRTMGP DYAMOND GPU benchmark $kind (minimum ns)") + println(io, "# device: $(CUDA.name(CUDA.device()))") + println(io, "# julia: $(VERSION)") + println(io, "# recorded: ", get(ENV, "BUILDKITE_BUILD_URL", "local run")) + println(io, "# regenerate with RRTMGP_BENCH_WRITE_BASELINE=1") + for (name, ns) in sort(collect(results); by = first) + println(io, "$name $ns") + end + end + return path +end + +const tol = parse(Float64, get(ENV, "RRTMGP_BENCH_TOL_PCT", "20")) / 100 +const write_mode = get(ENV, "RRTMGP_BENCH_WRITE_BASELINE", "0") == "1" +const path = baseline_path(device_key()) +const baseline = read_baseline(path) + +results = Dict{String, Float64}() +for FT in (Float32, Float64), case in cases + @info "benchmark_ratchet: running $(case.key) ($FT) at DYAMOND resolution" + trial_lw, trial_sw = case.run(FT) + # `minimum` is the recommended timing statistic: it is the most resilient to + # GC/scheduler noise on a shared cluster (CliMA DeveloperGuides, + # performance/allocation_debugging.md §5), which also makes the ratchet + # threshold reliable. + results[string(case.key, "_lw_", FT)] = minimum(trial_lw).time # ns + results[string(case.key, "_sw_", FT)] = minimum(trial_sw).time # ns +end + +# Always persist the measured minima as an artifact, whatever the mode. +const measured_path = + joinpath(@__DIR__, "benchmark_baselines", "measured_$(device_key()).txt") +write_baseline(measured_path, results; kind = "measurement") + +println("\ndevice: $(CUDA.name(CUDA.device())) (key: $(device_key()))") +println("tolerance: +$(round(Int, 100 * tol))% over baseline\n") +@printf( + "%-40s %14s %14s %9s\n", + "case", + "minimum [ms]", + "baseline [ms]", + "ratio" +) +for (name, ns) in sort(collect(results); by = first) + base = isnothing(baseline) ? missing : get(baseline, name, missing) + @printf( + "%-40s %14.3f %14s %9s\n", + name, + ns / 1e6, + ismissing(base) ? "—" : @sprintf("%.3f", base / 1e6), + ismissing(base) ? "—" : @sprintf("%.2fx", ns / base), + ) +end + +if write_mode || isnothing(baseline) + write_baseline(path, results) + action = write_mode ? "rewrote" : "no baseline found for this GPU; recorded" + @info "benchmark_ratchet: $action $path — commit it to arm the ratchet." + println("\n--- baseline file contents ---") + println(read(path, String)) +else + @testset "GPU benchmark ratchet ($(device_key()))" begin + for (name, ns) in sort(collect(results); by = first) + base = get(baseline, name, missing) + if ismissing(base) + @warn "no baseline entry for $name; rerun with " * + "RRTMGP_BENCH_WRITE_BASELINE=1 to record it" + continue + end + @test ns <= base * (1 + tol) + end + end +end diff --git a/src/ArtifactPaths.jl b/src/ArtifactPaths.jl index 484f931a6..3699524ec 100644 --- a/src/ArtifactPaths.jl +++ b/src/ArtifactPaths.jl @@ -8,7 +8,7 @@ export get_lookup_filename, get_input_filename """ get_lookup_filename(optics_type::Symbol, λ::Symbol) -This function generates the file names for lookup table files, for a given optics type, for the +Generate the file names for lookup table files, for a given optics type, for the shortwave and longwave solvers. - `:gas`, `:cloud` and `:aerosol` optics types are supported for the longwave and shortwave solvers. @@ -45,7 +45,7 @@ end """ get_input_filename(problemtype::Symbol, λ::Symbol) -This function generates the file names for input files for tests for a given problem type and wavelength type. +Generate the file names for input files for tests, for a given problem type and wavelength type. `:gas`, and `:gas_clouds` and `:gas_clouds_aerosols` problem types are supported. `:lw` (longwave) and `:sw` (shortwave) wavelength types are supported. @@ -55,7 +55,7 @@ This file provides data for loading the `AtmosphericState` struct, the provides - The `:gas_clouds` option is used for the `all sky` test. - The `:gas_clouds_aerosols` option is used for the `all sky with aerosols` test. -While these are primarily intended for the tests, some of this input data is also used in `ClimaAtmos.jl` and is therefore provided here for user's convenience. +While these are primarily intended for the tests, some of this input data is also used in `ClimaAtmos.jl` and is therefore provided here for users' convenience. These artifacts are obtained from "Pincus, R., Mlawer, E. J., Delamere, J., Iacono, M. J., & Pernak, R. (2023). RRTMGP data (Version 1.7) [Data set]. https://github.com/earth-system-radiation/rrtmgp-data" """ diff --git a/src/api/aerosols.jl b/src/api/aerosols.jl index 471f0b64d..91cfb1361 100644 --- a/src/api/aerosols.jl +++ b/src/api/aerosols.jl @@ -3,7 +3,7 @@ ##### # # This is the single source of truth for the order of aerosol species in the -# `AerosolState` arrays (`aero_mass`, `aero_size`), i.e. the meaning of the +# `AerosolState` arrays (`aero_mass`, `aero_size`), i.e., the meaning of the # first index of those arrays. It MUST agree with: # # - the integer indices hard-coded by the aerosol optics kernel diff --git a/src/api/atmosphere_profile.jl b/src/api/atmosphere_profile.jl index 51636e630..acd33e3c3 100644 --- a/src/api/atmosphere_profile.jl +++ b/src/api/atmosphere_profile.jl @@ -11,8 +11,8 @@ consumed by [`solve`](@ref). All arrays are plain `Array{FT}` on the host; [`solve`](@ref) moves them to the compute device. # Fields -- `p_lay`, `t_lay`: layer-center pressures [Pa] and temperatures [K], `(nlay, ncol)`. -- `p_lev`, `t_lev`: level pressures [Pa] and temperatures [K], `(nlay + 1, ncol)`. +- `p_lay`, `p_lev`: layer-center and level pressures [Pa], `(nlay, ncol)` and `(nlay + 1, ncol)`. +- `t_lay`, `t_lev`: layer-center and level temperatures [K], `(nlay, ncol)` and `(nlay + 1, ncol)`. - `z_lev`: level altitudes [m], `(nlay + 1, ncol)`. - `t_sfc`: surface temperature [K], `(ncol,)`. - `lat`: latitude [degrees], `(ncol,)`. @@ -38,7 +38,9 @@ end # [m], tropospheric lapse rate [K/m], stratospheric inverse lapse rate [K/m], # surface water-vapor vmr, default latitude [degrees]. # The values are idealized climatological choices (inspired by the AFGL -# reference-atmosphere climatology, but analytic rather than tabulated). +# reference-atmosphere climatology of Anderson et al. 1986, AFGL-TR-86-0110, +# https://apps.dtic.mil/sti/citations/ADA175173, but analytic rather than +# tabulated). const _STANDARD_ATMOSPHERES = Dict( :tropical => (; t_sfc = 300.0, @@ -112,8 +114,7 @@ levels uniformly spaced in altitude from the surface to `z_top` [m]: `kind` selects the idealized climatology: `:tropical`, `:midlatitude_summer` (default), or `:subarctic_winter`. The profiles are -analytic and idealized — made for teaching and testing, not for reproducing -tabulated reference atmospheres. All `ncol` columns are identical. +analytic and idealized (made for teaching and testing). All `ncol` columns are identical. """ function standard_atmosphere( ::Type{FT}; diff --git a/src/api/getters.jl b/src/api/getters.jl index 044c2b2fc..f059e9b2d 100644 --- a/src/api/getters.jl +++ b/src/api/getters.jl @@ -6,8 +6,8 @@ # domain-masked `view` into a solver-owned buffer (physical dimension first, `ncol` last), so # a host reads and writes RRTMGP's buffers without touching the packed layout -- which is how # RRTMGP stays free of ClimaCore. The full contract (layout, boundary-layer masking, -# writability, and the two deliberate exceptions: the well-mixed `volume_mixing_ratio` scalar -# and the allocating `spectral_*_flux_net`) is documented in `docs/src/getters.md`. +# writability, and the deliberate exceptions: the well-mixed `volume_mixing_ratio` scalar +# and the allocating `heating_rate`) is documented in `docs/src/getters.md`. import ..AtmosphericStates: getview_p_lay, getview_t_lay, getview_rel_hum import ..VolumeMixingRatios @@ -113,7 +113,7 @@ function _require_spectral_lookups(s::RRTMGPSolver) lookups = _lookup_tables(s) isnothing(lookups.lookup_lw) && error( "spectral lookup tables are not present on this solver (gray radiation); \ - construct it with a spectral radiation method (e.g. `ClearSkyRadiation`).", + construct it with a spectral radiation method (e.g., `ClearSkyRadiation`).", ) return lookups end @@ -147,10 +147,10 @@ _solver_band_flux(ws) = Return the spectrally-resolved (per-band) up/down/net longwave or shortwave flux [W/m²] as a domain-masked `(nlev, ncol, n_bnd)` array. The solver must have been constructed with -`spectral_fluxes = true` (otherwise an informative error is raised); this is supported only -for two-stream, non-gray radiation. Band `b`'s slice `[:, :, b]` has the same layout as the -broadband fluxes, and summing over the band dimension recovers them. Use -[`lw_band_bounds`](@ref) / [`sw_band_bounds`](@ref) to identify each band's wavenumber range. +`spectral_fluxes = true` (supported for two-stream, non-gray radiation). Band `b`'s slice +`[:, :, b]` has the same layout as the broadband fluxes, and summing over the band dimension +recovers them. Use [`lw_band_bounds`](@ref) / [`sw_band_bounds`](@ref) to identify each band's +wavenumber range. The `up`/`dn`/`net` getters are views into the retained per-band buffers. """ @@ -171,7 +171,7 @@ spectral_sw_flux_net(s::RRTMGPSolver) = lw_band_bounds(s::RRTMGPSolver) Return the `(2, n_bnd)` lower/upper wavenumber edges [cm⁻¹] of the longwave bands, -identifying the spectral range of each band in the per-band fluxes (e.g. +identifying the spectral range of each band in the per-band fluxes (e.g., [`spectral_lw_flux_up`](@ref)). """ lw_band_bounds(s::RRTMGPSolver) = @@ -181,8 +181,9 @@ lw_band_bounds(s::RRTMGPSolver) = sw_band_bounds(s::RRTMGPSolver) Return the `(2, n_bnd)` lower/upper wavenumber edges [cm⁻¹] of the shortwave bands, -identifying the spectral range of each band in the per-band fluxes (e.g. -[`spectral_sw_flux_up`](@ref RRTMGP.spectral_lw_flux_up)). +identifying the spectral range of each band in +[the per-band fluxes](@ref RRTMGP.spectral_lw_flux_up) (`spectral_sw_flux_up` +and companions). """ sw_band_bounds(s::RRTMGPSolver) = _require_spectral_lookups(s).lookup_sw.band_data.bnd_lims_wn @@ -191,7 +192,7 @@ sw_band_bounds(s::RRTMGPSolver) = optical_thickness_parameter(s::RRTMGPSolver) For a gray-radiation solver, return the gray optical-thickness parameters (an -`AbstractGrayOpticalThickness`, e.g. `GrayOpticalThicknessOGorman2008`) the atmospheric +`AbstractGrayOpticalThickness`, e.g., `GrayOpticalThicknessOGorman2008`) the atmospheric state was built with; `nothing` for lookup-table (non-gray) radiation, which has no such parameter. """ @@ -201,7 +202,7 @@ optical_thickness_parameter(s::RRTMGPSolver) = isothermal_boundary_layer(s::RRTMGPSolver) = s.grid_params.isothermal_boundary_layer """ - aerosol_radius(s::RRTMGPSolver, name::String) + aerosol_radius(s::RRTMGPSolver, name::AbstractString) Return the aerosol radius for the given aerosol name. @@ -219,7 +220,7 @@ function aerosol_radius(s::RRTMGPSolver, name::AbstractString) end """ - aerosol_column_mass_density(s::RRTMGPSolver, name::String) + aerosol_column_mass_density(s::RRTMGPSolver, name::AbstractString) Return the aerosol column mass density [kg/m²] for the given aerosol name. @@ -286,9 +287,8 @@ Return the volume mixing ratio for gas `name`. `"h2o"` and `"o3"` vary by layer and column (a domain-masked `(nlay, ncol)` view); with the global-mean `VmrGM` storage every other (well-mixed) gas is a single scalar. -Unlike the array getters, a well-mixed scalar is returned as a **host `Number` -copied off the device** (not a device view), so each call triggers a device→host -synchronization on the GPU. Read these once at setup, not inside a timestep loop. +A well-mixed scalar is returned as a host `Number` copied off the device, so each call triggers +a device→host synchronization on the GPU. Read these during setup to maintain performance. Available names are: $(gas_names_sw_docs()) diff --git a/src/api/grid_adaptation.jl b/src/api/grid_adaptation.jl index 83cf6e3e6..3f874a1c2 100644 --- a/src/api/grid_adaptation.jl +++ b/src/api/grid_adaptation.jl @@ -256,7 +256,7 @@ place and returns it. This updates only the dry-air column amount (`compute_col_gas!`); it does **not** recompute relative humidity. Relative humidity is a host responsibility: a caller that -needs an up-to-date `layer_relative_humidity` (e.g. for RH-dependent aerosol optics) must +needs an up-to-date `layer_relative_humidity` (e.g., for RH-dependent aerosol optics) must call `compute_relative_humidity!` itself after updating temperature, pressure, or humidity. """ update_concentrations!( diff --git a/src/api/lookup_bundle.jl b/src/api/lookup_bundle.jl index bc6c8a5f4..1c36e0bd7 100644 --- a/src/api/lookup_bundle.jl +++ b/src/api/lookup_bundle.jl @@ -14,7 +14,7 @@ are `nothing`), the gas and aerosol name→index maps, and the band/gas counts. Built by [`lookup_tables`](@ref); pass a prebuilt bundle back to the `RRTMGPSolver` constructor via `lookups = ...` to avoid a second NetCDF read, and use [`save_lookup_tables`](@ref)/[`load_lookup_tables`](@ref) to cache it -on disk (e.g. for standalone use without NCDatasets). +on disk (e.g., for standalone use without NCDatasets). # Fields - `lookup_lw`, `lookup_sw`: gas-optics lookup tables (`nothing` for gray). @@ -88,11 +88,11 @@ end save_lookup_tables(path, lookups::LookupBundle) Serialize `lookups` to `path` (host-side copies of any device arrays), so a -later session can [`load_lookup_tables`](@ref) without NCDatasets — e.g. for -standalone/classroom use of the spectral methods, or to skip the NetCDF read. +later session can [`load_lookup_tables`](@ref) without NCDatasets (e.g., for +standalone/classroom use of the spectral methods, or to skip the NetCDF read). Uses Julia's `Serialization` stdlib: the file is tied to the Julia version and -package layout that wrote it and is a *cache*, not an interchange format — the -NetCDF artifacts remain the source of truth. Returns `path`. +package layout that wrote it and serves as a cache; the NetCDF artifacts remain +the source of truth. Returns `path`. """ function save_lookup_tables(path::AbstractString, lookups::LookupBundle) Serialization.serialize(path, Adapt.adapt(Array, lookups)) diff --git a/src/api/radiation_methods.jl b/src/api/radiation_methods.jl index 0e5a1be41..9d246e35b 100644 --- a/src/api/radiation_methods.jl +++ b/src/api/radiation_methods.jl @@ -55,7 +55,7 @@ end AllSkyRadiationWithClearSkyDiagnostics(aerosol_radiation::Bool, reset_rng_seed::Bool) Like [`AllSkyRadiation`](@ref), but each call also runs a parallel **cloud-free** (clear-sky) -solve. Those fluxes are exposed through the `clear_*` getters (e.g. `clear_net_flux`, +solve. Those fluxes are exposed through the `clear_*` getters (e.g., `clear_net_flux`, `clear_lw_flux_up`); differenced against the all-sky fluxes they give the cloud radiative effect. Requires the lookup tables (load `NCDatasets`). diff --git a/src/api/solver.jl b/src/api/solver.jl index d0ae2aa9c..3abe81d76 100644 --- a/src/api/solver.jl +++ b/src/api/solver.jl @@ -12,14 +12,13 @@ import Random """ lookup_tables(grid_params::RRTMGPGridParams, radiation_method::AbstractRRTMGPMethod) -Build the lookup tables for `radiation_method`, returning a -[`LookupBundle`](@ref) — the gas/cloud/aerosol lookup tables, the name→index -maps, and the band/gas counts. Build it once and pass it back to the -`RRTMGPSolver` constructor via `lookups = ...` to avoid a second NetCDF read, +Build the lookup tables for `radiation_method`, returning a [`LookupBundle`](@ref) containing +the gas/cloud/aerosol lookup tables, the name→index maps, and the band/gas counts. Build it +once and pass it back to the `RRTMGPSolver` constructor via `lookups = ...` to reuse the tables, or cache it on disk with [`save_lookup_tables`](@ref). -The spectral (non-gray) methods are provided by an extension: load NCDatasets -(`using NCDatasets`) first (or [`load_lookup_tables`](@ref) from a cache). +The spectral methods are provided by an extension: load NCDatasets (`using NCDatasets`) first +(or use [`load_lookup_tables`](@ref) from a cache). """ function lookup_tables end @@ -47,7 +46,7 @@ end Aggregate bundling everything needed to run an RRTMGP radiation calculation. It holds the radiation configuration, the atmospheric state, the longwave and shortwave solvers, the lookup tables, and the output flux buffers, and exposes -getter methods (e.g. `layer_temperature`, `net_flux`) to read and write its data. +getter methods (e.g., `layer_temperature`, `net_flux`) to read and write its data. Construct it with the `RRTMGPSolver` constructor and drive it with `update_fluxes!`. @@ -60,14 +59,12 @@ Construct it with the `RRTMGPSolver` constructor and drive it with - `sws`: shortwave RTE solver and its flux/scratch buffers. - `lws`: longwave RTE solver and its flux/scratch buffers. - `as`: the atmospheric state (solver inputs). -- `lookups`: the [`LookupBundle`](@ref) from `lookup_tables` (for gray - radiation the tables are `nothing` and only the band counts are carried). +- `lookups`: the [`LookupBundle`](@ref) from `lookup_tables` (for gray radiation only the band counts are carried). - `clear_flux_lw`: clear-sky longwave fluxes, or `nothing`. - `clear_flux_sw`: clear-sky shortwave fluxes, or `nothing`. - `center_z`: layer-center altitudes [m], or `nothing`. - `face_z`: level (face) altitudes [m], or `nothing`. -- `deep_atmosphere_inverse_scaling`: `(nlev, ncol)` factor multiplied directly into the - fluxes for deep-atmosphere geometric scaling, or `nothing`. +- `deep_atmosphere_inverse_scaling`: `(nlev, ncol)` factor multiplied into the fluxes for deep-atmosphere geometric scaling (the host supplies the multiplicative inverse of its metric scaling), or `nothing` (default) for the shallow-atmosphere approximation. - `net_flux_buffer`: combined longwave + shortwave net flux at each level [W/m²], the full boundary-extended `(nlev, ncol)` buffer (read the domain-masked view via `net_flux(s)`). - `clear_net_flux_buffer`: combined clear-sky net-flux buffer, or `nothing`. @@ -82,14 +79,9 @@ and shortwave boundary conditions, and the atmospheric state. Keyword arguments: interpolation; default `nothing`. - `interpolation`: scheme for filling level values from layer values; default `NoInterpolation`. - `bottom_extrapolation`: scheme for the bottom-level value; default `SameAsInterpolation`. -- `deep_atmosphere_inverse_scaling`: a `(nlev, ncol)` array multiplied directly into the - fluxes for deep-atmosphere geometric scaling — the host supplies the multiplicative - inverse of its metric scaling, hence the name — or `nothing` (default) for the - shallow-atmosphere approximation. -- `lookups`: prebuilt lookup tables to reuse (avoids a second NetCDF read), or `nothing` - (default) to build them internally. -- `spectral_fluxes`: if `true`, also retain per-band fluxes (two-stream, non-gray only); - default `false`. +- `deep_atmosphere_inverse_scaling`: a `(nlev, ncol)` array multiplied into the fluxes for deep-atmosphere geometric scaling (the host supplies the multiplicative inverse of its metric scaling), or `nothing` (default) for the shallow-atmosphere approximation. +- `lookups`: prebuilt lookup tables to reuse, or `nothing` (default) to build them internally. +- `spectral_fluxes`: if `true`, also retain per-band fluxes (two-stream, non-gray only); default `false`. """ struct RRTMGPSolver{ S, diff --git a/src/api/standalone.jl b/src/api/standalone.jl index 194cb45d2..f05423395 100644 --- a/src/api/standalone.jl +++ b/src/api/standalone.jl @@ -17,10 +17,9 @@ using ClimaComms The result of a standalone radiation solve ([`solve`](@ref) or [`solve_gray`](@ref)): the broadband fluxes, the heating rate, and the -underlying solver. The field names are stable — teaching material written -against them does not depend on the getter API. The flux fields are views into -the solver's buffers (they change if the solver is re-solved); `heating_rate` -is a freshly allocated array. +underlying solver. The field names are stable, so teaching material written +against them remains decoupled from the getter API. The flux fields are views into +the solver's buffers; `heating_rate` is a freshly allocated array. # Fields - `lw_up`, `lw_dn`, `lw_net`: longwave up/down/net flux [W/m²], `(nlev, ncol)`. @@ -191,10 +190,9 @@ end """ solve(profile::AtmosphereProfile; method = ClearSkyRadiation(false), kwargs...) -Solve the radiative-transfer problem for an [`AtmosphereProfile`](@ref) in a -single call and return a [`RadiationOutput`](@ref). Builds the atmospheric -state, boundary conditions, and an `RRTMGPSolver` internally, then runs -[`update_fluxes!`](@ref) — the one-line standalone path: +Solve the radiative-transfer problem for an [`AtmosphereProfile`](@ref) and return a +[`RadiationOutput`](@ref). Builds the atmospheric state, boundary conditions, and an +`RRTMGPSolver` internally, then runs [`update_fluxes!`](@ref): ```julia using RRTMGP, NCDatasets @@ -204,11 +202,9 @@ out.net # net flux at each level [W/m²] out.heating_rate # heating rate at each layer [K/s] ``` -`method` selects the radiation model: `ClearSkyRadiation(false)` (the default; -requires lookup tables, so load NCDatasets first or pass cached `lookups`) or -`GrayRadiation()` (no lookup tables — runs after a bare `using RRTMGP`; the -profile's mixing ratios are ignored). Cloud and aerosol methods are not -supported here — construct an [`RRTMGPSolver`](@ref) directly for those. +`method` selects the radiation model: `ClearSkyRadiation(false)` (the default; requires lookup +tables, so load NCDatasets first or pass cached `lookups`) or `GrayRadiation()` (runs after +`using RRTMGP`). For cloud and aerosol methods, construct an [`RRTMGPSolver`](@ref). # Keyword Arguments - `method = ClearSkyRadiation(false)`: the radiation method (see above). @@ -223,6 +219,13 @@ supported here — construct an [`RRTMGPSolver`](@ref) directly for those. - `surface_albedo = 0.2`: shortwave surface albedo [-]. - `optical_thickness = GrayOpticalThicknessOGorman2008(FT)`: gray optical-thickness parameters (`GrayRadiation` only). +- `interpolation = NoInterpolation()`: how [`update_fluxes!`](@ref) rebuilds + the level (cell-face) pressures and temperatures from the layer values on + each call (see [`AbstractInterpolation`](@ref)); the default uses the + profile's level values as given. Useful when a driver marches the layer + temperatures and wants the faces kept consistent automatically. +- `bottom_extrapolation = SameAsInterpolation()`: scheme for the bottom face + (see [`AbstractBottomExtrapolation`](@ref)). """ function solve( profile::AtmosphereProfile; @@ -237,6 +240,8 @@ function solve( optical_thickness = AtmosphericStates.GrayOpticalThicknessOGorman2008( eltype(profile.p_lay), ), + interpolation::AbstractInterpolation = NoInterpolation(), + bottom_extrapolation::AbstractBottomExtrapolation = SameAsInterpolation(), ) FT = eltype(profile.p_lay) (nlay, ncol) = size(profile.p_lay) @@ -321,8 +326,17 @@ function solve( alb_dif = fill!(DA{FT}(undef, lookups.nbnd_sw, ncol), FT(surface_albedo)) bcs_sw = BCs.SwBCs(cos_zen, toa, alb_dir, nothing, alb_dif) - solver = - RRTMGPSolver(grid_params, method, params, bcs_lw, bcs_sw, as; lookups) + solver = RRTMGPSolver( + grid_params, + method, + params, + bcs_lw, + bcs_sw, + as; + lookups, + interpolation, + bottom_extrapolation, + ) update_fluxes!(solver) return _radiation_output(solver) end diff --git a/src/api/update_fluxes.jl b/src/api/update_fluxes.jl index e3d191300..d216fe819 100644 --- a/src/api/update_fluxes.jl +++ b/src/api/update_fluxes.jl @@ -5,7 +5,7 @@ """ update_lw_fluxes!(s::RRTMGPSolver) -Updates the longwave fluxes. +Update the longwave fluxes. """ update_lw_fluxes!(s::RRTMGPSolver) = update_lw_fluxes!(s, _radiation_method(s)) @@ -62,7 +62,7 @@ end """ update_sw_fluxes!(s::RRTMGPSolver) -Updates the shortwave fluxes. +Update the shortwave fluxes. """ update_sw_fluxes!(s::RRTMGPSolver) = update_sw_fluxes!(s, _radiation_method(s)) @@ -185,12 +185,12 @@ end """ update_fluxes!(s::RRTMGPSolver, seedval = nothing) -Run the full radiation update: prepare the atmospheric state (interpolate levels, +Run the radiation update: prepare the atmospheric state (interpolate levels, add the isothermal boundary layer, clip pressures/temperatures/humidity to the range the optics support, and compute concentrations), solve the longwave and shortwave problems (applying `deep_atmosphere_inverse_scaling` if present), and -combine them into the net flux. Mutates `s` in place — its atmospheric state and -flux buffers — and returns `nothing` (read results via `net_flux(s)` and the +combine them into the net flux. Mutates `s` in place (its atmospheric state and +flux buffers) and returns `nothing` (read results via `net_flux(s)` and the other flux getters). When the radiation method requests reproducible seeding, `seedval` reseeds the RNG used for cloud sampling. @@ -226,11 +226,10 @@ vapor, and — for non-gray optics — temperatures outside the lookup tables' range), and compute the dry-air column amounts. Mutates the solver's atmospheric state in place and returns `nothing`. -[`update_fluxes!`](@ref) calls this before solving; call it directly to inspect +[`update_fluxes!`](@ref) calls this before solving; call it to inspect the prepared state (interpolated level values, column amounts) without running -the radiative transfer — the prepare/solve split familiar from other radiation -drivers. Relative humidity is *not* recomputed here (a host responsibility; see -[`update_concentrations!`](@ref)). +the radiative transfer (the prepare/solve split familiar from other radiation +drivers). Relative humidity is managed by the host (see [`update_concentrations!`](@ref)). """ function prepare_atmosphere!(s::RRTMGPSolver) as = _atmospheric_state(s) diff --git a/src/api/validation.jl b/src/api/validation.jl index b3562dbdf..452738672 100644 --- a/src/api/validation.jl +++ b/src/api/validation.jl @@ -8,10 +8,9 @@ Global toggle for input validation: when enabled with `RRTMGP.check_values[] = true`, [`update_fluxes!`](@ref) calls -[`validate_inputs`](@ref) before each solve. Off by default — the checks -reduce over device arrays, so they are intended for development and debugging, -not for production time-stepping (with the toggle off they cost one branch and -keep `update_fluxes!` allocation-free). +[`validate_inputs`](@ref) before each solve. Off by default; the checks +reduce over device arrays, so they are intended for development and debugging. +With the toggle off, `update_fluxes!` remains allocation-free. """ const check_values = Ref(false) diff --git a/src/optics/AngularDiscretizations.jl b/src/optics/AngularDiscretizations.jl index fcca5af97..715d31ed4 100644 --- a/src/optics/AngularDiscretizations.jl +++ b/src/optics/AngularDiscretizations.jl @@ -1,6 +1,5 @@ module AngularDiscretizations -using DocStringExtensions using Adapt using ClimaComms import ..RRTMGPGridParams @@ -8,20 +7,19 @@ import ..RRTMGPGridParams export AngularDiscretization """ - AngularDiscretization{FT,FTA1D} + AngularDiscretization{FT, FTA1D} Weights and angle secants for "Gauss-Jacobi-5" quadrature. -Values from Table 1, R. J. Hogan 2023, doi:10.1002/qj.4598 +Values from Table 1 of R. J. Hogan (2024), doi:10.1002/qj.4598. # Fields -$(DocStringExtensions.FIELDS) +- `n_gauss_angles`: Number of quadrature angles. +- `gauss_Ds`: Quadrature secants (secant of propagation angle). +- `gauss_wts`: Quadrature weights. """ struct AngularDiscretization{FT <: AbstractFloat, FTA1D <: AbstractArray{FT, 1}} - "number of quadrature angles" n_gauss_angles::Int - "quadrature secants / secant of propagation angle" gauss_Ds::FTA1D - "quadrature weights" gauss_wts::FTA1D end Adapt.@adapt_structure AngularDiscretization diff --git a/src/optics/AtmosphericStates.jl b/src/optics/AtmosphericStates.jl index e75aedc1a..d31bf3d63 100644 --- a/src/optics/AtmosphericStates.jl +++ b/src/optics/AtmosphericStates.jl @@ -1,6 +1,5 @@ module AtmosphericStates import ClimaComms -using DocStringExtensions using Adapt import ..Parameters as RP @@ -18,43 +17,64 @@ export AbstractAtmosphericState, GrayOpticalThicknessOGorman2008, AbstractGrayOpticalThickness +""" + AbstractAtmosphericState + +Abstract type for atmospheric states: [`AtmosphericState`](@ref) for the +spectral (correlated-``k``) methods and [`GrayAtmosphericState`](@ref) for +gray radiation. +""" abstract type AbstractAtmosphericState end include("gray_atmospheric_states.jl") +""" + AbstractCloudMask + +Abstract type for cloud-overlap sampling masks; see [`MaxRandomOverlap`](@ref). +""" abstract type AbstractCloudMask end +""" + MaxRandomOverlap + +Maximum-random cloud overlap for McICA sampling: clouds in adjacent layers +overlap maximally, and clouds separated by clear sky overlap randomly. Used +by `build_cloud_mask!` to sample the g-point cloud masks from the cloud +fraction. +""" struct MaxRandomOverlap <: AbstractCloudMask end Adapt.@adapt_structure MaxRandomOverlap """ - AtmosphericState{FTA1D,FTA1DN,FTA2D,CLDP,CLDM,VMR} <: + AtmosphericState{FTA1D, FTA1DN, FTA2D, D, VMR, CLD, AER} <: AbstractAtmosphericState -Atmospheric conditions, used to compute optical properties. +Atmospheric conditions, used to compute optical properties. # Fields -$(DocStringExtensions.FIELDS) +- `lon`: Longitude in degrees `(ncol)`; optional. +- `lat`: Latitude in degrees `(ncol)`; optional. +- `layerdata`: Storage for `col_dry` (column amount of dry air [molecules/cm²]), + layer pressures [Pa, mb], layer temperatures [K], and relative humidity; + `(4, nlay, ncol)`. +- `p_lev`: Level pressures [Pa, mb] `(nlay+1, ncol)`. +- `t_lev`: Level temperatures [K] `(nlay+1, ncol)`. +- `t_sfc`: Surface temperatures [K] `(ncol)`. +- `vmr`: Volume mixing ratios of all relevant gases. +- `cloud_state`: Cloud state. +- `aerosol_state`: Aerosol state. """ struct AtmosphericState{FTA1D, FTA1DN, FTA2D, D, VMR, CLD, AER} <: AbstractAtmosphericState - "longitude, in degrees (`ncol`), optional" lon::FTA1DN - "latitude, in degrees (`ncol`), optional" lat::FTA1DN - "storage for col_dry [`molecules per cm^2 of dry air`], play `[Pa, mb]`, tlay `[K]`, rel_hum; `(4, nlay, ncol)`" layerdata::D - "Level pressures `[Pa, mb]`; `(nlay+1,ncol)`" p_lev::FTA2D - "Level temperatures `[K]`; `(nlay+1,ncol)`" t_lev::FTA2D - "Surface temperatures `[K]`; `(ncol)`" t_sfc::FTA1D - "volume mixing ratio of all relevant gases" vmr::VMR - "cloud state" cloud_state::CLD - "aerosol state" aerosol_state::AER end Adapt.@adapt_structure AtmosphericState @@ -98,29 +118,33 @@ Adapt.@adapt_structure AtmosphericState CloudState{CD, CF, CC, CM, CMT} Cloud state, used to compute optical properties. + +# Fields +- `cld_r_eff_liq`: Effective radius of cloud liquid particles. +- `cld_r_eff_ice`: Effective radius of cloud ice particles. +- `cld_path_liq`: Cloud water path. +- `cld_path_ice`: Cloud ice path. +- `cld_frac`: Cloud fraction. +- `cld_cover_sw`: McICA effective shortwave cloud cover in `[0, 1]`; `(ncol,)`, or + `nothing` if unused. +- `cld_cover_lw`: McICA effective longwave cloud cover in `[0, 1]`; `(ncol,)`, or + `nothing` if unused. +- `mask_lw`: Cloud mask (longwave); `true` if clouds are present. +- `mask_sw`: Cloud mask (shortwave); `true` if clouds are present. +- `mask_type`: Cloud mask type. +- `ice_rgh`: Ice roughness; 1 = none, 2 = medium, 3 = rough. """ struct CloudState{CD, CF, CC, CM, CMT} - "effective radius of cloud liquid particles" cld_r_eff_liq::CD - "effective radius of cloud ice particles" cld_r_eff_ice::CD - "cloud water path" cld_path_liq::CD - "cloud ice path" cld_path_ice::CD - "cloud fraction" cld_frac::CF - "McICA effective SW cloud cover `[0, 1]` `(ncol,)`; `nothing` if unused" cld_cover_sw::CC - "McICA effective LW cloud cover `[0, 1]` `(ncol,)`; `nothing` if unused" cld_cover_lw::CC - "cloud mask (longwave), = true if clouds are present" mask_lw::CM - "cloud mask (shortwave), = true if clouds are present" mask_sw::CM - "cloud mask type" mask_type::CMT - "ice roughness, 1 = none, 2 = medium, 3 = rough" ice_rgh::Int end Adapt.@adapt_structure CloudState @@ -158,17 +182,19 @@ end AerosolState{A, B, D} Aerosol state, used to compute optical properties. + +# Fields +- `aod_sw_ext`: Shortwave aerosol optical depth. +- `aod_sw_sca`: Shortwave aerosol optical depth (scattering component). +- `aero_mask`: Aerosol mask; `true` if any aerosol is present. +- `aero_size`: Aerosol size [microns]. +- `aero_mass`: Aerosol column mass [kg/m²]. """ struct AerosolState{A, B, D} - "shortwave aerosol optical depth" aod_sw_ext::A - "shortwave aerosol optical depth (scattering component)" aod_sw_sca::A - "aerosol mask, = true if any aerosol is present" aero_mask::B - "aerosol size (microns)" aero_size::D - "aerosol mass column (kg/m2)" aero_mass::D end Adapt.@adapt_structure AerosolState diff --git a/src/optics/BCs.jl b/src/optics/BCs.jl index 768947159..182ab9f07 100644 --- a/src/optics/BCs.jl +++ b/src/optics/BCs.jl @@ -1,23 +1,21 @@ module BCs -using DocStringExtensions using Adapt import ..RRTMGPGridParams export LwBCs, SwBCs """ - LwBCs{FT,FTA1D,FTA2DN} + LwBCs{FT, FTA2D, FTA2DN} -Longwave boundary conditions +Longwave boundary conditions. # Fields -$(DocStringExtensions.FIELDS) +- `sfc_emis`: Surface emissivity `(nbnd, ncol)`. +- `inc_flux`: Incident flux at the top of the atmosphere [W/m²] `(ncol, ngpt)`. """ struct LwBCs{FT, FTA2D, FTA2DN} - "Surface emissivity `[W/m²]` `(nbnd, ncol)`" sfc_emis::FTA2D - "incident flux at top of atmosphere `[W/m²]` `(ncol, ngpt)`" inc_flux::FTA2DN end LwBCs(sfc_emis, inc_flux) = @@ -28,21 +26,22 @@ LwBCs(sfc_emis, inc_flux) = Adapt.@adapt_structure LwBCs """ - SwBCs{FT,FTA1D} -Shortwave boundary conditions + SwBCs{FT, FTA1D, FTA1DN, FTA2D} + +Shortwave boundary conditions. + # Fields -$(DocStringExtensions.FIELDS) +- `cos_zenith`: Cosine of the solar zenith angle `(ncol)`. +- `toa_flux`: Top-of-atmosphere flux `(ncol)`. +- `sfc_alb_direct`: Surface albedo for specular (direct) radiation `(nbnd, ncol)`. +- `inc_flux_diffuse`: Incident diffuse flux at the top of the domain [W/m²] `(ncol, ngpt)`. +- `sfc_alb_diffuse`: Surface albedo for diffuse radiation `(nbnd, ncol)`. """ struct SwBCs{FT, FTA1D, FTA1DN, FTA2D} - "cosine of zenith angle `(ncol)`" cos_zenith::FTA1D - "top of atmosphere flux `(ncol)`" toa_flux::FTA1D - "surface albedo for specular (direct) radiation `(nbnd, ncol)`" sfc_alb_direct::FTA2D - "incident diffuse flux at top of domain `[W/m2]` `(ncol, ngpt)`" inc_flux_diffuse::FTA1DN - "surface albedo for diffuse radiation `(nbnd, ncol)`" sfc_alb_diffuse::FTA2D end SwBCs(cos_zenith, toa_flux, sfc_alb_direct, inc_flux_diffuse, sfc_alb_diffuse) = diff --git a/src/optics/Fluxes.jl b/src/optics/Fluxes.jl index 6cdddad83..ea6e738bd 100644 --- a/src/optics/Fluxes.jl +++ b/src/optics/Fluxes.jl @@ -1,7 +1,6 @@ module Fluxes using Adapt -using DocStringExtensions using ClimaComms import ..RRTMGPGridParams @@ -15,24 +14,29 @@ export AbstractFlux, apply_metric_scaling!, compute_net_flux! +""" + AbstractFlux + +Abstract type for broadband radiative-flux containers: [`FluxLW`](@ref) and +[`FluxSW`](@ref). +""" abstract type AbstractFlux{FT <: AbstractFloat, FTA2D <: AbstractArray{FT, 2}} end """ - FluxLW{FT,FTA2D} + FluxLW{FT, FTA2D} Upward, downward and net longwave fluxes at each level. # Fields -$(DocStringExtensions.FIELDS) +- `flux_up`: Upward flux [W/m²] `(nlev, ncol)`. +- `flux_dn`: Downward flux [W/m²] `(nlev, ncol)`. +- `flux_net`: Net flux [W/m²] `(nlev, ncol)`. """ struct FluxLW{FT <: AbstractFloat, FTA2D <: AbstractArray{FT, 2}} <: AbstractFlux{FT, FTA2D} - "upward flux `[W/m²]` `(nlev,ncol)`" flux_up::FTA2D - "downward flux `[W/m²]` `(nlev,ncol)`" flux_dn::FTA2D - "net flux `[W/m²]` `(nlev,ncol)`" flux_net::FTA2D end FluxLW(flux_up, flux_dn, flux_net) = @@ -50,22 +54,21 @@ function FluxLW(grid_params::RRTMGPGridParams) end """ - FluxSW{FT,FTA2D} + FluxSW{FT, FTA2D} Upward, downward and net shortwave fluxes at each level. # Fields -$(DocStringExtensions.FIELDS) +- `flux_up`: Upward flux [W/m²] `(nlev, ncol)`. +- `flux_dn`: Downward flux [W/m²] `(nlev, ncol)`. +- `flux_net`: Net flux [W/m²] `(nlev, ncol)`. +- `flux_dn_dir`: Direct downward flux [W/m²] `(nlev, ncol)`. """ struct FluxSW{FT <: AbstractFloat, FTA2D <: AbstractArray{FT, 2}} <: AbstractFlux{FT, FTA2D} - "upward flux `[W/m²]` `(nlev,ncol)`" flux_up::FTA2D - "downward flux `[W/m²]` `(nlev,ncol)`" flux_dn::FTA2D - "net flux `[W/m²]` `(nlev,ncol)`" flux_net::FTA2D - "direct downward flux `[W/m²]` `(nlev,ncol)`" flux_dn_dir::FTA2D end FluxSW(flux_up, flux_dn, flux_net, flux_dn_dir) = @@ -152,7 +155,7 @@ end compute_net_flux!(flux::AbstractFlux, gcol, nlev) compute_net_flux!(flux::AbstractFlux, gcol) -Computes net flux for column `gcol` across `nlev` levels: +Compute the net flux for column `gcol` across `nlev` levels: `flux.flux_net` = `flux.flux_up` - `flux.flux_dn` """ diff --git a/src/optics/GrayAtmosphere.jl b/src/optics/GrayAtmosphere.jl index 4f61aab0c..188c23850 100644 --- a/src/optics/GrayAtmosphere.jl +++ b/src/optics/GrayAtmosphere.jl @@ -25,7 +25,7 @@ export update_profile_lw!, compute_gray_heating_rate! ncol, ) -Updates t_lay and t_lev based on heating rate. +Update `t_lay` and `t_lev` based on the heating rate. """ function update_profile_lw!( device::ClimaComms.AbstractCPUDevice, @@ -118,7 +118,7 @@ end """ compute_gray_heating_rate!( - context, + device, hr_lay, p_lev, ncol, @@ -128,7 +128,7 @@ end grav_, ) -Computes the heating rate for the gray radiation simulation. +Compute the heating rate for the gray radiation simulation. """ function compute_gray_heating_rate!( device::ClimaComms.AbstractCPUDevice, diff --git a/src/optics/LookUpTables.jl b/src/optics/LookUpTables.jl index 0581794dd..4f054ee1b 100644 --- a/src/optics/LookUpTables.jl +++ b/src/optics/LookUpTables.jl @@ -1,6 +1,5 @@ module LookUpTables -using DocStringExtensions using Adapt export AbstractLookUp, @@ -24,24 +23,26 @@ abstract type AbstractLookUp end LookUpMinor{O, G, K} Lookup table for computing optical properties of minor gases. + +# Fields +- `bnd_st`: Starting index to `idx_gases_minor_lower/upper` for each band `(n_bnd + 1)`. +- `gpt_st`: Starting index in `kminor_lower/upper` for each g-point `(n_gpt + 1)`. +- `gasdata`: Indices for minor gases contributing to absorption in the lower/upper + atmosphere, indices for scaling gases, whether the minor gas scales with density, and + whether the minor gas scales by complement; `(4, n_min_absrb_lower/upper)`. +- `kminor`: Minor absorption coefficient in the lower/upper atmosphere + `(n_η, n_t_ref, n_contrib_lower/upper)`. """ struct LookUpMinor{O, G, K} - "starting index to `idx_gases_minor_lower/upper` for each band `(n_bnd + 1)`" bnd_st::O - "starting index in `kminor_lower/upper` for each g-point `(n_gpt + 1)`" gpt_st::O - "contains indices for minor gases contributing to absorption in the lower/upper atmosphere, - indices for scaling gases, - minor gas scales with density, - minor gas scales by complement `(4, n_min_absrb_lower/upper)`" gasdata::G - "minor absorption coefficient in lower/upper atmosphere `(n_η, n_t_ref, n_contrib_lower/upper)`" kminor::K end Adapt.@adapt_structure LookUpMinor # number of minor absorbors in the lower/upper atmosphere -@inline get_n_min_absrb(lkp::LookUpMinor) = size(lkp.idx_gases, 2) +@inline get_n_min_absrb(lkp::LookUpMinor) = size(lkp.gasdata, 2) # number of minor contributors in the lower/upper atmosphere @inline get_n_contrib(lkp::LookUpMinor) = size(lkp.kminor, 3) # get bounds for each g-point @@ -57,14 +58,18 @@ Adapt.@adapt_structure LookUpMinor """ ReferencePoints{RD1, RD3} -Log of reference pressures, reference temperatures, and volume mixing ratios used by the lookup table +Log of reference pressures, reference temperatures, and volume mixing ratios used by the +lookup table. + +# Fields +- `ln_p_ref`: Log of reference pressures used by the lookup table `(n_p_ref)`. +- `t_ref`: Reference temperatures used by the lookup table `(n_t_ref)`. +- `vmr_ref`: Reference volume mixing ratios used by the lookup table + `(2, n_gases, n_t_ref)`. """ struct ReferencePoints{RD1, RD3} - "log of reference pressures used by the lookup table `(n_p_ref)`" ln_p_ref::RD1 - "reference temperatures used by the lookup table `(n_t_ref)`" t_ref::RD1 - "reference volume mixing ratios used by the lookup table `(2, n_gases, n_t_ref)`" vmr_ref::RD3 end Adapt.@adapt_structure ReferencePoints @@ -72,14 +77,16 @@ Adapt.@adapt_structure ReferencePoints """ LookUpPlanck{PD1, PD2, PD4} -Look up data for Planck source calculations. +Lookup data for Planck source calculations. + +# Fields +- `planck_fraction`: Planck fraction `(n_η, n_p_ref, n_t_ref, n_gpt)`. +- `t_planck`: Reference temperatures for Planck source calculations `(n_t_plnk)`. +- `tot_planck`: Total Planck source for each band `(n_t_plnk, n_bnd)`. """ struct LookUpPlanck{PD1, PD2, PD4} - "Planck fraction `(n_η, n_p_ref, n_t_ref, n_gpt)`" planck_fraction::PD4 - "reference temperatures for Planck source calculations `(n_t_plnk)`" t_planck::PD1 - "total Planck source for each band `(n_t_plnk, n_bnd)`" tot_planck::PD2 end Adapt.@adapt_structure LookUpPlanck @@ -88,13 +95,15 @@ Adapt.@adapt_structure LookUpPlanck BandData{BNDI1, BNDI2, BNDD2} Band/g-point data for the lookup table. + +# Fields +- `major_gpt2bnd`: Map from g-point to band. +- `bnd_lims_gpt`: Starting and ending g-point for each band `(2, n_bnd)`. +- `bnd_lims_wn`: Starting and ending wavenumber for each band `(2, n_bnd)`. """ struct BandData{BNDI1, BNDI2, BNDD2} - "map from `g-point` to band" major_gpt2bnd::BNDI1 - "starting and ending `g-point` for each band `(2, n_bnd)`" bnd_lims_gpt::BNDI2 - "starting and ending wavenumber for each band `(2, n_bnd)`" bnd_lims_wn::BNDD2 end Adapt.@adapt_structure BandData @@ -105,32 +114,31 @@ Adapt.@adapt_structure BandData Longwave lookup tables, used to compute optical properties. # Fields -$(DocStringExtensions.FIELDS) +- `idx_h2o`: Vmr array index for H₂O. +- `p_ref_tropo`: Reference pressure separating the upper and lower atmosphere. +- `p_ref_min`: Minimum pressure supported by RRTMGP lookup tables. +- `t_ref_min`: Minimum temperature supported by RRTMGP lookup tables (first reference temperature). +- `t_ref_max`: Maximum temperature supported by RRTMGP lookup tables (last reference temperature). +- `key_species`: Major absorbing species in each band `(2, n_atmos_layers, n_bnd)`. +- `kmajor`: Major absorption coefficient `(n_η, n_p_ref + 1, n_t_ref, n_gpt)`. +- `planck`: Lookup data for Planck source calculations. +- `band_data`: Band data. +- `ref_points`: Reference temperatures, pressures and volume mixing ratios. +- `minor_lower`: Lookup data for minor gases in the lower atmosphere. +- `minor_upper`: Lookup data for minor gases in the upper atmosphere. """ struct LookUpLW{FT, IA3D, FTA4D, BND, P, R, LMNR} <: AbstractLookUp - "vmr array index for h2o" idx_h2o::Int - "Reference pressure separating upper and lower atmosphere" p_ref_tropo::FT - "minimum pressure supported by RRTMGP lookup tables" p_ref_min::FT - "minimum temperature supported by RRTMGP lookup tables (first reference temperature)" t_ref_min::FT - "maximum temperature supported by RRTMGP lookup tables (last reference temperature)" t_ref_max::FT - "major absorbing species in each band `(2, n_atmos_layers, n_bnd)`" key_species::IA3D - "major absorption coefficient `(n_η, n_p_ref + 1, n_t_ref, n_gpt)`" kmajor::FTA4D - "lookup data for Planck source calculations" planck::P - "band data" band_data::BND - "reference temperatures, pressures and volume mixing ratios" ref_points::R - "lookup data for minor gases in the lower atmosphere" minor_lower::LMNR - "lookup data for minor gases in the upper atmosphere" minor_upper::LMNR end Adapt.@adapt_structure LookUpLW @@ -156,38 +164,39 @@ Adapt.@adapt_structure LookUpLW Shortwave lookup tables, used to compute optical properties. # Fields -$(DocStringExtensions.FIELDS) +- `idx_h2o`: Vmr array index for H₂O. +- `p_ref_tropo`: Reference pressure separating the upper and lower atmosphere. +- `p_ref_min`: Minimum pressure supported by RRTMGP lookup tables. +- `t_ref_min`: Minimum temperature supported by RRTMGP lookup tables (first reference temperature). +- `t_ref_max`: Maximum temperature supported by RRTMGP lookup tables (last reference temperature). +- `solar_src_tot`: Total solar irradiation. +- `key_species`: Major absorbing species in each band `(2, n_atmos_layers, n_bnd)`. +- `kmajor`: Major absorption coefficient `(n_η, n_p_ref + 1, n_t_ref, n_gpt)`. +- `band_data`: Band data. +- `ref_points`: Reference temperatures, pressures and volume mixing ratios. +- `rayl_lower`: Rayleigh absorption coefficient for the lower atmosphere + `(n_η, n_t_ref, n_gpt)`. +- `rayl_upper`: Rayleigh absorption coefficient for the upper atmosphere + `(n_η, n_t_ref, n_gpt)`. +- `solar_src_scaled`: Relative solar source contribution from each g-point `(n_gpt)`. +- `minor_lower`: Lookup data for minor gases in the lower atmosphere. +- `minor_upper`: Lookup data for minor gases in the upper atmosphere. """ struct LookUpSW{FT, IA3D, FTA1D, FTA3D, FTA4D, BND, R, LMNR} <: AbstractLookUp - "vmr array index for h2o" idx_h2o::Int - "Reference pressure separating upper and lower atmosphere" p_ref_tropo::FT - "minimum pressure supported by RRTMGP lookup tables" p_ref_min::FT - "minimum temperature supported by RRTMGP lookup tables (first reference temperature)" t_ref_min::FT - "maximum temperature supported by RRTMGP lookup tables (last reference temperature)" t_ref_max::FT - "total solar irradiation" solar_src_tot::FT - "major absorbing species in each band `(2, n_atmos_layers, n_bnd)`" key_species::IA3D - "major absorption coefficient `(n_η, n_p_ref + 1, n_t_ref, n_gpt)`" kmajor::FTA4D - "band data" band_data::BND - "reference temperatures, pressures and volume mixing ratios" ref_points::R - "Rayleigh absorption coefficient for lower atmosphere `(n_η, n_t_ref, n_gpt)`" rayl_lower::FTA3D - "Rayleigh absorption coefficient for upper atmosphere `(n_η, n_t_ref, n_gpt)`" rayl_upper::FTA3D - "relative solar source contribution from each `g-point` `(n_gpt)`" solar_src_scaled::FTA1D - "lookup data for minor gases in the lower atmosphere" minor_lower::LMNR - "lookup data for minor gases in the upper atmosphere" minor_upper::LMNR end Adapt.@adapt_structure LookUpSW @@ -211,25 +220,27 @@ Adapt.@adapt_structure LookUpSW Lookup table for cloud optics. -This struct stores the lookup tables for determing extinction coeffient, +This struct stores the lookup tables for determining the extinction coefficient, single-scattering albedo, and asymmetry parameter g as a function of effective radius. -We compute the optical depth tau (=exintinction coeff * condensed water path) +We compute the optical depth tau (= extinction coefficient * condensed water path) and the products tau*ssa and tau*ssa*g for liquid and ice cloud separately. These are used to determine the optical properties of ice and water cloud together. # Fields -$(DocStringExtensions.FIELDS) +- `dims`: Dimensions `nband`, `nrghice`, `nsize_liq`, `nsize_ice`, `pair`. +- `bounds`: Particle size lower and upper bounds and factor for LUT interpolation for + liquid and ice particles. +- `liqdata`: Liquid extinction coefficient, single scattering albedo and asymmetry + parameter `(3*nsize_liq, nbnd)`. +- `icedata`: Ice extinction coefficient, single scattering albedo and asymmetry + parameter `(3*nsize_ice, nbnd, nrghice)`. +- `bnd_lims_wn`: Beginning and ending wavenumber for each band [cm⁻¹] `(2, nband)`. """ struct LookUpCld{D, B, L, I, W} <: AbstractLookUp - "`nband`, `nrghice`, `nsize_liq`, `nsize_ice`, `pair`" dims::D - "particle size lower and upper bounds and factor for LUT interpolation for liquid and ice particles" bounds::B - "liquid extinction coefficient, single scattering albedo and symmetry paramter `(3*nsize_liq, nbnd)`" liqdata::L - "ice extinction coefficient, single scattering albedo and symmetry paramter `(3*nsize_ice, nbnd, nrghice)`" icedata::I - "beginning and ending wavenumber for each band (`2, nband`) cm⁻¹" bnd_lims_wn::W end Adapt.@adapt_structure LookUpCld @@ -244,7 +255,7 @@ Adapt.@adapt_structure LookUpCld # number of ice particle sizes (cloud lookup table dimension) @inline get_nsize_ice(lkp::LookUpCld) = @inbounds lkp.dims[4] # pair = 2 (cloud lookup table dimension) -@inline get_pair(::LookUpCld) = @inbounds lkp.dims[5] +@inline get_pair(lkp::LookUpCld) = @inbounds lkp.dims[5] @inline function getview_liqdata(lkp::LookUpCld, ibnd) n = get_nsize_liq(lkp) @@ -275,42 +286,41 @@ end """ LookUpAerosolMerra{D, D1, D2, D3, D4, W} <: AbstractLookUp -Merra lookup table for aersols. +MERRA lookup table for aerosols. -This struct stores the lookup tables for determing extinction coeffient, +This struct stores the lookup tables for determining the extinction coefficient, single-scattering albedo, and asymmetry parameter g as a function of aerosol particle size, relative humidity and band. Data is provided for dust, sea salt, -sulfate, black carbon (hydrophobic and hydrophilic) and organic carbon +sulfate, black carbon (hydrophobic and hydrophilic) and organic carbon (hydrophobic and hydrophilic). - # Fields -$(DocStringExtensions.FIELDS) +- `dims`: Dimensions `nband`, `nval`, `nbin`, `nrh`, `pair`. +- `size_bin_limits`: Beginning and ending limit for each MERRA aerosol size bin + [microns]. +- `rh_levels`: Relative humidity levels for MERRA hydrophilic aerosols. +- `dust`: Dust `(nval, nbin, nband)`. +- `sea_salt`: Sea salt `(nval, nrh, nbin, nband)`. +- `sulfate`: Sulfate `(nval, nrh, nband)`. +- `black_carbon_rh`: Black carbon, hydrophilic `(nval, nrh, nband)`. +- `black_carbon`: Black carbon, hydrophobic `(nval, nband)`. +- `organic_carbon_rh`: Organic carbon, hydrophilic `(nval, nrh, nband)`. +- `organic_carbon`: Organic carbon, hydrophobic `(nval, nband)`. +- `bnd_lims_wn`: Beginning and ending wavenumber for each band [cm⁻¹] `(2, nband)`. +- `iband_550nm`: Band number index corresponding to 550 nm. """ struct LookUpAerosolMerra{D, D1, D2, D3, D4, W} <: AbstractLookUp - "`nband`, `nval`, `nbin`, `nrh`, `pair`" dims::D - "beginning and ending limit for each MERRA aerosol size bin (microns)" size_bin_limits::D2 - "relative humidity levels for MERRA hydrophilic aerosols" rh_levels::D1 - "dust `(nval, nbin, nband)`" dust::D3 - "sea salt `(nval, nrh, nbin, nband)`" sea_salt::D4 - "sulfate `(nval, nrh, nband)`" sulfate::D3 - "black carbon - hydrophilic `(nval, nhr, nband)`" black_carbon_rh::D3 - "black carbon - hydrophobic `(nval, nband)`" black_carbon::D2 - "organic carbon - hydrophilic `(nval, nhr, nband)`" organic_carbon_rh::D3 - "organic carbon - hydrophobic `(nval, nband)`" organic_carbon::D2 - "beginning and ending wavenumber for each band (`2, nband`) cm⁻¹" bnd_lims_wn::W - "Band number index corresponding to 550 nm" iband_550nm::Int end Adapt.@adapt_structure LookUpAerosolMerra diff --git a/src/optics/Optics.jl b/src/optics/Optics.jl index 24bf27e54..9037009bd 100644 --- a/src/optics/Optics.jl +++ b/src/optics/Optics.jl @@ -1,6 +1,5 @@ module Optics -using DocStringExtensions using Adapt using Random import ClimaComms diff --git a/src/optics/Sources.jl b/src/optics/Sources.jl index 0531b0ce3..6090af81f 100644 --- a/src/optics/Sources.jl +++ b/src/optics/Sources.jl @@ -1,7 +1,6 @@ module Sources using Adapt -using DocStringExtensions using ClimaComms import ..Parameters as RP @@ -17,23 +16,22 @@ Abstract longwave source for no-scattering and two stream longwave solvers. abstract type AbstractSourceLW end """ - SourceLWNoScat{FT,FTA1D,FTA2D} <: AbstractSourceLW + SourceLWNoScat{S, D, PS} <: AbstractSourceLW -Longwave sources: computed at layer center, layer edges, -and at the surface for no scattering calculations +Longwave sources: computed at layer center, layer edges, +and at the surface for no-scattering calculations. # Fields - -$(DocStringExtensions.FIELDS) +- `param_set`: Parameter set. +- `sfc_source`: Surface source [W/m²] `(ncol)`. +- `lay_source`: Planck source at layer average temperature [W/m²] `(nlay, ncol)`. +- `lev_source`: Planck level source at layer edges [W/m²] `(nlay+1, ncol)`; includes + spectral weighting that accounts for the state-dependent frequency to g-space mapping. """ struct SourceLWNoScat{S, D, PS} <: AbstractSourceLW - "Parameter set" param_set::PS - "Surface source `[W/m2]` `(ncol)`" sfc_source::S - "Planck source at layer average temperature `[W/m2]` `(nlay, ncol)`" lay_source::D - "Planck level source at layer edges `[W/m2]` `(nlay+1, ncol)`, includes spectral weighting that accounts for state-dependent frequency to g-space mapping" lev_source::D end Adapt.@adapt_structure SourceLWNoScat @@ -62,25 +60,23 @@ end """ SourceLW2Str{S, D, V, PS} <: AbstractSourceLW -Longwave sources: computed at layer center, layer edges, -and at the surface for 2-stream calculations +Longwave sources: computed at layer center, layer edges, +and at the surface for 2-stream calculations. # Fields - -$(DocStringExtensions.FIELDS) +- `param_set`: Parameter set. +- `sfc_source`: Surface source [W/m²] `(ncol)`. +- `leveldata`: Storage for level source, albedo and src `(3, nlay+1, ncol)`. +- `lev_source`: Level source [W/m²] `(nlay+1, ncol)`; used in 2-stream calculations. +- `albedo`: Temporary storage array `(nlay+1, ncol)`; used in 2-stream calculations. +- `src`: Temporary storage array `(nlay+1, ncol)`; used in 2-stream calculations. """ struct SourceLW2Str{S, D, V, PS} <: AbstractSourceLW - "Parameter set" param_set::PS - "Surface source `[W/m2]` `(ncol)`" sfc_source::S - "storage for level source, albedo and src `(3, nlay+1, ncol)`" leveldata::D - "level source `[W/m2]` `(nlay+1, ncol)`, used in 2 stream calculations" lev_source::V - "temporary storage array, used in 2 stream calculations `(nlay + 1, ncol)`" albedo::V - "temporary storage array, used in 2 stream calculations `(nlay + 1, ncol)`" src::V end @@ -132,23 +128,21 @@ end """ - SourceSW2Str{S,D,V} + SourceSW2Str{S, D, V} -Shortwave sources: computed at layer center, layer edges, -and at the surface for 2-stream calculations +Shortwave sources: computed at layer center, layer edges, +and at the surface for 2-stream calculations. # Fields - -$(DocStringExtensions.FIELDS) +- `sfc_source`: Surface source `(ncol)`. +- `leveldata`: Storage for albedo and src `(2, nlay+1, ncol)`. +- `albedo`: Albedo `(nlay+1, ncol)`. +- `src`: Temporary storage array `(nlay+1, ncol)`; used in 2-stream calculations. """ struct SourceSW2Str{S, D, V} - "surface source `(ncol)`" sfc_source::S - "storage for albedo and src `(2, nlay + 1, ncol)`" leveldata::D - "albedo `(nlay + 1, ncol)`" albedo::V - "temporary storage array, used in 2 stream calculations `(nlay + 1, ncol)`" src::V end diff --git a/src/optics/VolumeMixingRatios.jl b/src/optics/VolumeMixingRatios.jl index 9d3f0ab99..c23e79477 100644 --- a/src/optics/VolumeMixingRatios.jl +++ b/src/optics/VolumeMixingRatios.jl @@ -1,5 +1,4 @@ module VolumeMixingRatios -using DocStringExtensions using Adapt import ..RRTMGPGridParams @@ -7,28 +6,38 @@ export AbstractVmr, Vmr, VmrGM, get_vmr """ AbstractVmr{FT} + +Abstract type for storage strategies for gas volume mixing ratios. + +Concrete subtypes: +- [`VmrGM`](@ref): H₂O and O₃ vary spatially, other gases are well-mixed scalars. +- [`Vmr`](@ref): every gas varies by layer and column. + +Subtypes are accessed through the interface method [`get_vmr`](@ref), which returns the +volume mixing ratio of a given gas for a given layer and column. """ abstract type AbstractVmr{FT <: AbstractFloat} end + """ - VmrGM{FT,FTA1D,FTA2D} <: AbstractVmr{FT} + VmrGM{FT, FTA1D, FTA2D} <: AbstractVmr{FT} Volume mixing ratios for various gases in the atmosphere. This struct can be used -when only H₂O and O₃ concentrations vary spatially and a global mean is used +when only H₂O and O₃ concentrations vary spatially and a global mean is used for all other gases. # Fields -$(DocStringExtensions.FIELDS) +- `vmr_h2o`: Volume mixing ratio of H₂O. +- `vmr_o3`: Volume mixing ratio of O₃. +- `vmr`: Volume mixing ratios of all other gases, which are independent of location and + column. """ struct VmrGM{ FT <: AbstractFloat, FTA1D <: AbstractArray{FT, 1}, FTA2D <: AbstractArray{FT, 2}, } <: AbstractVmr{FT} - "volume mixing ratio of H₂O" vmr_h2o::FTA2D - "volume mixing ratio of Ozone" vmr_o3::FTA2D - "volume mixing ratio of all other gases, which are independent of location and column" vmr::FTA1D end VmrGM(vmr_h2o, vmr_o3, vmr) = @@ -55,17 +64,16 @@ function VolumeMixingRatioGlobalMean( end """ - Vmr{FT,FTA3D} <: AbstractVmr{FT} + Vmr{FT, FTA3D} <: AbstractVmr{FT} Volume mixing ratios for various gases in the atmosphere. This struct can be used when concentrations vary spatially for all gases. # Fields -$(DocStringExtensions.FIELDS) +- `vmr`: Volume mixing ratios of all gases as a function of layer and column. """ struct Vmr{FT <: AbstractFloat, FTA3D <: AbstractArray{FT, 3}} <: AbstractVmr{FT} - "volume mixing ratio of all gases as a function of location and column" vmr::FTA3D end Vmr(vmr) = Vmr{eltype(vmr), typeof(vmr)}(vmr) diff --git a/src/optics/aerosol_optics.jl b/src/optics/aerosol_optics.jl index 2ba8b7e5d..02176b030 100644 --- a/src/optics/aerosol_optics.jl +++ b/src/optics/aerosol_optics.jl @@ -12,8 +12,8 @@ iband_550nm ) -This function computes the `OneScalar` aerosol optics properties and adds them -to the exising `OneScalar` optics properties. +Compute the `OneScalar` aerosol optics properties and add them +to the existing `OneScalar` optics properties. """ @inline function add_aerosol_optics_1scalar!( τ, @@ -71,8 +71,8 @@ end delta_scaling = false, ) -This function computes the `TwoStream` aerosol optics properties and adds them -to the exising `TwoStream` optics properties: +Compute the `TwoStream` aerosol optics properties and add them +to the existing `TwoStream` optics properties: - `aod_ext` total aerosol optical depth - `aod_sca` scattering component of aerosol optical depth diff --git a/src/optics/cloud_optics.jl b/src/optics/cloud_optics.jl index b5bbbaaba..fc6d41af2 100644 --- a/src/optics/cloud_optics.jl +++ b/src/optics/cloud_optics.jl @@ -64,8 +64,8 @@ end ibnd; delta_scaling = false, ) -This function computes the TwoStream clouds optics properties and adds them -to the TwoStream gas optics properties. +Compute the `TwoStream` cloud optics properties and add them +to the `TwoStream` gas optics properties. """ @inline function add_cloud_optics_2stream!( τ, @@ -149,7 +149,7 @@ end cld_path_liq, ) -This function computes the `TwoStream` cloud liquid properties using the `LookUpTable` method. +Compute the `TwoStream` cloud liquid properties using the `LookUpTable` method. """ @inline function compute_lookup_cld_liq_props( nsize_liq, @@ -202,7 +202,7 @@ end cld_path_ice, ) -This function computes the `TwoStream` cloud ice properties using the `LookUpTable` method. +Compute the `TwoStream` cloud ice properties using the `LookUpTable` method. """ @inline function compute_lookup_cld_ice_props( nsize_ice, @@ -246,16 +246,20 @@ end """ build_cloud_mask!(cld_mask, cld_frac, ::MaxRandomOverlap) -Builds McICA-sampled cloud mask from cloud fraction data for maximum-random overlap +Build a McICA-sampled cloud mask from cloud fraction data for maximum-random overlap Reference: https://github.com/AER-RC/RRTMG_SW/ -Determinism: the mask is drawn from the global `Random` RNG (`Random.rand()`) within the -per-column g-point loop. On CPU with a fixed `Random.seed!` the sampling is reproducible, but -under multithreaded/GPU execution the columns share global RNG state, so the per-column McICA -sample is not guaranteed reproducible across runs or column orderings. Broadband fluxes are -statistically unbiased regardless; bit-reproducible per-column sampling would require -column-indexed seeding, deliberately not done here to keep the kernel allocation-free. +Determinism: the mask is drawn with `Random.rand()` within the per-column g-point loop, i.e. +from the RNG of whichever task runs that iteration. On a single CPU thread every draw comes +from the one RNG that `Random.seed!` reseeds, so a fixed seed makes the sampling reproducible. +Under multithreading each worker task uses its own independent task-local RNG (seeded from the +parent at spawn, not by `Random.seed!`), and on the GPU the device RNG is keyed by a per-launch +seed the host `Random.seed!` does not control (at least on CUDA 6.x); combined with a +work assignment that is not fixed across runs or thread counts, the per-column McICA sample is +then not guaranteed reproducible. Broadband fluxes are statistically unbiased regardless; +bit-reproducible per-column sampling would require column-indexed seeding, deliberately not +done here to keep the kernel allocation-free. """ function build_cloud_mask!( cld_mask::AbstractArray{Bool, 1}, diff --git a/src/optics/column_amounts.jl b/src/optics/column_amounts.jl index d9933a7e8..69fd53540 100644 --- a/src/optics/column_amounts.jl +++ b/src/optics/column_amounts.jl @@ -1,6 +1,6 @@ """ compute_col_gas!( - context, + device, p_lev, col_dry, param_set, @@ -9,8 +9,7 @@ max_threads = Int(256), ) -This function computes the column amounts of dry or moist air. - +Compute the column amounts of dry or moist air. """ function compute_col_gas!( device::ClimaComms.AbstractCPUDevice, @@ -47,7 +46,7 @@ end vmr_h2o::Union{AbstractArray{FT, 2}, Nothing} = nothing, ) where {FT} -This function computes the relative humidity. +Compute the relative humidity. """ function compute_relative_humidity!( diff --git a/src/optics/compute_optical_props.jl b/src/optics/compute_optical_props.jl index d973b82fd..67d233e0b 100644 --- a/src/optics/compute_optical_props.jl +++ b/src/optics/compute_optical_props.jl @@ -10,7 +10,7 @@ lkp_aero::Union{LookUpAerosolMerra, Nothing} = nothing, ) where {FT<:AbstractFloat} -Computes optical properties for the longwave problem. +Compute optical properties for the longwave problem. """ @inline function compute_optical_props!( op::OneScalar, @@ -244,10 +244,11 @@ end gcol::Int, igpt::Int, lkp::LookUpSW, - lkp_cld::Union{LookUpCld,Nothing} = nothing, + lkp_cld::Union{LookUpCld, Nothing} = nothing, + lkp_aero::Union{LookUpAerosolMerra, Nothing} = nothing, ) -Computes optical properties for the shortwave problem. +Compute optical properties for the shortwave problem. """ @inline function compute_optical_props!( op::OneScalar, diff --git a/src/optics/gas_optics.jl b/src/optics/gas_optics.jl index ba9654daa..bf6757bf8 100644 --- a/src/optics/gas_optics.jl +++ b/src/optics/gas_optics.jl @@ -4,14 +4,14 @@ p_lev, mol_m_dry, mol_m_h2o, - avogadro + avogadro, helmert1, - vmr_h2o + vmr_h2o, lat, glay, gcol, ) -This function computes the column amounts of dry or moist air. +Compute the column amounts of dry or moist air. """ function compute_col_gas_kernel!( col_gas::AbstractArray{FT, 2}, @@ -53,7 +53,7 @@ compute_relative_humidity_kernel!( gcol::Int, ) where {FT} -This function computes the relative humidity. +Compute the relative humidity. """ function compute_relative_humidity_kernel!( rh::AbstractArray{FT, 2}, @@ -80,9 +80,9 @@ function compute_relative_humidity_kernel!( end """ - compute_interp_frac_temp(Δ_t_ref, n_t_ref, t_ref, t_lay) + compute_interp_frac_temp(t_ref, t_lay) -compute interpolation fraction for temperature. +Compute interpolation fraction for temperature. """ @inline function compute_interp_frac_temp(t_ref, t_lay) @inbounds Δ_t_ref = t_ref[2] - t_ref[1] @@ -93,13 +93,7 @@ compute interpolation fraction for temperature. end """ - compute_interp_frac_press( - lkp::AbstractLookUp, - p_lay, - tropo, - glay, - gcol, - ) + compute_interp_frac_press(ln_p_ref, p_lay, tropo) Compute interpolation fraction for pressure. """ @@ -122,16 +116,15 @@ end """ compute_interp_frac_η( - lkp::AbstractLookUp, - vmr, + n_η, + ig, + vmr_ref, + (vmr1, vmr2), tropo, jtemp, - ibnd, - glay, - gcol, ) -Compute interpolation fraction for binary species parameter. +Compute interpolation fraction for the binary species parameter ``η``. """ @inline function compute_interp_frac_η( n_η, @@ -247,7 +240,7 @@ end p_lay, t_lay, glay, gcol, - ) where {FT<:AbstractFloat} + ) Compute optical thickness, single scattering albedo, and asymmetry parameter. """ @@ -328,7 +321,7 @@ end """ compute_τ_minor( - lkp::AbstractLookUp, + lkp_minor::LookUpMinor, vmr, vmr_h2o::FT, col_dry, @@ -420,7 +413,7 @@ end """ compute_τ_rayleigh( - lkp::LookUpSW, + rayleigh_coeff::AbstractArray{FT, 2}, col_dry::FT, vmr_h2o::FT, jtemp::Int, diff --git a/src/optics/gray_atmospheric_states.jl b/src/optics/gray_atmospheric_states.jl index e7a81b37e..11121addb 100644 --- a/src/optics/gray_atmospheric_states.jl +++ b/src/optics/gray_atmospheric_states.jl @@ -1,71 +1,103 @@ +""" + AbstractGrayOpticalThickness + +Abstract type for the gray-radiation optical-thickness parameterizations: +[`GrayOpticalThicknessSchneider2004`](@ref) and +[`GrayOpticalThicknessOGorman2008`](@ref). +""" abstract type AbstractGrayOpticalThickness end """ GrayOpticalThicknessSchneider2004{FT} <: AbstractGrayOpticalThickness + GrayOpticalThicknessSchneider2004(FT; α = 3.5, te = 300, tt = 200, Δt = 60) -Optical thickness function parameters from Schneider 2004, J. Atmos. Sci. (2004) 61 (12): 1317–1340. -DOI: https://doi.org/10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2 +Parameters of the semi-grey optical-thickness profile of [schneider2004](@cite). +The vertical longwave optical depth scales with pressure as ``\\widehat{\\tau}(p) = +d_0(\\phi)\\,(p/p_0)^\\alpha``, so the exponent `α` controls how the absorber is +distributed with height (`α = 1` for a well-mixed absorber, larger `α` for one +concentrated near the surface). The optical thickness at the surface, +``d_0(\\phi)``, is set by the latitude-dependent radiative-equilibrium +temperature built from `te`, `tt`, and `Δt`. # Fields -$(DocStringExtensions.FIELDS) +- `α`: Pressure exponent of the optical-depth profile. +- `te`: Global-mean surface temperature [K]. +- `tt`: Temperature at the top of the atmosphere [K]. +- `Δt`: Equator-to-pole surface temperature difference [K]. """ struct GrayOpticalThicknessSchneider2004{FT} <: AbstractGrayOpticalThickness - "scaling height ratio" α::FT - "global mean surface temperature (K)" te::FT - "temp at top of atmosphere (K)" tt::FT - "Δt (K)" Δt::FT end Adapt.@adapt_structure GrayOpticalThicknessSchneider2004 -GrayOpticalThicknessSchneider2004(::Type{FT}) where {FT} = - GrayOpticalThicknessSchneider2004{FT}(FT(3.5), FT(300), FT(200), FT(60)) +GrayOpticalThicknessSchneider2004( + ::Type{FT}; + α = 3.5, + te = 300, + tt = 200, + Δt = 60, +) where {FT} = + GrayOpticalThicknessSchneider2004{FT}(FT(α), FT(te), FT(tt), FT(Δt)) """ GrayOpticalThicknessOGorman2008{FT} <: AbstractGrayOpticalThickness + GrayOpticalThicknessOGorman2008(FT; α = 1.0, fₗ = 0.2, τₑ = 7.2, τₚ = 1.8, τ₀ = 0.22) -Optical thickness function parameters from O'Gorman 2008, Journal of Climate Vol 21, Page(s): 3815–3832. -DOI: https://doi.org/10.1175/2007JCLI2065.1 +Parameters of the grey optical-thickness profile of [frierson2006](@cite) and +[ogorman2008](@cite). The +vertical longwave optical depth blends a linear and a quartic dependence on the +normalized pressure ``\\sigma = p/p_0``, ``\\widehat{\\tau} \\propto f_\\ell\\,\\sigma + +(1 - f_\\ell)\\,\\sigma^4``, so that it stays finite aloft while thickening toward +the surface, with an equator-to-pole contrast set by `τₑ` and `τₚ`. The shortwave +optical depth scales as ``\\sigma^2`` with amplitude `τ₀`. This is the default +optical-thickness model for [`solve_gray`](@ref RRTMGP.solve_gray). # Fields -$(DocStringExtensions.FIELDS) +- `α`: Overall scaling factor for the longwave optical depth. +- `fₗ`: Weight of the linear (versus quartic) pressure dependence. +- `τₑ`: Longwave optical thickness at the equator. +- `τₚ`: Longwave optical thickness at the poles. +- `τ₀`: Shortwave optical thickness amplitude. """ struct GrayOpticalThicknessOGorman2008{FT} <: AbstractGrayOpticalThickness - "scaling factor" α::FT - "fₗ" fₗ::FT - "longwave optical thickness at equator" τₑ::FT - "longwave optical thickness at poles" τₚ::FT - "optical thickness for shortwave radiation" τ₀::FT end Adapt.@adapt_structure GrayOpticalThicknessOGorman2008 -GrayOpticalThicknessOGorman2008(::Type{FT}) where {FT <: AbstractFloat} = - GrayOpticalThicknessOGorman2008{FT}( - FT(1.0), - FT(0.2), - FT(7.2), - FT(1.8), - FT(0.22), - ) +GrayOpticalThicknessOGorman2008( + ::Type{FT}; + α = 1.0, + fₗ = 0.2, + τₑ = 7.2, + τₚ = 1.8, + τ₀ = 0.22, +) where {FT <: AbstractFloat} = + GrayOpticalThicknessOGorman2008{FT}(FT(α), FT(fₗ), FT(τₑ), FT(τₚ), FT(τ₀)) """ - GrayAtmosphericState{FT,FTA1D,FTA2D} <: - AbstractAtmosphericState{FT,FTA1D} + GrayAtmosphericState{FT, FTA1D, FTA2D, OTP} <: AbstractAtmosphericState -Atmospheric conditions, used to compute optical properties with the gray atmosphere approximation +Atmospheric conditions, used to compute optical properties with the gray atmosphere +approximation. # Fields -$(DocStringExtensions.FIELDS) +- `lat`: Latitude in degrees for each column `(ncol,)`. +- `p_lay`: Layer pressures [Pa, mb] `(nlay, ncol)`. +- `p_lev`: Level pressures [Pa, mb] `(nlay+1, ncol)`. +- `t_lay`: Layer temperatures [K] `(nlay, ncol)`. +- `t_lev`: Level temperatures [K] `(nlay+1, ncol)`. +- `z_lev`: Level altitudes [m] `(nlay+1, ncol)`. +- `t_sfc`: Surface temperatures [K] `(ncol)`. +- `otp`: Optical thickness parameters. """ struct GrayAtmosphericState{ FT <: AbstractFloat, @@ -73,21 +105,13 @@ struct GrayAtmosphericState{ FTA2D <: AbstractArray{FT, 2}, OTP <: AbstractGrayOpticalThickness, } <: AbstractAtmosphericState - "latitude, in degrees, for each column; `(ncol,)`" lat::FTA1D - "Layer pressures `[Pa, mb]`; `(nlay, ncol)`" p_lay::FTA2D - "Level pressures `[Pa, mb]`; `(nlay+1, ncol)`" p_lev::FTA2D - "Layer temperatures `[K]`; `(nlay, ncol)`" t_lay::FTA2D - "Level temperatures `[K]`; `(nlay+1,ncol)`" t_lev::FTA2D - "Level Altitude `[m]`; `(nlay+1,ncol)`" z_lev::FTA2D - "Surface temperatures `[K]`; `(ncol)`" t_sfc::FTA1D - "optical thickness parameters" otp::OTP end Adapt.@adapt_structure GrayAtmosphericState @@ -108,11 +132,25 @@ Adapt.@adapt_structure GrayAtmosphericState @inline getview_t_lay(as::GrayAtmosphericState, gcol) = @inbounds view(as.t_lay, :, gcol) -# This functions sets up a model temperature and pressure -# distributions for a gray atmosphere based on a pressure grid -# see Schneider 2004, J. Atmos. Sci. (2004) 61 (12): 1317–1340. -# https://doi.org/10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2 +""" + setup_gray_as_pr_grid( + context::ClimaComms.AbstractCommsContext, + nlay::Int, + lat, + p0, + pe, + otp::AbstractGrayOpticalThickness, + param_set, + ::Type{DA}, + step = "linear", + ) +Build a [`GrayAtmosphericState`](@ref) on a pressure grid from surface +pressure `p0` to top-of-atmosphere pressure `pe`, with the analytic +temperature profile of Schneider (2004), J. Atmos. Sci. 61 (12), 1317-1340 +(doi: [10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2](https://doi.org/10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2)). +One column is built per element of `lat`, with arrays of type `DA`. +""" function setup_gray_as_pr_grid( context::ClimaComms.AbstractCommsContext, nlay::Int, diff --git a/src/optics/gray_optics_kernels.jl b/src/optics/gray_optics_kernels.jl index 014bafade..860191ce1 100644 --- a/src/optics/gray_optics_kernels.jl +++ b/src/optics/gray_optics_kernels.jl @@ -9,7 +9,7 @@ gcol::Int, ) -Computes optical properties for the longwave gray radiation problem. +Compute optical properties for the longwave gray radiation problem. """ function compute_optical_props!( op::OneScalar, @@ -122,7 +122,7 @@ end gcol::Int, ) -Computes optical properties for the shortwave gray radiation problem. +Compute optical properties for the shortwave gray radiation problem. """ function compute_optical_props!( op::AbstractOpticalProps, @@ -162,8 +162,9 @@ end lat, ) where {FT<:AbstractFloat} -This functions calculates the optical thickness based on pressure -and lapse rate for a gray atmosphere. +Calculate the optical thickness from pressure and lapse rate for a gray +atmosphere. + See Schneider 2004, J. Atmos. Sci. (2004) 61 (12): 1317–1340. DOI: https://doi.org/10.1175/1520-0469(2004)061<1317:TTATTS>2.0.CO;2 """ @@ -199,10 +200,12 @@ compute_gray_optical_thickness_sw( lat, ) where {FT} -This functions calculates the optical thickness based on pressure -and lapse rate for a gray atmosphere. -See O'Gorman 2008, Journal of Climate Vol 21, Page(s): 3815–3832. -DOI: https://doi.org/10.1175/2007JCLI2065.1 +Calculate the optical thickness from pressure and lapse rate for a gray +atmosphere. + +See Frierson, Held & Zurita-Gotor (2006), J. Atmos. Sci. 63, 2548-2566 +(DOI: https://doi.org/10.1175/JAS3753.1) and O'Gorman & Schneider (2008), +J. Climate 21, 3815-3832 (DOI: https://doi.org/10.1175/2007JCLI2065.1). """ function compute_gray_optical_thickness_lw( params::GrayOpticalThicknessOGorman2008{FT}, @@ -230,8 +233,8 @@ end rest..., ) where {FT} -This functions calculates the optical thickness based on pressure -for a gray atmosphere. +Calculate the optical thickness from pressure for a gray atmosphere. + See O'Gorman 2008, Journal of Climate Vol 21, Page(s): 3815–3832. DOI: https://doi.org/10.1175/2007JCLI2065.1 """ diff --git a/src/optics/optical_props.jl b/src/optics/optical_props.jl index 4f3f8bf75..c3a3d1dde 100644 --- a/src/optics/optical_props.jl +++ b/src/optics/optical_props.jl @@ -1,16 +1,15 @@ """ - OneScalar{FTA2D,AD} <: AbstractOpticalProps + OneScalar{D, V} <: AbstractOpticalProps Single scalar approximation for optical depth, used in -calculations accounting for extinction and emission +calculations accounting for extinction and emission. # Fields -$(DocStringExtensions.FIELDS) +- `layerdata`: Storage for optical thickness. +- `τ`: View into optical depth. """ struct OneScalar{D, V} <: AbstractOpticalProps - "storage for optical thickness" layerdata::D - "view into optical depth" τ::V end @@ -32,22 +31,22 @@ end """ - TwoStream{FTA2D} <: AbstractOpticalProps + TwoStream{D, V} <: AbstractOpticalProps Two stream approximation for optical properties, used in -calculations accounting for extinction and emission +calculations accounting for extinction and emission. # Fields -$(DocStringExtensions.FIELDS) +- `layerdata`: Storage for optical depth, single scattering albedo and asymmetry + parameter. +- `τ`: View into optical depth. +- `ssa`: View into single scattering albedo. +- `g`: View into asymmetry parameter. """ struct TwoStream{D, V} <: AbstractOpticalProps - "storage for optical depth, single scattering albedo and asymmerty parameter" layerdata::D - "view into optical depth" τ::V - "view into single scattering albedo" ssa::V - "view into asymmetry parameter" g::V end diff --git a/src/optics/optics_utils.jl b/src/optics/optics_utils.jl index cf399a5bb..37d23adf6 100644 --- a/src/optics/optics_utils.jl +++ b/src/optics/optics_utils.jl @@ -2,8 +2,7 @@ """ loc_lower(xi, Δx, n, x) -Return the location of the left (lower) point of the interval in which `xi` is located in vector `x`. -This function assumes `Δx` is uniform. +Return the location of the left (lower) point of the interval in which `xi` is located in vector `x`, assuming uniform `Δx`. """ @inline function loc_lower(xi, Δx, n, x) @inbounds xi ≤ x[1] && return 1 @@ -47,7 +46,7 @@ end """ function interp1d_loc_factor(xi::FT, x::AbstractArray{FT, 1}) where {FT <: AbstractFloat} -Computes the weights for linear interpolation. This works with non-uniformly spaced `x`. +Compute the weights for linear interpolation. This works with non-uniformly spaced `x`. """ @inline function interp1d_loc_factor( xi::FT, @@ -111,7 +110,7 @@ end coeff::FTA3D, s1::FT = FT(1), s2::FT = FT(1), - ) where {FT<:AbstractFloat,FTA4D<:AbstractArray{FT,4}} + ) where {FT<:AbstractFloat,FTA3D<:AbstractArray{FT,3}} Perform 3D linear interpolation. @@ -181,11 +180,11 @@ where, ) end """ - increment_2stream!(τ1::FT, ssa1::FT, g1::FT, τ2::FT, ssa2::FT, g2::FT) where {FT} + increment_2stream(τ1::FT, ssa1::FT, g1::FT, τ2::FT, ssa2::FT, g2::FT) where {FT} -Increment TwoStream optical properties `τ1`, `ssa1` and `g1` +Increment TwoStream optical properties `τ1`, `ssa1` and `g1` with `τ2`, `ssa2` and `g2`. Here `τ` is the optical thickness, -`ssa` is the single-scattering albedo, and `g` is the symmetry parameter. +`ssa` is the single-scattering albedo, and `g` is the asymmetry parameter. """ function increment_2stream( τ1::FT, diff --git a/src/rte/RTE.jl b/src/rte/RTE.jl index 17c26e87c..92aaa2066 100644 --- a/src/rte/RTE.jl +++ b/src/rte/RTE.jl @@ -3,7 +3,6 @@ using Adapt import ClimaComms using ..AngularDiscretizations using ..AtmosphericStates -using DocStringExtensions using ..Sources using ..Fluxes using ..Optics @@ -22,7 +21,13 @@ properties, sources, boundary conditions and fluxes configurations for a non-scattering longwave simulation. # Fields -$(DocStringExtensions.FIELDS) +- `context`: ClimaComms context. +- `op`: Optical properties. +- `src`: Longwave source functions. +- `bcs`: Longwave boundary conditions. +- `fluxb`: Temporary storage for bandwise calculations. +- `flux`: Longwave fluxes. +- `angle_disc`: Angular discretization. """ struct NoScatLWRTE{ C, @@ -33,19 +38,12 @@ struct NoScatLWRTE{ FXL <: FluxLW, AD, } - "ClimaComms context" context::C - "optical properties" op::OP - "longwave source functions" src::SL - "longwave boundary conditions" bcs::BC - "temporary storage for bandwise calculations" fluxb::FXBL - "longwave fluxes" flux::FXL - "Angular discretization" angle_disc::AD end Adapt.@adapt_structure NoScatLWRTE @@ -69,7 +67,13 @@ properties, sources, boundary conditions and fluxes configurations for a `2-stream` longwave simulation. # Fields -$(DocStringExtensions.FIELDS) +- `context`: ClimaComms context. +- `op`: Optical properties. +- `src`: Longwave source functions. +- `bcs`: Longwave boundary conditions. +- `fluxb`: Temporary storage for bandwise calculations. +- `flux`: Longwave fluxes. +- `band_flux`: Optional per-band longwave fluxes `(nlev, ncol, n_bnd)`, or `nothing`. """ struct TwoStreamLWRTE{ C, @@ -80,19 +84,12 @@ struct TwoStreamLWRTE{ FXL <: FluxLW, FXBND, } - "ClimaComms context" context::C - "optical properties" op::OP - "longwave source functions" src::SL - "longwave boundary conditions" bcs::BC - "temporary storage for bandwise calculations" fluxb::FXBL - "longwave fluxes" flux::FXL - "optional per-band longwave fluxes `(nlev, ncol, n_bnd)`, or `nothing`" band_flux::FXBND end Adapt.@adapt_structure TwoStreamLWRTE @@ -123,18 +120,17 @@ properties, sources, boundary conditions and fluxes configurations for a non-scattering shortwave simulation. # Fields -$(DocStringExtensions.FIELDS) +- `context`: ClimaComms context. +- `op`: Optical properties. +- `bcs`: Shortwave boundary conditions. +- `fluxb`: Temporary storage for bandwise calculations. +- `flux`: Shortwave fluxes. """ struct NoScatSWRTE{C, OP <: OneScalar, BC <: SwBCs, FXBS, FXS <: FluxSW} - "ClimaComms context" context::C - "optical properties" op::OP - "shortwave boundary conditions" bcs::BC - "temporary storage for bandwise calculations" fluxb::FXBS - "shortwave fluxes" flux::FXS end Adapt.@adapt_structure NoScatSWRTE @@ -169,7 +165,13 @@ properties, sources, boundary conditions and fluxes configurations for a `2-stream` shortwave simulation. # Fields -$(DocStringExtensions.FIELDS) +- `context`: ClimaComms context. +- `op`: Optical properties. +- `src`: Shortwave source functions. +- `bcs`: Shortwave boundary conditions. +- `fluxb`: Temporary storage for bandwise calculations. +- `flux`: Shortwave fluxes. +- `band_flux`: Optional per-band shortwave fluxes `(nlev, ncol, n_bnd)`, or `nothing`. """ struct TwoStreamSWRTE{ C, @@ -180,19 +182,12 @@ struct TwoStreamSWRTE{ FXS <: FluxSW, FXBND, } - "ClimaComms context" context::C - "optical properties" op::OP - "shortwave source functions" src::SS - "shortwave boundary conditions" bcs::BC - "temporary storage for bandwise calculations" fluxb::FXBS - "shortwave fluxes" flux::FXS - "optional per-band shortwave fluxes `(nlev, ncol, n_bnd)`, or `nothing`" band_flux::FXBND end Adapt.@adapt_structure TwoStreamSWRTE diff --git a/src/rte/RTESolver.jl b/src/rte/RTESolver.jl index 210595d65..358622e30 100644 --- a/src/rte/RTESolver.jl +++ b/src/rte/RTESolver.jl @@ -24,7 +24,7 @@ include("shortwave_2stream.jl") """ - solve_lw!((; context, flux, src, bcs, op)::NoScatLWRTE, as::GrayAtmosphericState, metric_scaling::M = nothing) + solve_lw!((; context, flux, src, bcs, op, angle_disc)::NoScatLWRTE, as::GrayAtmosphericState, metric_scaling::M = nothing) Non-scattering RTE solver for the longwave problem, using gray optics. Additionally, takes an optional argument `metric_scaling` which scales the resultant fluxes by the @@ -57,7 +57,7 @@ end """ solve_lw!( - (; context, fluxb, flux, src, bcs, op)::NoScatLWRTE, + (; context, fluxb, flux, src, bcs, op, angle_disc)::NoScatLWRTE, as::AtmosphericState, lookup_lw::LookUpLW, lookup_lw_cld::Union{LookUpCld, Nothing}, @@ -95,7 +95,7 @@ end """ solve_lw!( - (; context, fluxb, flux, src, bcs, op)::TwoStreamLWRTE, + (; context, fluxb, flux, band_flux, src, bcs, op)::TwoStreamLWRTE, as::AtmosphericState, lookup_lw::LookUpLW, lookup_lw_cld::Union{LookUpCld, Nothing}, @@ -188,7 +188,7 @@ end """ solve_sw!( - (; context, fluxb, flux, src, bcs, op)::TwoStreamSWRTE, + (; context, fluxb, flux, band_flux, src, bcs, op)::TwoStreamSWRTE, as::AtmosphericState, lookup_sw::LookUpSW, lookup_sw_cld::Union{LookUpCld, Nothing}, diff --git a/src/rte/longwave_2stream.jl b/src/rte/longwave_2stream.jl index db5f9c4c1..c67c89a36 100644 --- a/src/rte/longwave_2stream.jl +++ b/src/rte/longwave_2stream.jl @@ -138,9 +138,9 @@ end """ lw_2stream_coeffs(τ::FT, ssa::FT, g::FT, lev_src_bot::FT, lev_src_top::FT) where {FT} -This function combines RRTMGP-specific sources at levels, -computes layer reflectance, transmittance, and -total source function at levels using linear-in-tau approximation. +Combine RRTMGP-specific sources at levels and compute the layer reflectance, +transmittance, and total source function at levels, using the linear-in-tau +approximation. """ @inline function lw_2stream_coeffs(τ, ssa, g, lev_src_bot, lev_src_top) FT = eltype(τ) diff --git a/src/rte/shortwave_2stream.jl b/src/rte/shortwave_2stream.jl index 74ad7679a..b55898f69 100644 --- a/src/rte/shortwave_2stream.jl +++ b/src/rte/shortwave_2stream.jl @@ -175,7 +175,7 @@ end """ sw_2stream_coeffs(τ::FT, ssa::FT, g::FT, μ₀::FT) where {FT} -Computes cell properties (transmittance and reflectance) for direct and diffuse radiation +Compute cell properties (transmittance and reflectance) for direct and diffuse radiation Two-stream solutions to direct and diffuse reflectance and transmittance for a layer with optical depth tau, single scattering albedo w0, and asymmetery parameter g. @@ -254,9 +254,9 @@ doi:10.1175/1520-0469(1980)037<0630:TSATRT>2.0.CO;2 # the beam can either be reflected, penetrate unscattered to the base of a layer, # or penetrate through but be scattered on the way - the rest is absorbed # Makes the equations safer in single precision. Credit: Robin Hogan, Peter Ukkonen - # Note: Unlike Eq. 9 and 10 in Hogan et al. (2024), "Improving the Two-Stream - # Approximation in RRTMGP" (doi:10.1029/2023MS003932) where the upper bounds are - # scaled by μ₀ (the cosine of the solar zenith angle), here they are not scaled. + # Note: Unlike Eqs. 9 and 10 in Ukkonen & Hogan (2024), "Twelve times faster yet + # accurate ..." (doi:10.1029/2023MS003932), where the upper bounds are + # scaled by μ₀ (the cosine of the solar zenith angle), here they are not scaled. # This is because in RRTMGP, Rdir and Tdir are defined as fractional quantities # relative to the horizontal incident direct flux (which already includes the μ₀ # factor), whereas in ecRad (the model in the paper), they are normalized relative diff --git a/src/rte/shortwave_noscat.jl b/src/rte/shortwave_noscat.jl index e54030855..b0d10530f 100644 --- a/src/rte/shortwave_noscat.jl +++ b/src/rte/shortwave_noscat.jl @@ -104,13 +104,15 @@ end flux::FluxSW, op::OneScalar, bcs_sw::SwBCs, + igpt::Int, + n_gpt::Int, solar_frac::AbstractFloat, gcol, nlev, ) No-scattering solver for the shortwave problem. -(Extinction-only i.e. solar direct beam) +(Extinction-only i.e., solar direct beam) """ @inline function rte_sw_noscat!( flux::FluxSW, diff --git a/test/Project.toml b/test/Project.toml index 861d1b33a..aee877163 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -8,7 +8,6 @@ ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d" ClimaParams = "5c42b081-d73a-476f-9059-fd94b934656c" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" Coverage = "a2441757-f6aa-5fb2-8edb-039e3f45d037" -DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -16,6 +15,7 @@ Profile = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" ProfileCanvas = "efd6af41-a80b-495e-886c-e51b0c7d77a3" RRTMGP = "a01a1ee8-cea4-48fc-987c-fc7878d79da1" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/all_sky_with_aerosols_dyamond_gpu_benchmark.jl b/test/all_sky_with_aerosols_dyamond_gpu_benchmark.jl index 302f68c88..d5b13fb61 100644 --- a/test/all_sky_with_aerosols_dyamond_gpu_benchmark.jl +++ b/test/all_sky_with_aerosols_dyamond_gpu_benchmark.jl @@ -28,12 +28,99 @@ using RRTMGP.ArtifactPaths include("reference_files.jl") include("read_all_sky_with_aerosols.jl") +if !@isdefined(_resample_benchmark_as) + function _resample_benchmark_as( + context, + as::AtmosphericStates.AtmosphericState, + target_nlay, + ::Type{FT}, + ) where {FT} + nlay_orig, _ = AtmosphericStates.get_dims(as) + nlay_orig == target_nlay && return as + device = ClimaComms.device(context) + DA = ClimaComms.array_type(device) + nlev_orig = nlay_orig + 1 + target_nlev = target_nlay + 1 + + lay_idx = [ + clamp(round(Int, k * nlay_orig / target_nlay), 1, nlay_orig) for + k in 1:target_nlay + ] + lev_idx = [ + clamp(round(Int, l * nlev_orig / target_nlev), 1, nlev_orig) for + l in 1:target_nlev + ] + + new_layerdata = DA{FT}(Array(as.layerdata)[:, lay_idx, :]) + new_p_lev = DA{FT}(Array(as.p_lev)[lev_idx, :]) + new_t_lev = DA{FT}(Array(as.t_lev)[lev_idx, :]) + + vmr = as.vmr + new_vmr = if vmr isa VolumeMixingRatios.VmrGM + VolumeMixingRatios.VmrGM( + DA{FT}(Array(vmr.vmr_h2o)[lay_idx, :]), + DA{FT}(Array(vmr.vmr_o3)[lay_idx, :]), + vmr.vmr, + ) + elseif vmr isa VolumeMixingRatios.Vmr + VolumeMixingRatios.Vmr(DA{FT}(Array(vmr.vmr)[:, lay_idx, :])) + else + vmr + end + + cld = as.cloud_state + new_cld = if cld !== nothing + AtmosphericStates.CloudState( + DA{FT}(Array(cld.cld_r_eff_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_r_eff_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_frac)[lay_idx, :]), + cld.cld_cover_sw, + cld.cld_cover_lw, + DA{Bool}(Array(cld.mask_lw)[lay_idx, :]), + DA{Bool}(Array(cld.mask_sw)[lay_idx, :]), + cld.mask_type, + cld.ice_rgh, + ) + else + nothing + end + + aer = as.aerosol_state + new_aer = if aer !== nothing + AtmosphericStates.AerosolState( + aer.aod_sw_ext, + aer.aod_sw_sca, + DA{Bool}(Array(aer.aero_mask)[lay_idx, :]), + DA{FT}(Array(aer.aero_size)[:, lay_idx, :]), + DA{FT}(Array(aer.aero_mass)[:, lay_idx, :]), + ) + else + nothing + end + + return AtmosphericStates.AtmosphericState( + as.lon, + as.lat, + new_layerdata, + new_p_lev, + new_t_lev, + as.t_sfc, + new_vmr, + new_cld, + new_aer, + ) + end +end + function benchmark_all_sky_with_aerosols( context, ::Type{SLVLW}, ::Type{SLVSW}, ::Type{FT}; ncol = 128,# repeats col#1 ncol times per RRTMGP example + nlay = nothing, cldfrac = FT(1), ) where {FT <: AbstractFloat, SLVLW, SLVSW} overrides = @@ -105,7 +192,9 @@ function benchmark_all_sky_with_aerosols( param_set, ) close(ds_in) - + if nlay !== nothing + as = _resample_benchmark_as(context, as, nlay, FT) + end nlay, _ = AtmosphericStates.get_dims(as) nlev = nlay + 1 grid_params = RRTMGPGridParams(FT; context, domain_nlay = nlay, ncol) @@ -132,14 +221,25 @@ function benchmark_all_sky_with_aerosols( lookup_lw_aero, metric_scaling, ) - trial_lw = @benchmark CUDA.@sync solve_lw!( - $slv_lw, - $as, - $lookup_lw, - $lookup_lw_cld, - $lookup_lw_aero, - $metric_scaling, - ) + trial_lw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_lw!( + $slv_lw, + $as, + $lookup_lw, + $lookup_lw_cld, + $lookup_lw_aero, + $metric_scaling, + ) + else + @benchmark solve_lw!( + $slv_lw, + $as, + $lookup_lw, + $lookup_lw_cld, + $lookup_lw_aero, + $metric_scaling, + ) + end solve_sw!( slv_sw, @@ -149,14 +249,25 @@ function benchmark_all_sky_with_aerosols( lookup_sw_aero, metric_scaling, ) - trial_sw = @benchmark CUDA.@sync solve_sw!( - $slv_sw, - $as, - $lookup_sw, - $lookup_sw_cld, - $lookup_sw_aero, - $metric_scaling, - ) + trial_sw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_sw!( + $slv_sw, + $as, + $lookup_sw, + $lookup_sw_cld, + $lookup_sw_aero, + $metric_scaling, + ) + else + @benchmark solve_sw!( + $slv_sw, + $as, + $lookup_sw, + $lookup_sw_cld, + $lookup_sw_aero, + $metric_scaling, + ) + end return trial_lw, trial_sw end @@ -218,17 +329,22 @@ function generate_gpu_all_sky_with_aerosols_benchmarks( return nothing end -for FT in (Float32, Float64) - generate_gpu_all_sky_with_aerosols_benchmarks( - FT, - 4, - NoScatLWRTE, - TwoStreamSWRTE, - ) - generate_gpu_all_sky_with_aerosols_benchmarks( - FT, - 4, - TwoStreamLWRTE, - TwoStreamSWRTE, - ) +# Run the resolution sweep only when executed as a script; +# perf/benchmark_ratchet.jl includes this file for +# `benchmark_all_sky_with_aerosols`. +if abspath(PROGRAM_FILE) == @__FILE__ + for FT in (Float32, Float64) + generate_gpu_all_sky_with_aerosols_benchmarks( + FT, + 4, + NoScatLWRTE, + TwoStreamSWRTE, + ) + generate_gpu_all_sky_with_aerosols_benchmarks( + FT, + 4, + TwoStreamLWRTE, + TwoStreamSWRTE, + ) + end end diff --git a/test/all_sky_with_aerosols_utils.jl b/test/all_sky_with_aerosols_utils.jl index 60f46e728..b777d4b50 100644 --- a/test/all_sky_with_aerosols_utils.jl +++ b/test/all_sky_with_aerosols_utils.jl @@ -184,6 +184,24 @@ function all_sky_with_aerosols( # h2o/o3 are layer fields (2D); well-mixed gases are global means. @test ndims(RRTMGP.volume_mixing_ratio(solver, "h2o")) == 2 + # Cloud radiative effect: the retained clear-sky diagnostics must differ + # from the all-sky fluxes — clouds reduce the outgoing longwave flux at + # the top of the atmosphere and increase the reflected shortwave flux. + olr_allsky = Array(RRTMGP.lw_flux_up(solver))[end, :] + olr_clear = Array(RRTMGP.clear_lw_flux_up(solver))[end, :] + @test all(olr_clear .>= olr_allsky) + @test maximum(olr_clear .- olr_allsky) > 0 + sw_up_allsky = Array(RRTMGP.sw_flux_up(solver))[end, :] + sw_up_clear = Array(RRTMGP.clear_sw_flux_up(solver))[end, :] + @test all(sw_up_allsky .>= sw_up_clear) + @test maximum(sw_up_allsky .- sw_up_clear) > 0 + @test maximum( + abs.( + Array(RRTMGP.clear_net_flux(solver)) .- + Array(RRTMGP.net_flux(solver)) + ), + ) > 0 + # Layer-2 update_fluxes! must stay allocation-free and type-stable on the # all-sky (spectral + clouds + aerosols) path too; the gray path is asserted # in test/standalone.jl. Single-threaded only: multi-threaded `@threaded` diff --git a/test/api_contract.jl b/test/api_contract.jl new file mode 100644 index 000000000..7c07368fa --- /dev/null +++ b/test/api_contract.jl @@ -0,0 +1,306 @@ +using Test +import ClimaComms +@static pkgversion(ClimaComms) >= v"0.6" && ClimaComms.@import_required_backends +import RRTMGP +import RRTMGP.VolumeMixingRatios: Vmr, VmrGM +import Serialization + +# Unit tests for the Layer-2 API contract: constructor validation, the +# informative getter-guard errors, input validation, boundary conditions, and +# the physical laws behind heating_rate and the deep-atmosphere flux scaling. + +# Small gray problem shared by these tests (the gray path needs no lookup +# tables, so every case here runs after a bare `using RRTMGP`). +function _gray_pieces(FT; nlay = 20, ncol = 4) + context = ClimaComms.context() + DA = ClimaComms.array_type(ClimaComms.device(context)) + gp = RRTMGP.RRTMGPGridParams(FT; context, domain_nlay = nlay, ncol) + params = RRTMGP.default_parameters(FT) + lat = DA{FT}(range(FT(-80), FT(80); length = ncol)) + as = RRTMGP.AtmosphericStates.setup_gray_as_pr_grid( + context, + nlay, + lat, + FT(1.0e5), + FT(9.0e3), + RRTMGP.AtmosphericStates.GrayOpticalThicknessOGorman2008(FT), + params, + DA, + ) + sfc_emis = fill!(DA{FT}(undef, 1, ncol), FT(1)) + cos_zen = fill!(DA{FT}(undef, ncol), FT(0.5)) + toa = fill!(DA{FT}(undef, ncol), FT(1361)) + alb_dir = fill!(DA{FT}(undef, 1, ncol), FT(0.2)) + alb_dif = fill!(DA{FT}(undef, 1, ncol), FT(0.2)) + bcs_sw = RRTMGP.BCs.SwBCs(cos_zen, toa, alb_dir, nothing, alb_dif) + return (; context, DA, gp, params, as, sfc_emis, bcs_sw) +end + +function _gray_solver(FT; nlay = 20, ncol = 4, inc_flux = nothing, kwargs...) + (; gp, params, as, sfc_emis, bcs_sw) = _gray_pieces(FT; nlay, ncol) + bcs_lw = RRTMGP.BCs.LwBCs(sfc_emis, inc_flux) + return RRTMGP.RRTMGPSolver( + gp, + RRTMGP.GrayRadiation(), + params, + bcs_lw, + bcs_sw, + as; + kwargs..., + ) +end + +@testset "RRTMGPSolver constructor validation" begin + (; gp, params, as, sfc_emis, bcs_sw) = _gray_pieces(Float64) + bcs_lw = RRTMGP.BCs.LwBCs(sfc_emis, nothing) + + # z-based interpolation/extrapolation without altitudes fails at + # construction (not with a MethodError mid-solve) + @test_throws "requires altitudes" RRTMGP.RRTMGPSolver( + gp, + RRTMGP.GrayRadiation(), + params, + bcs_lw, + bcs_sw, + as; + interpolation = RRTMGP.BestFit(), + ) + @test_throws "requires altitudes" RRTMGP.RRTMGPSolver( + gp, + RRTMGP.GrayRadiation(), + params, + bcs_lw, + bcs_sw, + as; + bottom_extrapolation = RRTMGP.HydrostaticBottom(), + ) + + # per-band fluxes are meaningless for a single-band (gray) atmosphere + @test_throws "not supported for GrayRadiation" RRTMGP.RRTMGPSolver( + gp, + RRTMGP.GrayRadiation(), + params, + bcs_lw, + bcs_sw, + as; + spectral_fluxes = true, + ) + + # ... and require two-stream optics for both bands. The guard fires before + # the lookup tables or atmospheric state are inspected, so a placeholder + # bundle and the gray state suffice (no NetCDF read). + placeholder = RRTMGP.LookupBundle(; nbnd_lw = 16, nbnd_sw = 14) + @test_throws "requires two-stream optics" RRTMGP.RRTMGPSolver( + gp, + RRTMGP.ClearSkyRadiation(false), + params, + bcs_lw, + bcs_sw, + as; + op_lw = RRTMGP.Optics.OneScalar(gp), + lookups = placeholder, + spectral_fluxes = true, + ) +end + +@testset "getter guards raise informative errors (gray solver)" begin + solver = _gray_solver(Float64) + + # optional workspaces that this solver was not built with + @test_throws "clear-sky diagnostic fluxes" RRTMGP.clear_net_flux(solver) + @test_throws "clear-sky diagnostic fluxes" RRTMGP.clear_lw_flux_up(solver) + @test_throws "clear-sky diagnostic fluxes" RRTMGP.clear_sw_flux_dn(solver) + @test_throws "spectral lookup tables are not present" RRTMGP.lw_band_bounds( + solver, + ) + @test_throws "spectral lookup tables are not present" RRTMGP.sw_band_bounds( + solver, + ) + @test_throws "aerosol lookup tables are not present" RRTMGP.aerosol_radius( + solver, + "dust1", + ) + @test_throws "aerosol lookup tables are not present" RRTMGP.aerosol_column_mass_density( + solver, + "dust1", + ) + @test_throws "spectral fluxes were not retained" RRTMGP.spectral_lw_flux_up( + solver, + ) + @test_throws "spectral fluxes were not retained" RRTMGP.spectral_sw_flux_dn( + solver, + ) + + # configuration getters on the same solver + @test RRTMGP.radiation_method(solver) isa RRTMGP.GrayRadiation + @test RRTMGP.optical_thickness_parameter(solver) isa + RRTMGP.AtmosphericStates.GrayOpticalThicknessOGorman2008 + @test RRTMGP.isothermal_boundary_layer(solver) == false + @test RRTMGP.top_of_atmosphere_lw_flux_dn(solver) === nothing + @test RRTMGP.top_of_atmosphere_diffuse_sw_flux_dn(solver) === nothing +end + +@testset "validate_inputs names the offending field" begin + # each case corrupts one input on a fresh solver and asserts the error + # message identifies it + cases = [ + ("level_pressure", s -> RRTMGP.level_pressure(s) .= -1), + ("level_temperature", s -> RRTMGP.level_temperature(s) .= NaN), + ("layer_pressure", s -> RRTMGP.layer_pressure(s) .= 0), + ("layer_temperature", s -> RRTMGP.layer_temperature(s) .= -5), + ("surface_temperature", s -> RRTMGP.surface_temperature(s) .= 0), + ("toa_flux", s -> RRTMGP.toa_flux(s) .= -1), + ("surface_emissivity", s -> RRTMGP.surface_emissivity(s) .= 2), + ( + "direct_sw_surface_albedo", + s -> RRTMGP.direct_sw_surface_albedo(s) .= 2, + ), + ( + "diffuse_sw_surface_albedo", + s -> RRTMGP.diffuse_sw_surface_albedo(s) .= -1, + ), + ] + for (name, corrupt!) in cases + solver = _gray_solver(Float64; nlay = 10, ncol = 2) + RRTMGP.validate_inputs(solver) # sane inputs pass + corrupt!(solver) + @test_throws name RRTMGP.validate_inputs(solver) + end +end + +@testset "_check_vmr rejects negative / non-finite mixing ratios" begin + vmr_gm = VmrGM(zeros(2, 2), zeros(2, 2), zeros(3)) + @test RRTMGP._check_vmr(vmr_gm) == true + vmr_gm.vmr_h2o[1] = -1 + @test_throws "vmr_h2o" RRTMGP._check_vmr(vmr_gm) + vmr_gm.vmr_h2o[1] = 0 + vmr_gm.vmr_o3[2] = NaN + @test_throws "vmr_o3" RRTMGP._check_vmr(vmr_gm) + + vmr = Vmr(zeros(3, 2, 2)) + @test RRTMGP._check_vmr(vmr) == true + vmr.vmr[1] = -1 + @test_throws "vmr" RRTMGP._check_vmr(vmr) + + @test RRTMGP._check_vmr(nothing) == true +end + +# Physical law: the downwelling longwave flux at the top level equals the +# prescribed incident flux (the LW top boundary condition); with no incident +# flux it is zero. Also pins the (ncol, ngpt) → physical-dim-first transpose +# contract of the TOA getters. +@testset "incident longwave flux boundary condition" begin + for FT in (Float32, Float64) + pieces = _gray_pieces(FT) + ncol = size(pieces.sfc_emis, 2) + + solver0 = _gray_solver(FT) + RRTMGP.update_fluxes!(solver0) + @test all(iszero, Array(RRTMGP.lw_flux_dn(solver0))[end, :]) + + inc = fill!(pieces.DA{FT}(undef, ncol, 1), FT(25)) + solver = _gray_solver(FT; inc_flux = inc) + toa_dn = RRTMGP.top_of_atmosphere_lw_flux_dn(solver) + @test size(toa_dn) == (1, ncol) + @test all(==(FT(25)), Array(toa_dn)) + + RRTMGP.update_fluxes!(solver) + @test all(Array(RRTMGP.lw_flux_dn(solver))[end, :] .≈ FT(25)) + # the incident flux warms the surface's incoming longwave radiation + @test all( + Array(RRTMGP.lw_flux_dn(solver))[1, :] .> + Array(RRTMGP.lw_flux_dn(solver0))[1, :], + ) + end +end + +# Physical law: `deep_atmosphere_inverse_scaling` multiplies the fluxes +# pointwise (geometric expansion of grid columns aloft), so a uniform factor c +# must scale every flux by exactly c relative to the unscaled solve. +@testset "deep-atmosphere metric scaling multiplies fluxes" begin + for FT in (Float32, Float64) + nlay, ncol = 20, 4 + base = _gray_solver(FT; nlay, ncol) + RRTMGP.update_fluxes!(base) + + pieces = _gray_pieces(FT; nlay, ncol) + scaling = fill!(pieces.DA{FT}(undef, nlay + 1, ncol), FT(2)) + bcs_lw = RRTMGP.BCs.LwBCs(pieces.sfc_emis, nothing) + scaled = RRTMGP.RRTMGPSolver( + pieces.gp, + RRTMGP.GrayRadiation(), + pieces.params, + bcs_lw, + pieces.bcs_sw, + pieces.as; + deep_atmosphere_inverse_scaling = scaling, + ) + @test RRTMGP.deep_atmosphere_inverse_scaling(scaled) !== nothing + RRTMGP.update_fluxes!(scaled) + + for getter in ( + RRTMGP.lw_flux_up, + RRTMGP.lw_flux_dn, + RRTMGP.lw_flux_net, + RRTMGP.sw_flux_up, + RRTMGP.sw_flux_dn, + RRTMGP.sw_direct_flux_dn, + RRTMGP.sw_flux_net, + RRTMGP.net_flux, + ) + @test Array(getter(scaled)) ≈ 2 .* Array(getter(base)) + end + end +end + +# Physical law: the radiative heating rate is the net-flux divergence in +# pressure coordinates, hr = (g/cₚ) ∂F_net/∂p. Recompute the finite difference +# from the getters and compare against heating_rate(s). +@testset "heating_rate matches the net-flux divergence" begin + for FT in (Float32, Float64) + out = RRTMGP.solve_gray(FT; nlay = 20, ncol = 3) + params = out.solver.params + g = RRTMGP.Parameters.grav(params) + cₚ = RRTMGP.Parameters.cp_d(params) + F = Array(out.net) + p = Array(RRTMGP.level_pressure(out.solver)) + expected = + (g / cₚ) .* (F[2:end, :] .- F[1:(end - 1), :]) ./ + (p[2:end, :] .- p[1:(end - 1), :]) + @test Array(out.heating_rate) ≈ expected + end +end + +@testset "gray clip! floors pressures and leaves temperatures alone" begin + FT = Float64 + nlay, ncol = 3, 2 + p_lay = FT[50 60; 500 510; 5000 5010] + p_lev = FT[10 20; 100 110; 1000 1010; 10000 10010] + t_lay = fill(FT(150), nlay, ncol) + t_lev = fill(FT(150), nlay + 1, ncol) + z_lev = zeros(FT, nlay + 1, ncol) + as = RRTMGP.AtmosphericStates.GrayAtmosphericState( + zeros(FT, ncol), + p_lay, + p_lev, + t_lay, + t_lev, + z_lev, + fill(FT(300), ncol), + RRTMGP.AtmosphericStates.GrayOpticalThicknessOGorman2008(FT), + ) + p_min = FT(100) + # the temperature clamp bounds are ignored for the gray state: its analytic + # optics remain valid outside the lookup-table range + @test RRTMGP.clip!(as, p_min; t_min = FT(160), t_max = FT(355)) === as + @test all(≥(p_min), as.p_lay) + @test all(≥(p_min), as.p_lev) + @test as.p_lay[3, 1] == 5000 # values above the floor are untouched + @test all(==(FT(150)), as.t_lay) + @test all(==(FT(150)), as.t_lev) + + # update_concentrations! is a no-op for gray radiation + device = ClimaComms.device(ClimaComms.context()) + params = RRTMGP.default_parameters(FT) + @test RRTMGP.update_concentrations!(as, params, device) === as +end diff --git a/test/clear_sky_dyamond_gpu_benchmark.jl b/test/clear_sky_dyamond_gpu_benchmark.jl index 48d132691..b6722f15c 100644 --- a/test/clear_sky_dyamond_gpu_benchmark.jl +++ b/test/clear_sky_dyamond_gpu_benchmark.jl @@ -28,6 +28,92 @@ using RRTMGP.ArtifactPaths include("reference_files.jl") include("read_clear_sky.jl") +if !@isdefined(_resample_benchmark_as) + function _resample_benchmark_as( + context, + as::AtmosphericStates.AtmosphericState, + target_nlay, + ::Type{FT}, + ) where {FT} + nlay_orig, _ = AtmosphericStates.get_dims(as) + nlay_orig == target_nlay && return as + device = ClimaComms.device(context) + DA = ClimaComms.array_type(device) + nlev_orig = nlay_orig + 1 + target_nlev = target_nlay + 1 + + lay_idx = [ + clamp(round(Int, k * nlay_orig / target_nlay), 1, nlay_orig) for + k in 1:target_nlay + ] + lev_idx = [ + clamp(round(Int, l * nlev_orig / target_nlev), 1, nlev_orig) for + l in 1:target_nlev + ] + + new_layerdata = DA{FT}(Array(as.layerdata)[:, lay_idx, :]) + new_p_lev = DA{FT}(Array(as.p_lev)[lev_idx, :]) + new_t_lev = DA{FT}(Array(as.t_lev)[lev_idx, :]) + + vmr = as.vmr + new_vmr = if vmr isa VolumeMixingRatios.VmrGM + VolumeMixingRatios.VmrGM( + DA{FT}(Array(vmr.vmr_h2o)[lay_idx, :]), + DA{FT}(Array(vmr.vmr_o3)[lay_idx, :]), + vmr.vmr, + ) + elseif vmr isa VolumeMixingRatios.Vmr + VolumeMixingRatios.Vmr(DA{FT}(Array(vmr.vmr)[:, lay_idx, :])) + else + vmr + end + + cld = as.cloud_state + new_cld = if cld !== nothing + AtmosphericStates.CloudState( + DA{FT}(Array(cld.cld_r_eff_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_r_eff_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_frac)[lay_idx, :]), + cld.cld_cover_sw, + cld.cld_cover_lw, + DA{Bool}(Array(cld.mask_lw)[lay_idx, :]), + DA{Bool}(Array(cld.mask_sw)[lay_idx, :]), + cld.mask_type, + cld.ice_rgh, + ) + else + nothing + end + + aer = as.aerosol_state + new_aer = if aer !== nothing + AtmosphericStates.AerosolState( + aer.aod_sw_ext, + aer.aod_sw_sca, + DA{Bool}(Array(aer.aero_mask)[lay_idx, :]), + DA{FT}(Array(aer.aero_size)[:, lay_idx, :]), + DA{FT}(Array(aer.aero_mass)[:, lay_idx, :]), + ) + else + nothing + end + + return AtmosphericStates.AtmosphericState( + as.lon, + as.lat, + new_layerdata, + new_p_lev, + new_t_lev, + as.t_sfc, + new_vmr, + new_cld, + new_aer, + ) + end +end + function benchmark_clear_sky( context, ::Type{SLVLW}, @@ -35,6 +121,7 @@ function benchmark_clear_sky( ::Type{VMR}, ::Type{FT}; ncol = 100, + nlay = nothing, ) where {FT <: AbstractFloat, SLVLW, SLVSW, VMR} overrides = (; grav = 9.80665, molmass_dryair = 0.028964, molmass_water = 0.018016) @@ -77,6 +164,9 @@ function benchmark_clear_sky( ) close(ds_lw_in) + if nlay !== nothing + as = _resample_benchmark_as(context, as, nlay, FT) + end nlay, _ = AtmosphericStates.get_dims(as) nlev = nlay + 1 grid_params = RRTMGPGridParams(FT; context, domain_nlay = nlay, ncol) @@ -100,24 +190,46 @@ function benchmark_clear_sky( #------calling solvers metric_scaling = DA(one.(slv_sw.flux.flux_up)) solve_lw!(slv_lw, as, lookup_lw, nothing, nothing, metric_scaling) - trial_lw = @benchmark CUDA.@sync solve_lw!( - $slv_lw, - $as, - $lookup_lw, - nothing, - nothing, - $metric_scaling, - ) + trial_lw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_lw!( + $slv_lw, + $as, + $lookup_lw, + nothing, + nothing, + $metric_scaling, + ) + else + @benchmark solve_lw!( + $slv_lw, + $as, + $lookup_lw, + nothing, + nothing, + $metric_scaling, + ) + end solve_sw!(slv_sw, as, lookup_sw, nothing, nothing, metric_scaling) - trial_sw = @benchmark CUDA.@sync solve_sw!( - $slv_sw, - $as, - $lookup_sw, - nothing, - nothing, - $metric_scaling, - ) + trial_sw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_sw!( + $slv_sw, + $as, + $lookup_sw, + nothing, + nothing, + $metric_scaling, + ) + else + @benchmark solve_sw!( + $slv_sw, + $as, + $lookup_sw, + nothing, + nothing, + $metric_scaling, + ) + end return trial_lw, trial_sw end @@ -174,13 +286,23 @@ function generate_gpu_clear_sky_benchmarks( return nothing end -for FT in (Float32, Float64) - generate_gpu_clear_sky_benchmarks(FT, 4, NoScatLWRTE, TwoStreamSWRTE, VmrGM) - generate_gpu_clear_sky_benchmarks( - FT, - 4, - TwoStreamLWRTE, - TwoStreamSWRTE, - VmrGM, - ) +# Run the resolution sweep only when executed as a script; +# perf/benchmark_ratchet.jl includes this file for `benchmark_clear_sky`. +if abspath(PROGRAM_FILE) == @__FILE__ + for FT in (Float32, Float64) + generate_gpu_clear_sky_benchmarks( + FT, + 4, + NoScatLWRTE, + TwoStreamSWRTE, + VmrGM, + ) + generate_gpu_clear_sky_benchmarks( + FT, + 4, + TwoStreamLWRTE, + TwoStreamSWRTE, + VmrGM, + ) + end end diff --git a/test/cloudy_sky_dyamond_gpu_benchmark.jl b/test/cloudy_sky_dyamond_gpu_benchmark.jl index 9e255e15a..6fc4d7517 100644 --- a/test/cloudy_sky_dyamond_gpu_benchmark.jl +++ b/test/cloudy_sky_dyamond_gpu_benchmark.jl @@ -28,12 +28,99 @@ using RRTMGP.ArtifactPaths include("reference_files.jl") include("read_cloudy_sky.jl") +if !@isdefined(_resample_benchmark_as) + function _resample_benchmark_as( + context, + as::AtmosphericStates.AtmosphericState, + target_nlay, + ::Type{FT}, + ) where {FT} + nlay_orig, _ = AtmosphericStates.get_dims(as) + nlay_orig == target_nlay && return as + device = ClimaComms.device(context) + DA = ClimaComms.array_type(device) + nlev_orig = nlay_orig + 1 + target_nlev = target_nlay + 1 + + lay_idx = [ + clamp(round(Int, k * nlay_orig / target_nlay), 1, nlay_orig) for + k in 1:target_nlay + ] + lev_idx = [ + clamp(round(Int, l * nlev_orig / target_nlev), 1, nlev_orig) for + l in 1:target_nlev + ] + + new_layerdata = DA{FT}(Array(as.layerdata)[:, lay_idx, :]) + new_p_lev = DA{FT}(Array(as.p_lev)[lev_idx, :]) + new_t_lev = DA{FT}(Array(as.t_lev)[lev_idx, :]) + + vmr = as.vmr + new_vmr = if vmr isa VolumeMixingRatios.VmrGM + VolumeMixingRatios.VmrGM( + DA{FT}(Array(vmr.vmr_h2o)[lay_idx, :]), + DA{FT}(Array(vmr.vmr_o3)[lay_idx, :]), + vmr.vmr, + ) + elseif vmr isa VolumeMixingRatios.Vmr + VolumeMixingRatios.Vmr(DA{FT}(Array(vmr.vmr)[:, lay_idx, :])) + else + vmr + end + + cld = as.cloud_state + new_cld = if cld !== nothing + AtmosphericStates.CloudState( + DA{FT}(Array(cld.cld_r_eff_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_r_eff_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_liq)[lay_idx, :]), + DA{FT}(Array(cld.cld_path_ice)[lay_idx, :]), + DA{FT}(Array(cld.cld_frac)[lay_idx, :]), + cld.cld_cover_sw, + cld.cld_cover_lw, + DA{Bool}(Array(cld.mask_lw)[lay_idx, :]), + DA{Bool}(Array(cld.mask_sw)[lay_idx, :]), + cld.mask_type, + cld.ice_rgh, + ) + else + nothing + end + + aer = as.aerosol_state + new_aer = if aer !== nothing + AtmosphericStates.AerosolState( + aer.aod_sw_ext, + aer.aod_sw_sca, + DA{Bool}(Array(aer.aero_mask)[lay_idx, :]), + DA{FT}(Array(aer.aero_size)[:, lay_idx, :]), + DA{FT}(Array(aer.aero_mass)[:, lay_idx, :]), + ) + else + nothing + end + + return AtmosphericStates.AtmosphericState( + as.lon, + as.lat, + new_layerdata, + new_p_lev, + new_t_lev, + as.t_sfc, + new_vmr, + new_cld, + new_aer, + ) + end +end + function benchmark_all_sky( context, ::Type{SLVLW}, ::Type{SLVSW}, ::Type{FT}; ncol = 128,# repeats col#1 ncol times per RRTMGP example + nlay = nothing, cldfrac = FT(1), ) where {FT <: AbstractFloat, SLVLW, SLVSW} overrides = @@ -88,6 +175,9 @@ function benchmark_all_sky( param_set, ) close(ds_in) + if nlay !== nothing + as = _resample_benchmark_as(context, as, nlay, FT) + end nlay, ncol = AtmosphericStates.get_dims(as) nlev = nlay + 1 grid_params = RRTMGPGridParams(FT; context, domain_nlay = nlay, ncol) @@ -114,24 +204,46 @@ function benchmark_all_sky( #------calling solvers metric_scaling = DA(one.(slv_sw.flux.flux_up)) solve_lw!(slv_lw, as, lookup_lw, lookup_lw_cld, nothing, metric_scaling) - trial_lw = @benchmark CUDA.@sync solve_lw!( - $slv_lw, - $as, - $lookup_lw, - $lookup_lw_cld, - nothing, - $metric_scaling, - ) + trial_lw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_lw!( + $slv_lw, + $as, + $lookup_lw, + $lookup_lw_cld, + nothing, + $metric_scaling, + ) + else + @benchmark solve_lw!( + $slv_lw, + $as, + $lookup_lw, + $lookup_lw_cld, + nothing, + $metric_scaling, + ) + end solve_sw!(slv_sw, as, lookup_sw, lookup_sw_cld, nothing, metric_scaling) - trial_sw = @benchmark CUDA.@sync solve_sw!( - $slv_sw, - $as, - $lookup_sw, - $lookup_sw_cld, - nothing, - $metric_scaling, - ) + trial_sw = if device isa ClimaComms.CUDADevice + @benchmark CUDA.@sync solve_sw!( + $slv_sw, + $as, + $lookup_sw, + $lookup_sw_cld, + nothing, + $metric_scaling, + ) + else + @benchmark solve_sw!( + $slv_sw, + $as, + $lookup_sw, + $lookup_sw_cld, + nothing, + $metric_scaling, + ) + end return trial_lw, trial_sw end @@ -193,7 +305,11 @@ function generate_gpu_allsky_benchmarks( return nothing end -for FT in (Float32, Float64) - generate_gpu_allsky_benchmarks(FT, 4, NoScatLWRTE, TwoStreamSWRTE) - generate_gpu_allsky_benchmarks(FT, 4, TwoStreamLWRTE, TwoStreamSWRTE) +# Run the resolution sweep only when executed as a script; +# perf/benchmark_ratchet.jl includes this file for `benchmark_all_sky`. +if abspath(PROGRAM_FILE) == @__FILE__ + for FT in (Float32, Float64) + generate_gpu_allsky_benchmarks(FT, 4, NoScatLWRTE, TwoStreamSWRTE) + generate_gpu_allsky_benchmarks(FT, 4, TwoStreamLWRTE, TwoStreamSWRTE) + end end diff --git a/test/interpolation_schemes.jl b/test/interpolation_schemes.jl new file mode 100644 index 000000000..5d88ed0b5 --- /dev/null +++ b/test/interpolation_schemes.jl @@ -0,0 +1,279 @@ +using Test +import RRTMGP +import RRTMGP.AtmosphericStates: AtmosphericState +import RRTMGP.VolumeMixingRatios: VmrGM + +# Closed-form unit tests for every level<->layer interpolation and bottom- +# extrapolation scheme in src/api/interpolation.jl. The laws being verified are +# the ones stated in that file's comment block: a constant lapse rate between +# the two known points, hydrostatic balance for isothermal columns, and the +# isentropic p ∝ T^(cₚ/R) law otherwise. + +# Minimal clear-sky AtmosphericState (VmrGM storage), mirroring the helper in +# grid_adaptation.jl but under a different name so both files can be included +# into one test session. +function _make_interp_as(FT, nlay, ncol) + layerdata = zeros(FT, 4, nlay, ncol) + p_lev = zeros(FT, nlay + 1, ncol) + t_lev = zeros(FT, nlay + 1, ncol) + t_sfc = fill(FT(300), ncol) + vmr = VmrGM(zeros(FT, nlay, ncol), zeros(FT, nlay, ncol), zeros(FT, 3)) + return AtmosphericState( + nothing, + nothing, + layerdata, + p_lev, + t_lev, + t_sfc, + vmr, + nothing, + nothing, + ) +end + +@testset "interp! closed forms (all schemes)" begin + for FT in (Float32, Float64) + pꜜ = FT[100000, 90000] + Tꜜ = FT[300, 290] + pꜛ = FT[80000, 70000] + Tꜛ = FT[280, 270] + p = zeros(FT, 2) + T = zeros(FT, 2) + + # UniformZ: T is the arithmetic mean; p follows the isentropic + # p ∝ T^B law, checked independently by the bounds here and the + # isothermal limit below. + RRTMGP.interp!(RRTMGP.UniformZ(), p, T, pꜜ, Tꜜ, pꜛ, Tꜛ) + @test T ≈ (Tꜜ .+ Tꜛ) ./ 2 + # the interpolated face lies between the adjacent layers + @test all(min.(pꜜ, pꜛ) .< p .< max.(pꜜ, pꜛ)) + @test all(min.(Tꜜ, Tꜛ) .< T .< max.(Tꜜ, Tꜛ)) + + # isothermal limit: hydrostatic balance reduces UniformZ to the + # geometric mean of the pressures. + RRTMGP.interp!(RRTMGP.UniformZ(), p, T, pꜜ, Tꜜ, pꜛ, Tꜜ) + @test T ≈ Tꜜ + @test p ≈ sqrt.(pꜜ .* pꜛ) + + # UniformP: p is the arithmetic mean; T inverts the same power law, + # checked independently by the bounds here and the isothermal limit + # below. + RRTMGP.interp!(RRTMGP.UniformP(), p, T, pꜜ, Tꜜ, pꜛ, Tꜛ) + @test p ≈ (pꜜ .+ pꜛ) ./ 2 + @test all(min.(Tꜜ, Tꜛ) .< T .< max.(Tꜜ, Tꜛ)) + # isothermal limit (independent oracle): an isothermal layer stays + # isothermal, whatever the pressure interpolation. + RRTMGP.interp!(RRTMGP.UniformP(), p, T, pꜜ, Tꜜ, pꜛ, Tꜜ) + @test T ≈ Tꜜ + @test p ≈ (pꜜ .+ pꜛ) ./ 2 + + # BestFit with distinct temperatures: T is linear in z and p follows + # the power law fitted through the two layer points. + z = FT[500] + zꜜ = FT[0] + zꜛ = FT[1000] + pb = zeros(FT, 1) + Tb = zeros(FT, 1) + RRTMGP.interp!( + RRTMGP.BestFit(), + pb, + Tb, + z, + pꜜ[1:1], + Tꜜ[1:1], + zꜜ, + pꜛ[1:1], + Tꜛ[1:1], + zꜛ, + ) + @test Tb ≈ [(Tꜜ[1] + Tꜛ[1]) / 2] # midpoint of a linear profile + # the fitted pressure lies between the two layer pressures; its exact + # value is checked by the dry-adiabat test below. + @test pꜛ[1] < pb[1] < pꜜ[1] + end +end + +@testset "extrap! closed forms (all schemes)" begin + for FT in (Float32, Float64) + params = RRTMGP.default_parameters(FT) + g = RRTMGP.Parameters.grav(params) + cₚ = RRTMGP.Parameters.cp_d(params) + R = RRTMGP.Parameters.R_d(params) + + p⁺ = FT[90000] + T⁺ = FT[285] + p⁺⁺ = FT[80000] + T⁺⁺ = FT[275] + Tₛ = FT[300] + p = zeros(FT, 1) + T = zeros(FT, 1) + + # GeometricMean continues the geometric progression of the two layers + # above to the boundary face half a layer below, so the boundary/nearest + # ratio is the square root of the layer-to-layer ratio. + RRTMGP.extrap!(RRTMGP.GeometricMean(), p, T, p⁺, T⁺, p⁺⁺, T⁺⁺, Tₛ, params) + @test T ./ T⁺ ≈ sqrt.(T⁺ ./ T⁺⁺) + @test p ./ p⁺ ≈ sqrt.(p⁺ ./ p⁺⁺) + @test all(T .> T⁺) && all(p .> p⁺) # continues downward + + # UniformZ: linear T continuation; the isothermal limit independently + # pins the isentropic pressure law to the hydrostatic geometric mean. + RRTMGP.extrap!(RRTMGP.UniformZ(), p, T, p⁺, T⁺, p⁺⁺, T⁺⁺, Tₛ, params) + @test T ≈ (3 .* T⁺ .- T⁺⁺) ./ 2 + @test all(p .> p⁺) # continues downward below the lowest layer + RRTMGP.extrap!(RRTMGP.UniformZ(), p, T, p⁺, T⁺, p⁺⁺, T⁺, Tₛ, params) + @test T ≈ T⁺ + @test p ≈ sqrt.(p⁺ .* p⁺⁺) + + # UniformP: linear p continuation; an isothermal layer extrapolates + # isothermally. + RRTMGP.extrap!(RRTMGP.UniformP(), p, T, p⁺, T⁺, p⁺⁺, T⁺⁺, Tₛ, params) + @test p ≈ (3 .* p⁺ .- p⁺⁺) ./ 2 + @test all(T .> T⁺) # continues downward below the lowest layer + RRTMGP.extrap!(RRTMGP.UniformP(), p, T, p⁺, T⁺, p⁺⁺, T⁺, Tₛ, params) + @test T ≈ T⁺ + @test p ≈ (3 .* p⁺ .- p⁺⁺) ./ 2 + + # UseSurfaceTempAtBottom: T is the surface temperature; p follows the + # dry-isentropic law p = p⁺ (T/T⁺)^(cₚ/R). + RRTMGP.extrap!( + RRTMGP.UseSurfaceTempAtBottom(), + p, + T, + p⁺, + T⁺, + p⁺⁺, + T⁺⁺, + Tₛ, + params, + ) + @test T == Tₛ + @test all(p .> p⁺) # Tₛ > T⁺, so the boundary pressure exceeds p⁺ + # identity limit: Tₛ = T⁺ recovers p = p⁺ + RRTMGP.extrap!( + RRTMGP.UseSurfaceTempAtBottom(), + p, + T, + p⁺, + T⁺, + p⁺⁺, + T⁺⁺, + T⁺, + params, + ) + @test p ≈ p⁺ + + # HydrostaticBottom: dry-adiabatic lapse rate T = T⁺ + (g/cₚ)(z⁺ - z) + # and the same isentropic pressure law. + z = FT[0] + z⁺ = FT[500] + z⁺⁺ = FT[1500] + RRTMGP.extrap!( + RRTMGP.HydrostaticBottom(), + p, + T, + z, + p⁺, + T⁺, + z⁺, + p⁺⁺, + T⁺⁺, + z⁺⁺, + Tₛ, + params, + ) + @test T ≈ T⁺ .+ (g / cₚ) .* (z⁺ .- z) + # pressure follows the isentropic law; its exact value is pinned by the + # identity limit below and the dry-adiabat test. + @test all(p .> p⁺) # continues downward below the lowest layer + # identity limit: z = z⁺ recovers (p, T) = (p⁺, T⁺) + RRTMGP.extrap!( + RRTMGP.HydrostaticBottom(), + p, + T, + z⁺, + p⁺, + T⁺, + z⁺, + p⁺⁺, + T⁺⁺, + z⁺⁺, + Tₛ, + params, + ) + @test T ≈ T⁺ + @test p ≈ p⁺ + + # BestFit extrapolation: linear T in z and the fitted power law. + RRTMGP.extrap!( + RRTMGP.BestFit(), + p, + T, + z, + p⁺, + T⁺, + z⁺, + p⁺⁺, + T⁺⁺, + z⁺⁺, + Tₛ, + params, + ) + @test T ≈ T⁺ .+ (T⁺⁺ .- T⁺) .* (z .- z⁺) ./ (z⁺⁺ .- z⁺) + # the fitted pressure continues downward; its exact value is checked by + # the dry-adiabat test below. + @test all(p .> p⁺) + end +end + +# On a dry adiabat — T(z) linear with slope -g/cₚ and p(z) = p₀ (T/T₀)^(cₚ/R) — +# BestFit interpolation and HydrostaticBottom extrapolation are exact, so +# interpolate_levels! must reproduce the analytic level values from the layer +# values alone. This exercises the z-based branch of interpolate_levels! +# (center_z/face_z plumbing) end to end. +@testset "interpolate_levels! reproduces a dry adiabat (BestFit + HydrostaticBottom)" begin + for FT in (Float32, Float64) + params = RRTMGP.default_parameters(FT) + g = RRTMGP.Parameters.grav(params) + cₚ = RRTMGP.Parameters.cp_d(params) + R = RRTMGP.Parameters.R_d(params) + T₀ = FT(300) + p₀ = FT(100000) + T_ad(z) = T₀ - (g / cₚ) * z + p_ad(z) = p₀ * (T_ad(z) / T₀)^(cₚ / R) + + nlay, ncol = 8, 2 + z_lev = collect(FT, range(FT(0), FT(8000); length = nlay + 1)) + z_lay = (z_lev[1:(end - 1)] .+ z_lev[2:end]) ./ 2 + face_z = repeat(z_lev, 1, ncol) + center_z = repeat(z_lay, 1, ncol) + + as = _make_interp_as(FT, nlay, ncol) + RRTMGP.AtmosphericStates.getview_p_lay(as) .= p_ad.(center_z) + RRTMGP.AtmosphericStates.getview_t_lay(as) .= T_ad.(center_z) + + RRTMGP.interpolate_levels!( + as, + RRTMGP.BestFit(), + RRTMGP.HydrostaticBottom(), + params; + center_z, + face_z, + ) + + rtol = sqrt(eps(FT)) + @test as.t_lev ≈ T_ad.(face_z) rtol = rtol + @test as.p_lev ≈ p_ad.(face_z) rtol = rtol + end +end + +# UniformP divides by log(pꜛ/pꜜ) and therefore assumes distinct layer +# pressures (see the comment in interpolation.jl). Pin the precondition: equal +# pressures produce NaN temperatures rather than silently wrong values. +@testset "UniformP precondition: distinct layer pressures" begin + p = zeros(1) + T = zeros(1) + RRTMGP.interp!(RRTMGP.UniformP(), p, T, [500.0], [250.0], [500.0], [260.0]) + @test p == [500.0] + @test isnan(T[1]) +end diff --git a/test/lookup_serialization.jl b/test/lookup_serialization.jl index 7e601de16..6bb3703d4 100644 --- a/test/lookup_serialization.jl +++ b/test/lookup_serialization.jl @@ -3,6 +3,7 @@ import ClimaComms @static pkgversion(ClimaComms) >= v"0.6" && ClimaComms.@import_required_backends import RRTMGP import NCDatasets # loads the lookup-table extension +import Serialization # LookupBundle round-trip through save_lookup_tables/load_lookup_tables: the # cache must reproduce the NetCDF-built bundle exactly (structure and values), @@ -40,4 +41,26 @@ import NCDatasets # loads the lookup-table extension RRTMGP.save_lookup_tables(path2, gray) gray2 = RRTMGP.load_lookup_tables(path2, gp) @test gray2.nbnd_lw == 1 && isnothing(gray2.lookup_lw) + + # an aerosol-only (clear-sky) bundle keeps its aerosol tables and index + # maps through the round-trip, with no cloud tables + clear_aero = RRTMGP.lookup_tables(gp, RRTMGP.ClearSkyRadiation(true)) + path3 = joinpath(mktempdir(), "clear_aero.jls") + RRTMGP.save_lookup_tables(path3, clear_aero) + clear_aero2 = RRTMGP.load_lookup_tables(path3, gp) + @test isnothing(clear_aero2.lookup_lw_cld) && + isnothing(clear_aero2.lookup_sw_cld) + @test !isnothing(clear_aero2.lookup_lw_aero) && + !isnothing(clear_aero2.lookup_sw_aero) + @test clear_aero2.idx_aerosol_lw == clear_aero.idx_aerosol_lw + @test clear_aero2.idx_aerosize_sw == clear_aero.idx_aerosize_sw + + # a file that was not written by save_lookup_tables is rejected with an + # actionable error + path4 = joinpath(mktempdir(), "not_a_bundle.jls") + Serialization.serialize(path4, (; a = 1)) + @test_throws "did not deserialize to a LookupBundle" RRTMGP.load_lookup_tables( + path4, + gp, + ) end diff --git a/test/runtests.jl b/test/runtests.jl index 5c41ea3a8..4db8b789c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,14 @@ end include("grid_adaptation.jl") end +@testset "Interpolation and extrapolation scheme tests" begin + include("interpolation_schemes.jl") +end + +@testset "API contract tests" begin + include("api_contract.jl") +end + @testset "RRTMGP gray radiation tests" begin include("gray_atm.jl") end @@ -99,7 +107,9 @@ printstyled("=================================\n\n", color = color1) @testset "RRTMGP partial cloud fraction reproducibility tests" begin include("partial_cloud_fraction.jl") context = ClimaComms.context() - partial_cloud_fraction_test(context, Float64) + for FT in (Float32, Float64) + partial_cloud_fraction_test(context, FT) + end end printstyled( diff --git a/test/standalone_spectral.jl b/test/standalone_spectral.jl index 9433c82d5..d45d71734 100644 --- a/test/standalone_spectral.jl +++ b/test/standalone_spectral.jl @@ -60,4 +60,28 @@ using NCDatasets delete!(prof_bad.well_mixed_vmr, "h2o") prof_bad.well_mixed_vmr["kryptonite"] = FT(1e-6) # not an RRTMGP gas @test_throws ErrorException RRTMGP.solve(prof_bad; lookups) + + # volume_mixing_ratio getter branches (VmrGM storage): + # spatially varying gases are domain views ... + solver = out.solver + vmr_h2o = RRTMGP.volume_mixing_ratio(solver, "h2o") + @test size(vmr_h2o) == (nlay, 1) + # ... the water-vapor continuum pseudo-gases share the h2o storage ... + for continuum in ("h2o_self", "h2o_frgn") + @test Array(RRTMGP.volume_mixing_ratio(solver, continuum)) == + Array(vmr_h2o) + end + # ... and a well-mixed gas is returned as a host scalar (not a device view) + co2 = RRTMGP.volume_mixing_ratio(solver, "co2") + @test co2 isa Number + @test co2 ≈ 420e-6 + + # per-gas Vmr storage returns a (nlay, ncol) view for every gas + ngas = solver.lookups.ngas_sw + vmr_all = RRTMGP.VolumeMixingRatios.Vmr(zeros(FT, ngas, nlay, 1)) + idx_co2 = solver.lookups.idx_gases_sw["co2"] + vmr_all.vmr[idx_co2, :, :] .= FT(400e-6) + co2_view = RRTMGP._volume_mixing_ratio(solver, vmr_all, "co2") + @test size(co2_view) == (nlay, 1) + @test all(==(FT(400e-6)), Array(co2_view)) end