diff --git a/Manifest.toml b/Manifest.toml index 8c9034d63..e6f2f97be 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.4" +julia_version = "1.10.2" manifest_format = "2.0" -project_hash = "37e11a3bdd396c973917441db34ded05b97faa85" +project_hash = "d9a7ed1497e9d29249634fa08034c82fcc96d9c4" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -115,9 +115,9 @@ version = "0.9.2+0" [[deps.CUDA_Runtime_Discovery]] deps = ["Libdl"] -git-tree-sha1 = "f3b237289a5a77c759b2dd5d4c2ff641d67c4030" +git-tree-sha1 = "33576c7c1b2500f8e7e6baa082e04563203b3a45" uuid = "1af6417a-86b4-443c-805f-a4643ffb695f" -version = "0.3.4" +version = "0.3.5" [[deps.CUDA_Runtime_jll]] deps = ["Artifacts", "CUDA_Driver_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML"] @@ -171,7 +171,7 @@ weakdeps = ["Dates", "LinearAlgebra"] [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.1.1+0" +version = "1.1.0+0" [[deps.CompositionsBase]] git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad" @@ -183,17 +183,18 @@ weakdeps = ["InverseFunctions"] CompositionsBaseInverseFunctionsExt = "InverseFunctions" [[deps.ConstructionBase]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "a33b7ced222c6165f624a3f2b55945fac5a598d9" +git-tree-sha1 = "76219f1ed5771adbb096743bff43fb5fdd4c1157" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" -version = "1.5.7" +version = "1.5.8" [deps.ConstructionBase.extensions] ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseLinearAlgebraExt = "LinearAlgebra" ConstructionBaseStaticArraysExt = "StaticArrays" [deps.ConstructionBase.weakdeps] IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [[deps.Crayons]] @@ -202,10 +203,10 @@ uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" version = "4.1.1" [[deps.CubedSphere]] -deps = ["Elliptic", "FFTW", "Printf", "ProgressBars", "SpecialFunctions", "TaylorSeries", "Test"] -git-tree-sha1 = "10134667d7d3569b191a65801514271b8a93b292" +deps = ["TaylorSeries"] +git-tree-sha1 = "51bb25de518b4c62b7cdf26e5fbb84601bb27a60" uuid = "7445602f-e544-4518-8976-18f8e8ae6cdb" -version = "0.2.5" +version = "0.3.0" [[deps.DataAPI]] git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe" @@ -265,11 +266,6 @@ deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" version = "1.6.0" -[[deps.Elliptic]] -git-tree-sha1 = "71c79e77221ab3a29918aaf6db4f217b89138608" -uuid = "b305315f-e792-5b7a-8f41-49f472929428" -version = "1.0.1" - [[deps.ExprTools]] git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec" uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04" @@ -313,9 +309,9 @@ version = "6.2.1+6" [[deps.GPUArrays]] deps = ["Adapt", "GPUArraysCore", "LLVM", "LinearAlgebra", "Printf", "Random", "Reexport", "Serialization", "Statistics"] -git-tree-sha1 = "a74c3f1cf56a3dfcdef0605f8cdb7015926aae30" +git-tree-sha1 = "62ee71528cca49be797076a76bdc654a170a523e" uuid = "0c68f7d7-f131-5f86-a1c3-88cf8149b2d7" -version = "10.3.0" +version = "10.3.1" [[deps.GPUArraysCore]] deps = ["Adapt"] @@ -384,10 +380,10 @@ version = "1.4.2" Parsers = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" [[deps.IntelOpenMP_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "14eb2b542e748570b56446f4c50fbfb2306ebc45" +deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"] +git-tree-sha1 = "10bd689145d2c3b2a9844005d01087cc1194e79e" uuid = "1d5cc7b8-4909-519e-a0f8-d0f5ad9712d0" -version = "2024.2.0+0" +version = "2024.2.1+0" [[deps.InteractiveUtils]] deps = ["Markdown"] @@ -408,11 +404,6 @@ git-tree-sha1 = "0dc7b50b8d436461be01300fd8cd45aa0274b038" uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" version = "1.3.0" -[[deps.IrrationalConstants]] -git-tree-sha1 = "630b497eafcc20001bba38a4651b327dcfc491d2" -uuid = "92d709cd-6900-40b7-9082-c6be49f344b6" -version = "0.2.2" - [[deps.IterativeSolvers]] deps = ["LinearAlgebra", "Printf", "Random", "RecipesBase", "SparseArrays"] git-tree-sha1 = "59545b0a2b27208b0650df0a46b8e3019f85055b" @@ -425,16 +416,16 @@ uuid = "82899510-4779-5014-852e-03e436cf321d" version = "1.0.0" [[deps.JLD2]] -deps = ["FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "Reexport", "Requires", "TranscodingStreams", "UUIDs", "Unicode"] -git-tree-sha1 = "67d4690d32c22e28818a434b293a374cc78473d3" +deps = ["FileIO", "MacroTools", "Mmap", "OrderedCollections", "PrecompileTools", "Requires", "TranscodingStreams"] +git-tree-sha1 = "a0746c21bdc986d0dc293efa6b1faee112c37c28" uuid = "033835bb-8acc-5ee8-8aae-3f567f8a3819" -version = "0.4.51" +version = "0.4.53" [[deps.JLLWrappers]] deps = ["Artifacts", "Preferences"] -git-tree-sha1 = "7e5d6779a1e09a36db2a7b6cff50942a0a7d0fca" +git-tree-sha1 = "f389674c99bfcde17dc57454011aa44d5a260a40" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.5.0" +version = "1.6.0" [[deps.JuliaNVTXCallbacks_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] @@ -443,16 +434,20 @@ uuid = "9c1d0b0a-7046-5b2e-a33f-ea22f176ac7e" version = "0.2.1+0" [[deps.KernelAbstractions]] -deps = ["Adapt", "Atomix", "InteractiveUtils", "LinearAlgebra", "MacroTools", "PrecompileTools", "Requires", "SparseArrays", "StaticArrays", "UUIDs", "UnsafeAtomics", "UnsafeAtomicsLLVM"] -git-tree-sha1 = "0fac59881e91c7233a9b0d47f4b7d9432e534f0f" +deps = ["Adapt", "Atomix", "InteractiveUtils", "MacroTools", "PrecompileTools", "Requires", "StaticArrays", "UUIDs", "UnsafeAtomics", "UnsafeAtomicsLLVM"] +git-tree-sha1 = "cb1cff88ef2f3a157cbad75bbe6b229e1975e498" uuid = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -version = "0.9.23" +version = "0.9.25" [deps.KernelAbstractions.extensions] EnzymeExt = "EnzymeCore" + LinearAlgebraExt = "LinearAlgebra" + SparseArraysExt = "SparseArrays" [deps.KernelAbstractions.weakdeps] EnzymeCore = "f151be2c-9106-41f4-ab19-57ee4f262869" + LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[deps.LLVM]] deps = ["CEnum", "LLVMExtra_jll", "Libdl", "Preferences", "Printf", "Requires", "Unicode"] @@ -536,22 +531,6 @@ version = "1.17.0+0" deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -[[deps.LogExpFunctions]] -deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] -git-tree-sha1 = "a2d09619db4e765091ee5c6ffe8872849de0feea" -uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" -version = "0.3.28" - - [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" @@ -635,9 +614,9 @@ version = "2023.1.10" [[deps.NCDatasets]] deps = ["CFTime", "CommonDataModel", "DataStructures", "Dates", "DiskArrays", "NetCDF_jll", "NetworkOptions", "Printf"] -git-tree-sha1 = "a640912695952b074672edb5f9aaee2f7f9fd59a" +git-tree-sha1 = "77df6d3708ec0eb3441551e1f20f7503b37c2393" uuid = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" -version = "0.14.4" +version = "0.14.5" [[deps.NVTX]] deps = ["Colors", "JuliaNVTXCallbacks_jll", "Libdl", "NVTX_jll"] @@ -669,16 +648,17 @@ version = "1.2.0" [[deps.Oceananigans]] deps = ["Adapt", "CUDA", "Crayons", "CubedSphere", "Dates", "Distances", "DocStringExtensions", "FFTW", "Glob", "IncompleteLU", "InteractiveUtils", "IterativeSolvers", "JLD2", "KernelAbstractions", "LinearAlgebra", "Logging", "MPI", "NCDatasets", "OffsetArrays", "OrderedCollections", "Pkg", "Printf", "Random", "Rotations", "SeawaterPolynomials", "SparseArrays", "Statistics", "StructArrays"] -git-tree-sha1 = "ed415deb1d80c2a15c9b8bf0c7df04c6dec9d6c3" +git-tree-sha1 = "9b1b114e7853bd744ad3feff93232a1e5747ffa1" uuid = "9e8cae18-63c1-5223-a75c-80ca9d6e9a09" -version = "0.91.8" +version = "0.91.13" [deps.Oceananigans.extensions] OceananigansEnzymeExt = "Enzyme" - OceananigansMakieExt = "MakieCore" + OceananigansMakieExt = ["MakieCore", "Makie"] [deps.Oceananigans.weakdeps] Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" + Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" [[deps.OffsetArrays]] @@ -695,11 +675,6 @@ deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" version = "0.3.23+4" -[[deps.OpenLibm_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "05823500-19ac-5b8b-9628-191a04bc5112" -version = "0.8.1+2" - [[deps.OpenMPI_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML", "Zlib_jll"] git-tree-sha1 = "bfce6d523861a6c562721b262c0d1aaeead2647f" @@ -708,15 +683,9 @@ version = "5.0.5+0" [[deps.OpenSSL_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a028ee3cb5641cccc4c24e90c36b0a4f7707bdf5" +git-tree-sha1 = "1b35263570443fdd9e76c76b7062116e2f374ab8" uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.14+0" - -[[deps.OpenSpecFun_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1" -uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" -version = "0.5.5+0" +version = "3.0.15+0" [[deps.OrderedCollections]] git-tree-sha1 = "dfdf5519f235516220579f949664f1bf44e741c5" @@ -768,12 +737,6 @@ version = "2.3.2" deps = ["Unicode"] uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -[[deps.ProgressBars]] -deps = ["Printf"] -git-tree-sha1 = "b437cdb0385ed38312d91d9c00c20f3798b30256" -uuid = "49802e3a-d2f1-5c88-81d8-b72133a6f568" -version = "1.5.1" - [[deps.Quaternions]] deps = ["LinearAlgebra", "Random", "RealDot"] git-tree-sha1 = "994cc27cdacca10e68feb291673ec3a76aa2fae9" @@ -825,9 +788,9 @@ version = "1.3.0" [[deps.Roots]] deps = ["Accessors", "ChainRulesCore", "CommonSolve", "Printf"] -git-tree-sha1 = "3484138c9fa4296a0cf46a74ca3f97b59d12b1d0" +git-tree-sha1 = "48a7925c1d971b03bb81183b99d82c1dc7a3562f" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "2.1.6" +version = "2.1.8" [deps.Roots.extensions] RootsForwardDiffExt = "ForwardDiff" @@ -862,9 +825,9 @@ uuid = "6c6a2e73-6563-6170-7368-637461726353" version = "1.2.1" [[deps.SeawaterPolynomials]] -git-tree-sha1 = "6d85acd6de472f8e6da81c61c7c5b6280a55e0bc" +git-tree-sha1 = "78f965a2f0cd5250a20c9aba9979346dd2b35734" uuid = "d496a93d-167e-4197-9f49-d3af4ff8fe40" -version = "0.3.4" +version = "0.3.5" [[deps.SentinelArrays]] deps = ["Dates", "Random"] @@ -889,16 +852,6 @@ deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" version = "1.10.0" -[[deps.SpecialFunctions]] -deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] -git-tree-sha1 = "2f5d4697f21388cbe1ff299430dd169ef97d7e14" -uuid = "276daf66-3868-5448-9aa4-cd146d93841b" -version = "2.4.0" -weakdeps = ["ChainRulesCore"] - - [deps.SpecialFunctions.extensions] - SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" - [[deps.StaticArrays]] deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] git-tree-sha1 = "eeafab08ae20c62c44c8399ccb9354a04b80db50" @@ -974,15 +927,21 @@ version = "1.10.0" [[deps.TaylorSeries]] deps = ["LinearAlgebra", "Markdown", "Requires", "SparseArrays"] -git-tree-sha1 = "1c7170668366821b0c4c4fe03ee78f8d6cf36e2c" +git-tree-sha1 = "90c9bc500f4c5cdd235c81503ec91b2048f06423" uuid = "6aa5eb33-94cf-58f4-a9d0-e4b2c4fc25ea" -version = "0.16.0" +version = "0.17.8" [deps.TaylorSeries.extensions] TaylorSeriesIAExt = "IntervalArithmetic" + TaylorSeriesJLD2Ext = "JLD2" + TaylorSeriesRATExt = "RecursiveArrayTools" + TaylorSeriesSAExt = "StaticArrays" [deps.TaylorSeries.weakdeps] IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253" + JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" + RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [[deps.Test]] deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] @@ -1013,9 +972,9 @@ version = "0.2.1" [[deps.UnsafeAtomicsLLVM]] deps = ["LLVM", "UnsafeAtomics"] -git-tree-sha1 = "4073c836c2befcb041e5fe306cb6abf621eb3140" +git-tree-sha1 = "2d17fabcd17e67d7625ce9c531fb9f40b7c42ce4" uuid = "d80eeb9a-aca5-4d75-85e5-170c8b632249" -version = "0.2.0" +version = "0.2.1" [[deps.XML2_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] diff --git a/Project.toml b/Project.toml index 2c922e89b..bf71dfbee 100644 --- a/Project.toml +++ b/Project.toml @@ -22,7 +22,7 @@ EnsembleKalmanProcesses = "1" GibbsSeaWater = "0.1" JLD2 = "0.4" KernelAbstractions = "0.9" -Oceananigans = "0.91" +Oceananigans = "0.91.9" Roots = "2" SeawaterPolynomials = "0.3" StructArrays = "0.4, 0.5, 0.6" diff --git a/docs/make.jl b/docs/make.jl index 4a84eb77b..b139e0c2a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -77,10 +77,14 @@ end parameter_pages = ["$name" => "generated/$(name)_parameters.md" for name in model_names] +pisces_pages = ["PISCES" => "model_components/biogeochemical/PISCES/PISCES.md", + "Queries" => "model_components/biogeochemical/PISCES/notable_differences.md"] + bgc_pages = [ "Overview" => "model_components/biogeochemical/index.md", "LOBSTER" => "model_components/biogeochemical/LOBSTER.md", - "NPZD" => "model_components/biogeochemical/NPZ.md" + "NPZD" => "model_components/biogeochemical/NPZ.md", + "PISCES" => pisces_pages ] sediments_pages = [ diff --git a/docs/oceanbiome.bib b/docs/oceanbiome.bib index e2c6412ee..23156f6ae 100644 --- a/docs/oceanbiome.bib +++ b/docs/oceanbiome.bib @@ -396,4 +396,17 @@ @article{Weiss1974 bdsk-url-2 = {https://doi.org/10.1016/0304-4203(74)90015-2} } - +@article{Aumont2015, + abstract = {PISCES-v2 (Pelagic Interactions Scheme for Carbon and Ecosystem Studies volume 2) is a biogeochemical model which simulates the lower trophic levels of marine ecosystems (phytoplankton, microzooplankton and mesozooplankton) and the biogeochemical cycles of carbon and of the main nutrients (P, N, Fe, and Si). The model is intended to be used for both regional and global configurations at high or low spatial resolutions as well as for short-term (seasonal, interannual) and long-term (climate change, paleoceanography) analyses. There are 24 prognostic variables (tracers) including two phytoplankton compartments (diatoms and nanophytoplankton), two zooplankton size classes (microzooplankton and mesozooplankton) and a description of the carbonate chemistry. Formulations in PISCES-v2 are based on a mixed Monod-quota formalism. On the one hand, stoichiometry of C / N / P is fixed and growth rate of phytoplankton is limited by the external availability in N, P and Si. On the other hand, the iron and silicon quotas are variable and the growth rate of phytoplankton is limited by the internal availability in Fe. Various parameterizations can be activated in PISCES-v2, setting, for instance, the complexity of iron chemistry or the description of particulate organic materials. So far, PISCES-v2 has been coupled to the Nucleus for European Modelling of the Ocean (NEMO) and Regional Ocean Modeling System (ROMS) systems. A full description of PISCES-v2 and of its optional functionalities is provided here. The results of a quasi-steady-state simulation are presented and evaluated against diverse observational and satellite-derived data. Finally, some of the new functionalities of PISCES-v2 are tested in a series of sensitivity experiments.}, + author = {O. Aumont and C. Ethé and A. Tagliabue and L. Bopp and M. Gehlen}, + doi = {10.5194/gmd-8-2465-2015}, + issn = {19919603}, + issue = {8}, + journal = {Geoscientific Model Development}, + month = {8}, + pages = {2465-2513}, + publisher = {Copernicus GmbH}, + title = {PISCES-v2: An ocean biogeochemical model for carbon and ecosystem studies}, + volume = {8}, + year = {2015}, +} diff --git a/docs/src/appendix/library.md b/docs/src/appendix/library.md index 64b5ac576..8a3451e3e 100644 --- a/docs/src/appendix/library.md +++ b/docs/src/appendix/library.md @@ -9,7 +9,7 @@ Modules = [OceanBioME] ## Biogeochemical Models -### Nutrient Phytoplankton Zooplankton Detritus +### Nutrient Phytoplankton Zooplankton Detritus (NPZD) ```@autodocs Modules = [OceanBioME.Models.NPZDModel] @@ -21,6 +21,12 @@ Modules = [OceanBioME.Models.NPZDModel] Modules = [OceanBioME.Models.LOBSTERModel] ``` +### Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel] +``` + ### Sugar kelp (Saccharina latissima) ```@autodocs diff --git a/docs/src/index.md b/docs/src/index.md index 026553160..d537bf500 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,7 +2,7 @@ OceanBioME.jl is a fast and flexible ocean biogeochemical modelling environment. It is highly modular and is designed to make it easy to implement and use a variety of biogeochemical and physical models. OceanBioME is built to be coupled with physics models from [Oceananigans.jl](https://github.com/CliMA/Oceananigans.jl) allowing simulations across a wide range of spatial scales ranging from a global hydrostatic free surface model to non-hydrostatic large-eddy simulations. OceanBioME was designed specifically for ocean carbon dioxide removal applications. Notably, it includes active particles which allow individual-based models to be seamlessly coupled with the flow physics, ecosystem models, and carbonate chemistry. -OceanBioME.jl currently provides a core of several biogeochemical models Nutrient--Phytoplankton--Zooplankton--Detritus (NPZD) and [LOBSTER](https://doi.org/10.1029/2004JC002588), a medium complexity model, air-sea gas exchange models to provide appropriate top boundary conditions, and sediment models to for the benthic boundary. [PISCES](https://doi.org/10.5194/gmd-8-2465-2015) and other higher complexity models are in our future development plans. +OceanBioME.jl currently provides a core of several biogeochemical models Nutrient--Phytoplankton--Zooplankton--Detritus ([NPZD](@ref NPZD)), [LOBSTER](https://doi.org/10.1029/2004JC002588), a medium complexity model, and an early implementation of [PISCES](https://www.pisces-community.org/), a complex model. It also provides essential utilities like air-sea gas exchange models to provide appropriate top boundary conditions, a carbon chemistry model for computing the pCO₂, and sediment models to for the benthic boundary. OceanBioME.jl includes a framework for integrating the growth of biological/active Lagrangian particles which move around and can interact with the (Eulerian) tracer fields - for example, consuming nutrients and carbon dioxide while releasing dissolved organic material. A growth model for sugar kelp is currently implemented using active particles, and this model can be used in a variety of dynamical scenarios including free-floating or bottom-attached particles. diff --git a/docs/src/model_components/biogeochemical/PISCES/PISCES.md b/docs/src/model_components/biogeochemical/PISCES/PISCES.md new file mode 100644 index 000000000..748c872f8 --- /dev/null +++ b/docs/src/model_components/biogeochemical/PISCES/PISCES.md @@ -0,0 +1,19 @@ +# [PISCES (Pelagic Interactions Scheme for Carbon and Ecosystem Studies) model](@id PISCES) +PISCES ([PEES-kays, /ˈpiːs.keːs/](https://forvo.com/word/pisc%C4%93s/#la)) is a high complexity ocean biogeochemical model with 24 prognostic tracers. +It has previously been used with the [NEMO](https://www.nemo-ocean.eu/) transport model in the [IPSL-CM5A-LR](https://doi.org/10.1007/s00382-012-1636-1) and [CNRM-CM5](https://doi.org/10.1007/s00382-011-1259-y) CMIP-5 earth system models (ESM). +This is an early attempt to implement PISCES for use as a test bed in a more flexible environment to allow rapid prototyping and testing of new parametrisations as well as use in idealised experiments, additionally we want to be able to replicate the dynamics of the operational model for possible future use in a Julia based ESM as the ecosystem matures. + +An overview of the model structure is available from the [PISCES community website](https://www.pisces-community.org): + +![PISCES model structure](https://www.pisces-community.org/wp-content/uploads/2021/12/PISCES_Operational-1.png) + +More documentation to follow..., for now see our list of key differences from [Aumont2015](@citet) and queries we have about the parameterisations on the [notable differences](@ref PISCES_queries). + +## Model conservation +When the permanent scavenging of iron, and nitrogen fixation are turned off, PISCES conserves: + +- Carbon: ``\partial_tP + \partial_tD + \partial_tZ + \partial_tM + \partial_tDOC + \partial_tPOC + \partial_tGOC + \partial_tDIC + \partial_tCaCO_3=0`` +- Iron: ``\partial_tPFe + \partial_tDFe + \theta^{Fe}\left(\partial_tZ + \partial_tM + \partial_tDOC\right) + \partial_tSFe + \partial_tBFe + \partial_tFe=0`` +- Phosphate: ``\theta^P\left(\partial_tPFe + \partial_tDFe + \partial_tZ + \partial_tM + \partial_tDOC + \partial_tPOC + \partial_tGOC\right) + \partial_tPO_4=0`` +- Silicon: ``\partial_tDSi + \partial_tPSi + \partial_tSi=0`` +- Nitrogen: ``\theta^N\left(\partial_tPFe + \partial_tDFe + \partial_tZ + \partial_tM + \partial_tDOC + \partial_tPOC + \partial_tGOC\right) + \partial_tNH_4 + \partial_tNO_3=0`` \ No newline at end of file diff --git a/docs/src/model_components/biogeochemical/PISCES/notable_differences.md b/docs/src/model_components/biogeochemical/PISCES/notable_differences.md new file mode 100644 index 000000000..0b43d3e3d --- /dev/null +++ b/docs/src/model_components/biogeochemical/PISCES/notable_differences.md @@ -0,0 +1,3 @@ +# [A (probably not comprehensive) list of differences from the operational version and other queries](@id PISCES_queries) + +TODO: write this list \ No newline at end of file diff --git a/src/BoxModel/boxmodel.jl b/src/BoxModel/boxmodel.jl index cb229cb42..fda60b0b7 100644 --- a/src/BoxModel/boxmodel.jl +++ b/src/BoxModel/boxmodel.jl @@ -13,6 +13,7 @@ using Oceananigans.Biogeochemistry: update_biogeochemical_state! using Oceananigans.Fields: CenterField +using Oceananigans.Grids: RectilinearGrid, Flat using Oceananigans.TimeSteppers: tick!, TimeStepper using Oceananigans: UpdateStateCallsite, TendencyCallsite @@ -20,8 +21,8 @@ using OceanBioME: BoxModelGrid using StructArrays, JLD2 import Oceananigans.Simulations: run! -import Oceananigans: set! -import Oceananigans.Fields: regularize_field_boundary_conditions, TracerFields +import Oceananigans: set!, fields +import Oceananigans.Fields: regularize_field_boundary_conditions, TracerFields, interpolate import Oceananigans.Architectures: architecture import Oceananigans.Models: default_nan_checker, iteration, AbstractModel, prognostic_fields import Oceananigans.TimeSteppers: update_state! @@ -47,7 +48,7 @@ end forcing = NamedTuple(), timestepper = :RungeKutta3, clock = Clock(; time = 0.0), - prescribed_tracers::PT = (T = (t) -> 0, )) + prescribed_tracers::PT = NamedTuple()) Constructs a box model of a `biogeochemistry` model. Once this has been constructed you can set initial condiitons by `set!(model, X=1.0...)`. @@ -65,7 +66,7 @@ function BoxModel(; biogeochemistry::B, forcing = NamedTuple(), timestepper = :RungeKutta3, clock::C = Clock(; time = 0.0), - prescribed_tracers::PT = (T = (t) -> 0, )) where {B, C, PT} + prescribed_tracers::PT = NamedTuple()) where {B, C, PT} variables = required_biogeochemical_tracers(biogeochemistry) fields = NamedTuple{variables}([CenterField(grid) for var in eachindex(variables)]) @@ -107,7 +108,9 @@ architecture(model::BoxModel) = architecture(model.grid) # this might be the def default_nan_checker(::BoxModel) = nothing iteration(model::BoxModel) = model.clock.iteration prognostic_fields(model::BoxModel) = @inbounds model.fields[required_biogeochemical_tracers(model.biogeochemistry)] +fields(model::BoxModel) = model.fields +interpolate(at_node, from_field, from_loc, from_grid::RectilinearGrid{<:Any, Flat, Flat, Flat}) = @inbounds from_field[1, 1, 1] """ set!(model::BoxModel; kwargs...) diff --git a/src/BoxModel/timesteppers.jl b/src/BoxModel/timesteppers.jl index ec7fc6bb5..ca586eee0 100644 --- a/src/BoxModel/timesteppers.jl +++ b/src/BoxModel/timesteppers.jl @@ -60,8 +60,6 @@ end @inline tracer_tendency(val_name, biogeochemistry, forcing, time, model_fields, grid) = biogeochemistry(val_name, boxmodel_xyz(nodes(grid, Center(), Center(), Center()), grid)..., time, model_fields...) + forcing(time, model_fields...) -@inline tracer_tendency(::Val{:T}, biogeochemistry, forcing, time, model_fields, grid) = 0 - function rk3_substep!(model::BoxModel, Δt, γⁿ, ζⁿ) model_fields = prognostic_fields(model) diff --git a/src/Light/compute_euphotic_depth.jl b/src/Light/compute_euphotic_depth.jl index 8a21d574f..8652d9938 100644 --- a/src/Light/compute_euphotic_depth.jl +++ b/src/Light/compute_euphotic_depth.jl @@ -1,25 +1,31 @@ +using Oceananigans.Fields: ConstantField, ZeroField + @kernel function _compute_euphotic_depth!(euphotic_depth, PAR, grid, cutoff) i, j = @index(Global, NTuple) - surface_PAR = @inbounds PAR[i, j, grid.Nz] + surface_PAR = @inbounds (PAR[i, j, grid.Nz] + PAR[i, j, grid.Nz + 1])/2 - @inbounds euphotic_depth[i, j] = -Inf + @inbounds euphotic_depth[i, j, 1] = -Inf for k in grid.Nz-1:-1:1 - PARₖ = PAR[i, j, k] + PARₖ = @inbounds PAR[i, j, k] # BRANCHING! if (PARₖ <= surface_PAR * cutoff) && isinf(euphotic_depth[i, j]) # interpolate to find depth - PARₖ₊₁ = PAR[i, j, k + 1] + PARₖ₊₁ = @inbounds PAR[i, j, k + 1] zₖ = znode(i, j, k, grid, Center(), Center(), Center()) zₖ₊₁ = znode(i, j, k + 1, grid, Center(), Center(), Center()) - euphotic_depth[i, j] = zₖ₊₁ + (surface_PAR * cutoff - PARₖ₊₁) * (zₖ - zₖ₊₁)/ (PARₖ - PARₖ₊₁) + @inbounds euphotic_depth[i, j, 1] = zₖ + (log(surface_PAR * cutoff) - log(PARₖ)) * (zₖ - zₖ₊₁) / (log(PARₖ) - log(PARₖ₊₁)) end end + + zₑᵤ = @inbounds euphotic_depth[i, j, 1] + + @inbounds euphotic_depth[i, j, 1] = ifelse(isfinite(zₑᵤ), zₑᵤ, znode(i, j, 0, grid, Center(), Center(), Center())) end function compute_euphotic_depth!(euphotic_depth, PAR, cutoff = 1/1000) @@ -31,4 +37,8 @@ function compute_euphotic_depth!(euphotic_depth, PAR, cutoff = 1/1000) fill_halo_regions!(euphotic_depth) return nothing -end \ No newline at end of file +end + +# fallback for box models +compute_euphotic_depth!(::ConstantField, args...) = nothing +compute_euphotic_depth!(::ZeroField, args...) = nothing \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/PISCES.jl b/src/Models/AdvectedPopulations/PISCES/PISCES.jl index d0292dd9f..5b4a00efb 100644 --- a/src/Models/AdvectedPopulations/PISCES/PISCES.jl +++ b/src/Models/AdvectedPopulations/PISCES/PISCES.jl @@ -1,62 +1,45 @@ """ Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) model. -Tracers -======= - -* Nano-phytoplankton: P (μmolC/L) -* Diatoms: D (μmolC/L) -* Zooplankton: Z (μmolC/L) -* Mesozooplankton: M (μmolC/L) -* Chlorophyll in nano-phytoplankton: Pᶜʰˡ (μgChl/L) -* Chlorophyll in diatoms: Dᶜʰˡ (μgChl/L) -* Iron in nano-phytoplanktons: Pᶠᵉ (nmolFe/L) -* Iron in diatoms: Dᶠᵉ (nmolFe/L) -* Silicon in diatoms: Dˢⁱ (μmolSi/L) - -* Dissolved organic carbon: DOC (μmolC/L) -* Small sinking particles : POC (μmolC/L) -* Large sinking particles: GOC (μmolC/L) -* Iron in small particles: SFe (nmolFe/L) -* Iron in large particles: BFe (nmolFe/L) -* Silicate in large particles : PSi (μmolSi/L) -* Nitrates: NO₃ (μmolN/L) -* Ammonium: NH₄ (μmolN/L) -* Phosphate: PO₄ (μmolP/L) -* Dissolved iron: Fe (nmolFe/L) -* Silicate: Si (μmolSi/L) -* Calcite: CaCO₃ (μmolC/L) -* Dissolved oxygen: O₂ (μmolO₂/L) -* Dissolved inorganic carbon: DIC (μmolC/L) -* Total alkalinity: Alk (μmolN/L) - - -Required submodels -================== -# you will need something like this, they use a different PAR model but I wouldn't worry about that for now, you might also need temperatre and salinity (not sure) -* Photosynthetically available radiation: PAR (W/m²) +This is *not* currently an official version supported by the PISCES community +and is not yet verified to be capable of producing results mathcing that of the +operational PISCES configuration. This is a work in progress, please open an +issue or discusison if you'd like to know more. + +Notes to developers +=================== +Part of the vision for this implementation of PISCES is to harness the features +of Julia that would allow it to be fully modular. An obvious step to improve the +ease of this would be to do some minor refactoring to group the phytoplankton +classes, and zooplankton classes together, and for the other groups to generically +call the whole lot. This may cause some issues with argument passing, and although +it may not be the best way todo it my first thought is to pass them round as named +tuples built from something like, +``` +phytoplankton_tracers = phytoplankton_arguments(bgc.phytoplankton, args...) +``` """ module PISCESModel -export PISCES +export PISCES, DepthDependantSinkingSpeed, PrescribedLatitude, ModelLatitude using Oceananigans.Units - using Oceananigans: KernelFunctionOperation using Oceananigans.Fields: Field, TracerFields, CenterField, ZeroField, ConstantField, Center, Face using OceanBioME.Light: MultiBandPhotosyntheticallyActiveRadiation, default_surface_PAR, compute_euphotic_depth! using OceanBioME: setup_velocity_fields, show_sinking_velocities, Biogeochemistry, ScaleNegativeTracers using OceanBioME.BoxModels: BoxModel +using OceanBioME.Models.CarbonChemistryModel: CarbonChemistry using Oceananigans.Biogeochemistry: AbstractContinuousFormBiogeochemistry +using Oceananigans.Fields: set! using Oceananigans.Grids: φnodes, RectilinearGrid import OceanBioME: redfield, conserved_tracers, maximum_sinking_velocity, chlorophyll - import Oceananigans.Biogeochemistry: required_biogeochemical_tracers, required_biogeochemical_auxiliary_fields, biogeochemical_drift_velocity, @@ -65,333 +48,401 @@ import Oceananigans.Biogeochemistry: required_biogeochemical_tracers, import OceanBioME: maximum_sinking_velocity -import Adapt: adapt_structure, adapt import Base: show, summary -import OceanBioME.Models.Sediments: nitrogen_flux, carbon_flux, remineralisation_receiver, sinking_tracers +struct PISCES{NP, DP, SZ, BZ, DM, PM, NI, FE, SI, OX, PO, CA, CE, FT, LA, DL, ML, EU, MS, VD, MP, CC, CS, SS} <: AbstractContinuousFormBiogeochemistry + nanophytoplankton :: NP + diatoms :: DP -include("common.jl") + microzooplankton :: SZ + mesozooplankton :: BZ + + dissolved_organic_matter :: DM + particulate_organic_matter :: PM + + nitrogen :: NI + iron :: FE + silicate :: SI + oxygen :: OX + phosphate :: PO + + calcite :: CA + carbon_system :: CE + + first_anoxia_threshold :: FT + second_anoxia_threshold :: FT + + nitrogen_redfield_ratio :: FT + phosphate_redfield_ratio :: FT + + mixed_layer_shear :: FT + background_shear :: FT + + latitude :: LA + day_length :: DL + + mixed_layer_depth :: ML + euphotic_depth :: EU + silicate_climatology :: MS + + mean_mixed_layer_vertical_diffusivity :: VD + mean_mixed_layer_light :: MP + + carbon_chemistry :: CC + calcite_saturation :: CS -struct PISCES{FT, PD, ZM, OT, W, CF, ZF, LA, FFMLD, FFEU, K, CC, CS} <: AbstractContinuousFormBiogeochemistry - growth_rate_at_zero :: FT - growth_rate_reference_for_light_limitation :: FT - basal_respiration_rate :: FT - temperature_sensitivity_of_growth :: FT - initial_slope_of_PI_curve :: PD - exudation_of_DOC :: PD - absorption_in_the_blue_part_of_light :: PD - absorption_in_the_green_part_of_light :: PD - absorption_in_the_red_part_of_light :: PD - min_half_saturation_const_for_phosphate :: PD - min_half_saturation_const_for_ammonium :: PD - min_half_saturation_const_for_nitrate :: PD - min_half_saturation_const_for_silicate :: FT # - parameter_for_half_saturation_const :: FT - parameter_for_SiC :: OT # - min_half_saturation_const_for_iron_uptake :: PD # - size_ratio_of_phytoplankton :: PD# - optimal_SiC_uptake_ratio_of_diatoms :: FT - optimal_iron_quota :: PD# - max_iron_quota :: PD# - phytoplankton_mortality_rate :: PD - min_quadratic_mortality_of_phytoplankton :: FT - max_quadratic_mortality_of_diatoms :: FT - max_ChlC_ratios_of_phytoplankton :: PD - min_ChlC_ratios_of_phytoplankton :: FT - threshold_concentration_for_size_dependency :: PD - mean_residence_time_of_phytoplankton_in_unlit_mixed_layer :: PD - - latitude :: LA - - temperature_sensitivity_term :: ZM - max_growth_efficiency_of_zooplankton :: ZM - non_assimilated_fraction :: ZM - excretion_as_DOM :: ZM - max_grazing_rate :: ZM - flux_feeding_rate :: FT - half_saturation_const_for_grazing :: ZM - preference_for_nanophytoplankton :: ZM - preference_for_diatoms :: ZM - preference_for_POC :: ZM - preference_for_microzooplankton :: FT - food_threshold_for_zooplankton :: ZM - specific_food_thresholds_for_microzooplankton :: FT - specific_food_thresholds_for_mesozooplankton :: FT - zooplankton_quadratic_mortality :: ZM - zooplankton_linear_mortality :: ZM - half_saturation_const_for_mortality :: FT - fraction_of_calcite_not_dissolving_in_guts :: ZM - FeC_ratio_of_zooplankton :: FT - FeZ_redfield_ratio :: FT - - remineralisation_rate_of_DOC :: FT - half_saturation_const_for_DOC_remin :: FT - NO3_half_saturation_const_for_DOC_remin :: FT - NH4_half_saturation_const_for_DOC_remin :: FT - PO4_half_saturation_const_for_DOC_remin :: FT - Fe_half_saturation_const_for_DOC_remin :: FT - aggregation_rate_of_DOC_to_POC_1 :: FT - aggregation_rate_of_DOC_to_POC_2 :: FT - aggregation_rate_of_DOC_to_GOC_3 :: FT - aggregation_rate_of_DOC_to_POC_4 :: FT - aggregation_rate_of_DOC_to_POC_5 :: FT - - - degradation_rate_of_POC :: FT - sinking_speed_of_POC :: FT - min_sinking_speed_of_GOC :: FT - sinking_speed_of_dust :: FT - aggregation_rate_of_POC_to_GOC_6 :: FT - aggregation_rate_of_POC_to_GOC_7 :: FT - aggregation_rate_of_POC_to_GOC_8 :: FT - aggregation_rate_of_POC_to_GOC_9 :: FT - min_scavenging_rate_of_iron :: FT - slope_of_scavenging_rate_of_iron :: FT - scavenging_rate_of_iron_by_dust :: FT - dissolution_rate_of_calcite :: FT - exponent_in_the_dissolution_rate_of_calcite :: FT - proportion_of_the_most_labile_phase_in_PSi :: FT - slow_dissolution_rate_of_PSi :: FT - fast_dissolution_rate_of_PSi :: FT - - max_nitrification_rate :: FT - half_sat_const_for_denitrification1 :: FT - half_sat_const_for_denitrification2 :: FT - total_concentration_of_iron_ligands :: FT - max_rate_of_nitrogen_fixation :: FT - Fe_half_saturation_constant_of_nitrogen_fixation :: FT - photosynthetic_parameter_of_nitrogen_fixation :: FT - iron_concentration_in_sea_ice :: FT - max_sediment_flux_of_Fe :: FT - solubility_of_iron_in_dust :: FT - OC_for_ammonium_based_processes :: FT - OC_ratio_of_nitrification :: FT - CN_ratio_of_ammonification :: FT - CN_ratio_of_denitrification :: FT - NC_redfield_ratio :: FT - PC_redfield_ratio :: FT - rain_ratio_parameter :: FT - bacterial_reference :: FT - - NC_stoichiometric_ratio_of_dentitrification :: FT - NC_stoichiometric_ratio_of_ANOTHERPLACEHOLDER :: FT - dissolution_rate_of_silicon :: FT - coefficient_of_bacterial_uptake_of_iron_in_POC :: FT - coefficient_of_bacterial_uptake_of_iron_in_GOC :: FT - max_FeC_ratio_of_bacteria :: FT - Fe_half_saturation_const_for_Bacteria :: FT #not sure what this should be called - - mixed_layer_shear :: FT - background_shear :: FT - - mixed_layer_depth :: FFMLD - euphotic_depth :: FFEU - yearly_maximum_silicate :: CF - dust_deposition :: ZF - - mean_mixed_layer_vertical_diffusivity :: K - - carbon_chemistry :: CC - calcite_saturation :: CS - - sinking_velocities :: W + sinking_velocities :: SS end +const NANO_PHYTO = Union{Val{:P}, Val{:PChl}, Val{:PFe}} +const DIATOMS = Union{Val{:D}, Val{:DChl}, Val{:DFe}, Val{:DSi}} +const PARTICLES = Union{Val{:POC}, Val{:SFe}, Val{:GOC}, Val{:BFe}, Val{:PSi}} +const NITROGEN = Union{Val{:NO₃}, Val{:NH₄}} +const CARBON_SYSTEM = Union{Val{:DIC}, Val{:Alk}} + +include("group_methods.jl") + +@inline biogeochemical_auxiliary_fields(bgc::PISCES) = + (zₘₓₗ = bgc.mixed_layer_depth, + zₑᵤ = bgc.euphotic_depth, + Si′ = bgc.silicate_climatology, + Ω = bgc.calcite_saturation, + κ = bgc.mean_mixed_layer_vertical_diffusivity, + mixed_layer_PAR = bgc.mean_mixed_layer_light, + wPOC = bgc.sinking_velocities.POC, + wGOC = bgc.sinking_velocities.GOC) + +@inline required_biogeochemical_tracers(::PISCES) = + (:P, :D, :Z, :M, :PChl, :DChl, :PFe, :DFe, :DSi, + :DOC, :POC, :GOC, :SFe, :BFe, :PSi, # its really silly that this is called PSi when DSi also exists + :NO₃, :NH₄, :PO₄, :Fe, :Si, + :CaCO₃, :DIC, :Alk, :O₂, :T, :S) + +@inline required_biogeochemical_auxiliary_fields(::PISCES) = + (:zₘₓₗ, :zₑᵤ, :Si′, :Ω, :κ, :mixed_layer_PAR, :wPOC, :wGOC, :PAR, :PAR₁, :PAR₂, :PAR₃) + +const small_particle_components = Union{Val{:POC}, Val{:SFe}} +const large_particle_components = Union{Val{:GOC}, Val{:BFe}, Val{:PSi}, Val{:CaCO₃}} + +biogeochemical_drift_velocity(bgc::PISCES, ::small_particle_components) = (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.POC) +biogeochemical_drift_velocity(bgc::PISCES, ::large_particle_components) = (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.GOC) + +include("common.jl") +include("phytoplankton.jl") +include("zooplankton.jl") +include("dissolved_organic_matter.jl") +include("particulate_organic_matter.jl") +include("nitrate_ammonia.jl") +include("phosphates.jl") +include("iron.jl") +include("silicon.jl") +include("calcite.jl") +include("carbonate_system.jl") +include("oxygen.jl") +include("mean_mixed_layer_properties.jl") +include("compute_calcite_saturation.jl") +include("update_state.jl") +include("coupling_utils.jl") +include("show_methods.jl") +include("adapts.jl") +include("hack.jl") + """ PISCES(; grid, - + nanophytoplankton = + MixedMondoPhytoplankton( + growth_rate = GrowthRespirationLimitedProduction(dark_tollerance = 3days), + nutrient_limitation = + NitrogenIronPhosphateSilicateLimitation(minimum_ammonium_half_saturation = 0.013, + minimum_nitrate_half_saturation = 0.13, + minimum_phosphate_half_saturation = 0.8, + half_saturation_for_iron_uptake = 1.0, + silicate_limited = false), + blue_light_absorption = 2.1, + green_light_absorption = 0.42, + red_light_absorption = 0.4, + maximum_quadratic_mortality = 0.0, + maximum_chlorophyll_ratio = 0.033), + + diatoms = + MixedMondoPhytoplankton( + growth_rate = GrowthRespirationLimitedProduction(dark_tollerance = 4days), + nutrient_limitation = + NitrogenIronPhosphateSilicateLimitation(minimum_ammonium_half_saturation = 0.039, + minimum_nitrate_half_saturation = 0.39, + minimum_phosphate_half_saturation = 2.4, + half_saturation_for_iron_uptake = 3.0, + silicate_limited = true), + blue_light_absorption = 1.6, + green_light_absorption = 0.69, + red_light_absorption = 0.7, + maximum_quadratic_mortality = 0.03/day, + maximum_chlorophyll_ratio = 0.05), + + microzooplankton = Zooplankton(maximum_grazing_rate = 3/day, + preference_for_nanophytoplankton = 1.0, + preference_for_diatoms = 0.5, + preference_for_particulates = 0.1, + preference_for_zooplankton = 0.0, + quadratic_mortality = 0.004/day, + linear_mortality = 0.03/day, + minimum_growth_efficiency = 0.3, + maximum_flux_feeding_rate = 0.0, + undissolved_calcite_fraction = 0.5), + + mesozooplankton = Zooplankton(maximum_grazing_rate = 0.75/day, + preference_for_nanophytoplankton = 0.3, + preference_for_diatoms = 1.0, + preference_for_particulates = 0.3, + preference_for_zooplankton = 1.0, + quadratic_mortality = 0.03/day, + linear_mortality = 0.005/day, + minimum_growth_efficiency = 0.35, + maximum_flux_feeding_rate = 2e3 / 1e6 / day, + undissolved_calcite_fraction = 0.75), + + dissolved_organic_matter = DissolvedOrganicMatter(), + particulate_organic_matter = TwoCompartementParticulateOrganicMatter(), + + nitrogen = NitrateAmmonia(), + iron = SimpleIron(), + silicate = Silicate(), + oxygen = Oxygen(), + phosphate = Phosphate(), + + calcite = Calcite(), + carbon_system = CarbonateSystem(), + + # from Aumount 2005 rather than 2015 since it doesn't work the other way around + first_anoxia_thresehold = 6.0, + second_anoxia_thresehold = 1.0, + + nitrogen_redfield_ratio = 16/122, + phosphate_redfield_ratio = 1/122, + + mixed_layer_shear = 1.0, + background_shear = 0.01, + + latitude = PrescribedLatitude(45), + day_length = day_length_function, + + mixed_layer_depth = Field{Center, Center, Nothing}(grid), + euphotic_depth = Field{Center, Center, Nothing}(grid), + + silicate_climatology = ConstantField(7.5), + + mean_mixed_layer_vertical_diffusivity = Field{Center, Center, Nothing}(grid), + mean_mixed_layer_light = Field{Center, Center, Nothing}(grid), + + carbon_chemistry = CarbonChemistry(), + calcite_saturation = CenterField(grid), + + surface_photosynthetically_active_radiation = default_surface_PAR, + + light_attenuation = + MultiBandPhotosyntheticallyActiveRadiation(; grid, + surface_PAR = surface_photosynthetically_active_radiation), + + sinking_speeds = (POC = 2/day, + # might be more efficient to just precompute this + GOC = Field(KernelFunctionOperation{Center, Center, Face}(DepthDependantSinkingSpeed(), + grid, + mixed_layer_depth, + euphotic_depth)), + open_bottom = true, + + scale_negatives = false, + invalid_fill_value = NaN, + + sediment = nothing, + particles = nothing, + modifiers = nothing) + +Constructs an instance of the PISCES biogeochemical model. -Construct an instance of the [PISCES](@ref PISCES) biogeochemical model. Keyword Arguments ================= -- `grid`: (required) the geometry to build the model on, required to calculate sinking -- `parameter_1`...: PISCES parameter values -- `surface_photosynthetically_active_radiation`: funciton (or array in the future) for the photosynthetically available radiation at the surface, should be shape `f(x, y, t)` +- `grid`: (required) the geometry to build the model on +- `nanophytoplankton`: nanophytoplankton (`P`, `PChl`, `PFe``) evolution parameterisation such as `MixedMondoPhytoplankton` +- `diatoms`: diatom (`D`, `DChl`, `DFe`, `DSi`) evolution parameterisation such as `MixedMondoPhytoplankton` +- `microzooplankton`: microzooplankton (`Z`) evolution parameterisation +- `mesozooplankton`: mesozooplankton (`M`) evolution parameterisation +- `dissolved_organic_matter`: parameterisaion for the evolution of dissolved organic matter (`DOC`) +- `particulate_organic_matter`: parameterisation for the evolution of particulate organic matter (`POC`, `GOC`, `SFe`, `BFe`, `PSi`) +- `nitrogen`: parameterisation for the nitrogen compartements (`NH₄` and `NO₃`) +- `iron`: parameterisation for iron (`Fe`), currently the "complex chemistry" of Aumount 2015 is not implemented +- `silicate`: parameterisaion for silicate (`Si`) +- `oxygen`: parameterisaion for oxygen (`O₂`) +- `phosphate`: parameterisaion for phosphate (`PO₄`) +- `calcite`: parameterisaion for calcite (`CaCO₃`) +- `carbon_system`: parameterisation for the evolution of the carbon system (`DIC` and `Alk`alinity) +- `first_anoxia_thresehold` and `second_anoxia_thresehold`: thresholds in anoxia parameterisation +- `nitrogen_redfield_ratio` and `phosphate_redfield_ratio`: the assumed element ratios N/C and P/C +- `mixed_layer_shear` and `background_shear`: the mixed layer and background shear rates, TODO: move this to a computed field +- `latitude`: model latitude, should be `PrescribedLatitude` for `RectilinearGrid`s and `ModelLatitude` for grids providing their own latitude +- `day_length`: parameterisation for day length based on time of year and latitude, you may wish to change this to (φ, t) -> 1day if you + want to ignore the effect of day length, or something else if you're modelling a differen planet +- `mixed_layer_depth`: an `AbstractField` containing the mixed layer depth (to be computed during update state) +- `euphotic`: an `AbstractField` containing the euphotic depth, the depth where light reduces to 1/1000 of + the surface value (computed during update state) +- `silicate_climatology`: an `AbstractField` containing the silicate climatology which effects the diatoms silicate + half saturation constant +- `mean_mixed_layer_vertical_diffusivity`: an `AbstractField` containing the mean mixed layer vertical diffusivity + (to be computed during update state) +- `mean_mixed_layer_light`: an `AbstractField` containing the mean mixed layer light (computed during update state) +- `carbon_chemistry`: the `CarbonChemistry` model used to compute the calicte saturation +- `calcite_saturation`: an `AbstractField` containing the calcite saturation (computed during update state) +- `surface_photosynthetically_active_radiation`: funciton for the photosynthetically available radiation at the surface - `light_attenuation_model`: light attenuation model which integrated the attenuation of available light -- `sediment_model`: slot for `AbstractSediment` -- `sinking_speed`: named tuple of constant sinking, of fields (i.e. `ZFaceField(...)`) for any tracers which sink (convention is that a sinking speed is positive, but a field will need to follow the usual down being negative) +- `sinking_speed`: named tuple of constant sinking speeds, or fields (i.e. `ZFaceField(...)`) for any tracers which sink + (convention is that a sinking speed is positive, but a field will need to follow the usual down being negative) - `open_bottom`: should the sinking velocity be smoothly brought to zero at the bottom to prevent the tracers leaving the domain - `scale_negatives`: scale negative tracers? - `particles`: slot for `BiogeochemicalParticles` - `modifiers`: slot for components which modify the biogeochemistry when the tendencies have been calculated or when the state is updated -Example -======= +All parameterisations default to the operaitonal version of PISCES as close as possible. -```jldoctest -julia> using OceanBioME +Notes +===== +Currently only `MixedMondoPhytoplankton` are implemented, and some work should be done to generalise +the classes to a single `phytoplankton` if more classes are required (see +`OceanBioME.Models.PISCESModel` docstring). Similarly, if a more generic `particulate_organic_matter` +was desired a way to specify arbitary tracers for arguments would be required. +""" +function PISCES(; grid, + nanophytoplankton = + MixedMondoPhytoplankton( + growth_rate = GrowthRespirationLimitedProduction(dark_tollerance = 3days), + nutrient_limitation = + NitrogenIronPhosphateSilicateLimitation(minimum_ammonium_half_saturation = 0.013, + minimum_nitrate_half_saturation = 0.13, + minimum_phosphate_half_saturation = 0.8, + half_saturation_for_iron_uptake = 1.0, + silicate_limited = false), + blue_light_absorption = 2.1, + green_light_absorption = 0.42, + red_light_absorption = 0.4, + maximum_quadratic_mortality = 0.0, + maximum_chlorophyll_ratio = 0.033), + + diatoms = + MixedMondoPhytoplankton( + growth_rate = GrowthRespirationLimitedProduction(dark_tollerance = 4days), + nutrient_limitation = + NitrogenIronPhosphateSilicateLimitation(minimum_ammonium_half_saturation = 0.039, + minimum_nitrate_half_saturation = 0.39, + minimum_phosphate_half_saturation = 2.4, + half_saturation_for_iron_uptake = 3.0, + silicate_limited = true), + blue_light_absorption = 1.6, + green_light_absorption = 0.69, + red_light_absorption = 0.7, + maximum_quadratic_mortality = 0.03/day, + maximum_chlorophyll_ratio = 0.05), + + microzooplankton = Zooplankton(maximum_grazing_rate = 3/day, + preference_for_nanophytoplankton = 1.0, + preference_for_diatoms = 0.5, + preference_for_particulates = 0.1, + preference_for_zooplankton = 0.0, + quadratic_mortality = 0.004/day, + linear_mortality = 0.03/day, + minimum_growth_efficiency = 0.3, + maximum_flux_feeding_rate = 0.0, + undissolved_calcite_fraction = 0.5, + iron_ratio = 0.01), + + mesozooplankton = Zooplankton(maximum_grazing_rate = 0.75/day, + preference_for_nanophytoplankton = 0.3, + preference_for_diatoms = 1.0, + preference_for_particulates = 0.3, + preference_for_zooplankton = 1.0, + quadratic_mortality = 0.03/day, + linear_mortality = 0.005/day, + minimum_growth_efficiency = 0.35, + # not documented but the below must implicitly contain a factor of second/day + # to be consistent in the NEMO namelist to go from this * mol / L * m/s to mol / L / day + maximum_flux_feeding_rate = 2e3 / 1e6 / day, # (day * meter/s * mol/L)^-1 to (meter * μ mol/L)^-1 + undissolved_calcite_fraction = 0.75, + iron_ratio = 0.015), + + dissolved_organic_matter = DissolvedOrganicMatter(), + particulate_organic_matter = TwoCompartementParticulateOrganicMatter(), + + nitrogen = NitrateAmmonia(), + iron = SimpleIron(), + silicate = Silicate(), + oxygen = Oxygen(), + phosphate = Phosphate(), + + calcite = Calcite(), + carbon_system = CarbonateSystem(), + + # from Aumount 2005 rather than 2015 since it doesn't work the other way around + first_anoxia_thresehold = 6.0, + second_anoxia_thresehold = 1.0, -julia> using Oceananigans + nitrogen_redfield_ratio = 16/122, + phosphate_redfield_ratio = 1/122, + + mixed_layer_shear = 1.0, + background_shear = 0.01, + + latitude = PrescribedLatitude(45), + day_length = day_length_function, + + mixed_layer_depth = Field{Center, Center, Nothing}(grid), + euphotic_depth = Field{Center, Center, Nothing}(grid), -julia> grid = RectilinearGrid(size=(3, 3, 30), extent=(10, 10, 200)); + silicate_climatology = ConstantField(7.5), -julia> model = PISCES(; grid) -PISCES{Float64} - Light attenuation: Two-band light attenuation model (Float64) - Sediment: Nothing - Particles: Nothing - Modifiers: Nothing -``` -""" -function PISCES(; grid, - growth_rate_at_zero = 0.6 / day, # 1/second - growth_rate_reference_for_light_limitation = 1.0/ day, # 1/second - basal_respiration_rate = 0.033 / day, # 1/second - temperature_sensitivity_of_growth = 1.066, - initial_slope_of_PI_curve = (P = 2/day, D = 2/day), #(Wm⁻²)⁻¹s⁻¹ - exudation_of_DOC = (P = 0.05, D = 0.05), - absorption_in_the_blue_part_of_light = (P = 2.1, D = 1.6), - absorption_in_the_green_part_of_light = (P = 0.42, D = 0.69), - absorption_in_the_red_part_of_light = (P = 0.4, D = 0.7), - min_half_saturation_const_for_phosphate = (P = 0.8, D = 2.4), #nmolPL⁻¹ - min_half_saturation_const_for_ammonium = (P = 0.013, D = 0.039), #μmolNL⁻¹ - min_half_saturation_const_for_nitrate = (P = 0.13, D =0.39), #μmolNL⁻¹ - min_half_saturation_const_for_silicate = 1.0, #μmolSiL⁻¹ - parameter_for_half_saturation_const = 16.6, #μmolSiL⁻¹ - parameter_for_SiC = (one = 2.0, two = 20.0), #μmolSiL⁻¹ - min_half_saturation_const_for_iron_uptake = (P = 1.0, D = 3.0), #nmolFeL⁻¹ - size_ratio_of_phytoplankton = (P = 3.0, D = 3.0), - optimal_SiC_uptake_ratio_of_diatoms = 0.159, #molSi/(mol C) - optimal_iron_quota = (P = 7.0e-3, D = 7.0e-3), #mmolFe/(mol C) - max_iron_quota = (P = 40.0e-3, D = 40.0e-3), #molFe/(mol C) - phytoplankton_mortality_rate = (P = 0.01/day, D = 0.01/day), #1/second - min_quadratic_mortality_of_phytoplankton = 0.01 / day, #1/(d mol C) - max_quadratic_mortality_of_diatoms = 0.03 / day, #1/(d mol C) - max_ChlC_ratios_of_phytoplankton = (P = 0.033, D = 0.05), #mg Chl/(mg C) - min_ChlC_ratios_of_phytoplankton = 0.0033, #mg Chl/(mg C) - threshold_concentration_for_size_dependency = (P = 1.0, D = 1.0), #μmolCL⁻¹ - mean_residence_time_of_phytoplankton_in_unlit_mixed_layer = (P = 3days, D = 4days), #seconds - - latitude = 45, - - temperature_sensitivity_term = (Z = 1.079, M = 1.079), - max_growth_efficiency_of_zooplankton = (Z = 0.3, M = 0.35), - non_assimilated_fraction = (Z = 0.3, M = 0.3), - excretion_as_DOM = (Z = 0.6, M = 0.6), - max_grazing_rate = (Z = 3.0/day, M = 0.75/day), #1/second - flux_feeding_rate = 2.0e-3, #(m mol L⁻¹)⁻¹ - half_saturation_const_for_grazing = (Z = 20.0, M = 20.0), #μmolCL⁻¹ - preference_for_nanophytoplankton = (Z = 1.0, M = 0.3), - preference_for_diatoms = (Z = 0.5, M = 1.0), - preference_for_POC= (Z = 0.1, M = 0.3), - preference_for_microzooplankton = 1.0, - food_threshold_for_zooplankton = (Z = 0.3, M = 0.3), #μmolCL⁻¹ - specific_food_thresholds_for_microzooplankton = 0.001, #μmolCL⁻¹ - specific_food_thresholds_for_mesozooplankton = 0.001, #μmolCL⁻¹ - zooplankton_quadratic_mortality = (Z = 0.004/day, M = 0.03/day), #(μmolCL⁻¹)⁻¹s⁻¹ - zooplankton_linear_mortality = (Z = 0.03/day, M = 0.005/day), #1/second - half_saturation_const_for_mortality = 0.2, #μmolCL⁻¹ - fraction_of_calcite_not_dissolving_in_guts = (Z = 0.5, M = 0.75), - FeC_ratio_of_zooplankton = 10.0e-3, #mmolFe molC⁻¹ - FeZ_redfield_ratio = 3.0e-3, #mmolFe molC⁻¹, remove this, is actually FeC_ratio_of_zooplankton - - - remineralisation_rate_of_DOC = 0.3 / day, #1/second - half_saturation_const_for_DOC_remin = 417.0, #μmolCL⁻¹ - NO3_half_saturation_const_for_DOC_remin = 0.03, #μmolNL⁻¹ - NH4_half_saturation_const_for_DOC_remin = 0.003, #μmolNL⁻¹ - PO4_half_saturation_const_for_DOC_remin = 0.003, #μmolPL⁻¹ - Fe_half_saturation_const_for_DOC_remin = 0.01, #μmolFeL⁻¹ - aggregation_rate_of_DOC_to_POC_1 = 0.37e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_DOC_to_POC_2 = 102.0e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_DOC_to_GOC_3 = 3530.0e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_DOC_to_POC_4 = 5095.0e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_DOC_to_POC_5 = 114.0e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - - - degradation_rate_of_POC = 0.025 / day, #1/second - sinking_speed_of_POC = 2.0 / day, #ms⁻¹ - min_sinking_speed_of_GOC = 30.0 / day, #ms⁻¹ - sinking_speed_of_dust = 2.0, #ms⁻¹ - aggregation_rate_of_POC_to_GOC_6 = 25.9e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_POC_to_GOC_7 = 4452.0e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_POC_to_GOC_8 = 3.3e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - aggregation_rate_of_POC_to_GOC_9 = 47.1e-6 / day, #(μmolCL⁻¹)⁻¹s⁻¹ - min_scavenging_rate_of_iron = 3.0e-5 / day, #1/second - slope_of_scavenging_rate_of_iron = 0.005 / day, #d⁻¹μmol⁻¹L - scavenging_rate_of_iron_by_dust = 150.0 / day, #s⁻¹mg⁻¹L - dissolution_rate_of_calcite = 0.197 / day, #1/second - exponent_in_the_dissolution_rate_of_calcite = 1.0, - proportion_of_the_most_labile_phase_in_PSi = 0.5, - slow_dissolution_rate_of_PSi = 0.003 / day, #1/second - fast_dissolution_rate_of_PSi = 0.025 / day, #1/second - - - max_nitrification_rate = 0.05 / day, #1/sedonc - half_sat_const_for_denitrification1 = 1.0, #μmolO₂L⁻¹ - half_sat_const_for_denitrification2 = 6.0, #μmolO₂L⁻¹ - total_concentration_of_iron_ligands = 0.6, #nmolL⁻¹ - max_rate_of_nitrogen_fixation = 0.013 / day, #μmolNL⁻¹s⁻¹ - Fe_half_saturation_constant_of_nitrogen_fixation = 0.1, #nmolFeL⁻¹ - photosynthetic_parameter_of_nitrogen_fixation = 50.0, #Wm⁻² - iron_concentration_in_sea_ice = 15.0, #nmolFeL⁻¹ - max_sediment_flux_of_Fe = 2.0 / day, #μmolFem⁻²s⁻¹ - solubility_of_iron_in_dust = 0.02, - OC_for_ammonium_based_processes = 133/122, #molO₂(mol C)⁻¹ - OC_ratio_of_nitrification = 32/122, #molO₂(mol C)⁻¹ - CN_ratio_of_ammonification = 3/5, #molN(mol C)⁻¹ - CN_ratio_of_denitrification = 105/16, #molN(mol C)⁻¹ - NC_redfield_ratio = 16/122, - PC_redfield_ratio = 1/122, #molN(mol C)⁻¹ - rain_ratio_parameter = 0.3, - bacterial_reference = 1.0, #Not sure if this is what its called : denoted Bact_ref in paper - - NC_stoichiometric_ratio_of_dentitrification = 0.86, - NC_stoichiometric_ratio_of_ANOTHERPLACEHOLDER = 0.0, #again not sure what this is called - dissolution_rate_of_silicon = 1.0, - coefficient_of_bacterial_uptake_of_iron_in_POC = 0.5, - coefficient_of_bacterial_uptake_of_iron_in_GOC = 0.5, - max_FeC_ratio_of_bacteria = 10.0e-3, #or 6 - Fe_half_saturation_const_for_Bacteria = 0.03, #or 2.5e-10 - - mixed_layer_shear = 1.0, - background_shear = 0.01, - - mixed_layer_depth = FunctionField{Center, Center, Center}(-100.0, grid), - euphotic_depth = Field{Center, Center, Nothing}(grid), - mean_mixed_layer_vertical_diffusivity = Field{Center, Center, Nothing}(grid), - yearly_maximum_silicate = ConstantField(7.5), - dust_deposition = ZeroField(), + mean_mixed_layer_vertical_diffusivity = Field{Center, Center, Nothing}(grid), + mean_mixed_layer_light = Field{Center, Center, Nothing}(grid), + + carbon_chemistry = CarbonChemistry(), + calcite_saturation = CenterField(grid), surface_photosynthetically_active_radiation = default_surface_PAR, - light_attenuation_model = + light_attenuation = MultiBandPhotosyntheticallyActiveRadiation(; grid, surface_PAR = surface_photosynthetically_active_radiation), - sediment_model = nothing, - sinking_speeds = (POC = 2/day, - GOC = KernelFunctionOperation{Center, Center, Face}(DepthDependantSinkingSpeed(), - grid, - mixed_layer_depth, - euphotic_depth)), - - carbon_chemistry = CarbonChemistry(), - calcite_saturation = CenterField(grid), - + # might be more efficient to just precompute this + GOC = Field(KernelFunctionOperation{Center, Center, Face}(DepthDependantSinkingSpeed(), + grid, + mixed_layer_depth, + euphotic_depth))), open_bottom = true, scale_negatives = false, - + invalid_fill_value = NaN, + + sediment = nothing, particles = nothing, modifiers = nothing) - if !isnothing(sediment_model) && !open_bottom + @warn "This implementation of PISCES is in early development and has not yet been validated" + + if !isnothing(sediment) && !open_bottom @warn "You have specified a sediment model but not `open_bottom` which will not work as the tracer will settle in the bottom cell" end sinking_velocities = setup_velocity_fields(sinking_speeds, grid, open_bottom) - if (latitude isa Number) & !(grid isa RectilinearGrid) + sinking_velocities = merge(sinking_velocities, (; grid)) # we need to interpolate the fields so we need this for flux feeding inside a kernel - this might cause problems... + + if (latitude isa PrescribedLatitude) & !(grid isa RectilinearGrid) φ = φnodes(grid, Center(), Center(), Center()) @warn "A latitude of $latitude was given but the grid has its own latitude ($(minimum(φ)), $(maximum(φ))) so the prescribed value is ignored" latitude = nothing - elseif isnothing(latitude) & (grid isa RectilinearGrid) + elseif (latitude isa ModelLatitude) & (grid isa RectilinearGrid) throw(ArgumentError("You must prescribe a latitude when using a `RectilinearGrid`")) end @@ -402,248 +453,38 @@ function PISCES(; grid, set!(mean_mixed_layer_vertical_diffusivity, 1) end - underlying_biogeochemistry = PISCES(growth_rate_at_zero, - growth_rate_reference_for_light_limitation, - basal_respiration_rate, - temperature_sensitivity_of_growth, - initial_slope_of_PI_curve, - exudation_of_DOC, - absorption_in_the_blue_part_of_light, - absorption_in_the_green_part_of_light, - absorption_in_the_red_part_of_light, - min_half_saturation_const_for_phosphate, - min_half_saturation_const_for_ammonium, - min_half_saturation_const_for_nitrate, - min_half_saturation_const_for_silicate, - parameter_for_half_saturation_const, - parameter_for_SiC, - min_half_saturation_const_for_iron_uptake, - size_ratio_of_phytoplankton, - optimal_SiC_uptake_ratio_of_diatoms, - optimal_iron_quota, - max_iron_quota, - phytoplankton_mortality_rate, - min_quadratic_mortality_of_phytoplankton, - max_quadratic_mortality_of_diatoms, - max_ChlC_ratios_of_phytoplankton, - min_ChlC_ratios_of_phytoplankton, - threshold_concentration_for_size_dependency, - mean_residence_time_of_phytoplankton_in_unlit_mixed_layer, - - latitude, - - temperature_sensitivity_term, - max_growth_efficiency_of_zooplankton, - non_assimilated_fraction, - excretion_as_DOM, - max_grazing_rate, - flux_feeding_rate, - half_saturation_const_for_grazing, - preference_for_nanophytoplankton, - preference_for_diatoms, - preference_for_POC, - preference_for_microzooplankton, - food_threshold_for_zooplankton, - specific_food_thresholds_for_microzooplankton, - specific_food_thresholds_for_mesozooplankton, - zooplankton_quadratic_mortality, - zooplankton_linear_mortality, - half_saturation_const_for_mortality, - fraction_of_calcite_not_dissolving_in_guts, - FeC_ratio_of_zooplankton, - FeZ_redfield_ratio, - - - remineralisation_rate_of_DOC, - half_saturation_const_for_DOC_remin, - NO3_half_saturation_const_for_DOC_remin, - NH4_half_saturation_const_for_DOC_remin, - PO4_half_saturation_const_for_DOC_remin, - Fe_half_saturation_const_for_DOC_remin, - aggregation_rate_of_DOC_to_POC_1, - aggregation_rate_of_DOC_to_POC_2, - aggregation_rate_of_DOC_to_GOC_3, - aggregation_rate_of_DOC_to_POC_4, - aggregation_rate_of_DOC_to_POC_5, - - - degradation_rate_of_POC, - sinking_speed_of_POC, - min_sinking_speed_of_GOC, - sinking_speed_of_dust, - aggregation_rate_of_POC_to_GOC_6, - aggregation_rate_of_POC_to_GOC_7, - aggregation_rate_of_POC_to_GOC_8, - aggregation_rate_of_POC_to_GOC_9, - min_scavenging_rate_of_iron, - slope_of_scavenging_rate_of_iron, - scavenging_rate_of_iron_by_dust, - dissolution_rate_of_calcite, - exponent_in_the_dissolution_rate_of_calcite, - proportion_of_the_most_labile_phase_in_PSi, - slow_dissolution_rate_of_PSi, - fast_dissolution_rate_of_PSi, - - - max_nitrification_rate, - half_sat_const_for_denitrification1, - half_sat_const_for_denitrification2, - total_concentration_of_iron_ligands, - max_rate_of_nitrogen_fixation, - Fe_half_saturation_constant_of_nitrogen_fixation, - photosynthetic_parameter_of_nitrogen_fixation, - iron_concentration_in_sea_ice, - max_sediment_flux_of_Fe, - solubility_of_iron_in_dust, - OC_for_ammonium_based_processes, - OC_ratio_of_nitrification, - CN_ratio_of_ammonification, - CN_ratio_of_denitrification, - NC_redfield_ratio, - PC_redfield_ratio, - rain_ratio_parameter, - bacterial_reference, - - NC_stoichiometric_ratio_of_dentitrification, - NC_stoichiometric_ratio_of_ANOTHERPLACEHOLDER, - dissolution_rate_of_silicon, - coefficient_of_bacterial_uptake_of_iron_in_POC, - coefficient_of_bacterial_uptake_of_iron_in_GOC, - max_FeC_ratio_of_bacteria, - Fe_half_saturation_const_for_Bacteria, #not sure what this should be called - - mixed_layer_shear, - background_shear, - - mixed_layer_depth, - euphotic_depth, - yearly_maximum_silicate, - dust_deposition, - - mean_mixed_layer_vertical_diffusivity, - - carbon_chemistry, - calcite_saturation, - + underlying_biogeochemistry = PISCES(nanophytoplankton, diatoms, + microzooplankton, mesozooplankton, + dissolved_organic_matter, particulate_organic_matter, + nitrogen, iron, silicate, oxygen, phosphate, + calcite, carbon_system, + first_anoxia_thresehold, second_anoxia_thresehold, + nitrogen_redfield_ratio, phosphate_redfield_ratio, + mixed_layer_shear, background_shear, + latitude, day_length, + mixed_layer_depth, euphotic_depth, + silicate_climatology, + mean_mixed_layer_vertical_diffusivity, + mean_mixed_layer_light, + carbon_chemistry, calcite_saturation, sinking_velocities) if scale_negatives - scaler = ScaleNegativeTracers(underlying_biogeochemistry, grid; invalid_fill_value) + scalers = ScaleNegativeTracers(underlying_biogeochemistry, grid; invalid_fill_value) if isnothing(modifiers) - modifiers = scaler + modifiers = scalers elseif modifiers isa Tuple - modifiers = (modifiers..., scaler) + modifiers = (modifiers..., scalers...) else - modifiers = (modifiers, scaler) + modifiers = (modifiers, scalers...) end end return Biogeochemistry(underlying_biogeochemistry; - light_attenuation = light_attenuation_model, - sediment = sediment_model, + light_attenuation, + sediment, particles, modifiers) end -@inline biogeochemical_auxiliary_fields(bgc::PISCES) = - (zₘₓₗ = bgc.mixed_layer_depth, - zₑᵤ = bgc.euphotic_depth, - Si̅ = bgc.yearly_maximum_silicate, - D_dust = bgc.dust_deposition, - Ω = bgc.calcite_saturation, - κ = bgc.mean_mixed_layer_vertical_diffusivity) - -@inline required_biogeochemical_tracers(::PISCES) = - (:P, :D, :Z, :M, - :Pᶜʰˡ, :Dᶜʰˡ, - :Pᶠᵉ, :Dᶠᵉ, - :Dˢⁱ, - :DOC, :POC, :GOC, - :SFe, :BFe, - :PSi, - :NO₃, :NH₄, - :PO₄, :Fe, :Si, - :CaCO₃, :DIC, :Alk, - :O₂, - :T) - -@inline required_biogeochemical_auxiliary_fields(::PISCES) = - (:zₘₓₗ, :zₑᵤ, :Si̅, :D_dust, :Ω, :κ, :PAR, :PAR₁, :PAR₂, :PAR₃) - -# for sinking things like POM this is how we tell oceananigans ther sinking speed -@inline function biogeochemical_drift_velocity(bgc::PISCES, ::Val{tracer_name}) where tracer_name - if tracer_name in keys(bgc.sinking_velocities) - return (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities[tracer_name]) - else - return (u = ZeroField(), v = ZeroField(), w = ZeroField()) - end -end - -const small_particle_components = Union{Val{:POC}, Val{:SFe}} -const large_particle_components = Union{Val{:GOC}, Val{:BFe}, Val{:PSi}, Val{:CaCO₃}} -# not sure what the point of PSi is since they don't use it to compute the sinking speed anymore anyway - -biogeochemical_drift_velocity(bgc::PISCES, ::small_particle_components) = (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.POC) -biogeochemical_drift_velocity(bgc::PISCES, ::large_particle_components) = (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.GOC) - -# don't worry about this for now -adapt_structure(to, pisces::PISCES) = - PISCES(adapt(to, pisces.parameter_1), - adapt(to, pisces.sinking_velocities)) - -# you can updatye these if you want it to have a pretty way of showing uyou its a pisces model -summary(::PISCES{FT}) where {FT} = string("PISCES{$FT}") - -show(io::IO, model::PISCES) = print(io, string("Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) model")) # maybe add some more info here - -@inline maximum_sinking_velocity(bgc::PISCES) = maximum(abs, bgc.sinking_velocities.bPOM.w) # might need ot update this for wghatever the fastest sinking pareticles are - -include("phytoplankton.jl") -include("calcite.jl") -include("carbonate_system.jl") -include("dissolved_organic_matter.jl") -include("iron_in_particles.jl") -include("iron.jl") -include("nitrates_ammonium.jl") -include("oxygen.jl") -include("phosphates.jl") -include("particulate_organic_matter.jl") -include("silicon_in_particles.jl") -include("silicon.jl") -include("zooplankton.jl") -include("mean_mixed_layer_vertical_diffusivity.jl") -include("compute_calcite_saturation.jl") - -function update_biogeochemical_state!(model, bgc::PISCES) - # this should come from utils - #update_mixed_layer_depth!(bgc, model) - - PAR = biogeochemical_auxiliary_fields(model.biogeochemistry.light_attenuation).PAR - - compute_euphotic_depth!(bgc.euphotic_depth, PAR) - - compute_mean_mixed_layer_vertical_diffusivity!(bgc.mean_mixed_layer_vertical_diffusivity, bgc.mixed_layer_depth, model) - - compute_calcite_saturation!(bgc.carbon_chemistry, bgc.calcite_saturation, model) - - #update_yearly_maximum_silicate!(bgc, model) - - return nothing -end - -# to work with the sediment model we need to tell in the redfield ratio etc. of some things, but for now we can ignore -@inline redfield(i, j, k, val_tracer_name, bgc::PISCES, tracers) = NaN - -@inline nitrogen_flux(i, j, k, grid, advection, bgc::PISCES, tracers) = NaN - -@inline carbon_flux(i, j, k, grid, advection, bgc::PISCES, tracers) = NaN - -@inline remineralisation_receiver(::PISCES) = :NH₄ - -# this is for positivity preservation, if you can work it out it would be great, I don't think PISCES conserves C but probably does Nitrogen -@inline conserved_tracers(::PISCES) = NaN - -@inline sinking_tracers(::PISCES) = (:POC, :GOC, :SFe, :BFe, :PSi, :CaCO₃) # please list them here - -@inline chlorophyll(bgc::PISCES, model) = model.tracers.Pᶜʰˡ + model.tracers.Dᶜʰˡ end # module diff --git a/src/Models/AdvectedPopulations/PISCES/adapts.jl b/src/Models/AdvectedPopulations/PISCES/adapts.jl new file mode 100644 index 000000000..8f9e6459e --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/adapts.jl @@ -0,0 +1,35 @@ +using Adapt + +import Adapt: adapt_structure + +# we can throw away all of the fields since they're delt with outside of the kernels +Adapt.adapt_structure(to, bgc::PISCES) = + PISCES(adapt(to, bgc.nanophytoplankton), + adapt(to, bgc.diatoms), + adapt(to, bgc.microzooplankton), + adapt(to, bgc.mesozooplankton), + adapt(to, bgc.dissolved_organic_matter), + adapt(to, bgc.particulate_organic_matter), + adapt(to, bgc.nitrogen), + adapt(to, bgc.iron), + adapt(to, bgc.silicate), + adapt(to, bgc.oxygen), + adapt(to, bgc.phosphate), + adapt(to, bgc.calcite), + adapt(to, bgc.carbon_system), + adapt(to, bgc.first_anoxia_threshold), + adapt(to, bgc.second_anoxia_threshold), + adapt(to, bgc.nitrogen_redfield_ratio), + adapt(to, bgc.phosphate_redfield_ratio), + adapt(to, bgc.mixed_layer_shear), + adapt(to, bgc.background_shear), + adapt(to, bgc.latitude), + adapt(to, bgc.day_length), + adapt(to, bgc.mixed_layer_depth), + adapt(to, bgc.euphotic_depth), + adapt(to, bgc.silicate_climatology), + adapt(to, bgc.mean_mixed_layer_vertical_diffusivity), + adapt(to, bgc.mean_mixed_layer_light), + adapt(to, bgc.carbon_chemistry), + adapt(to, bgc.silicate_climatology), + adapt(to, bgc.sinking_velocities)) diff --git a/src/Models/AdvectedPopulations/PISCES/base_production.jl b/src/Models/AdvectedPopulations/PISCES/base_production.jl new file mode 100644 index 000000000..96a470b21 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/base_production.jl @@ -0,0 +1,142 @@ +abstract type BaseProduction end + +@inline function (μ::BaseProduction)(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) + bₜ = μ.temperature_sensetivity + μ₀ = μ.base_growth_rate + α₀ = μ.initial_slope_of_PI_curve + β = μ.low_light_adaptation + + dark_tollerance = μ.dark_tollerance + + β₁ = phyto.blue_light_absorption + β₂ = phyto.green_light_absorption + β₃ = phyto.red_light_absorption + + PAR = β₁ * PAR₁ + β₂ * PAR₂ + β₃ * PAR₃ + + φ = bgc.latitude(y) + day_length = bgc.day_length(φ, t) + + dark_residence_time = max(0, zₑᵤ - zₘₓₗ) ^ 2 / κ + + fₜ = bₜ ^ T + + μᵢ = μ₀ * fₜ + + f₁ = 1.5 * day_length / (day_length + 0.5day) + + f₂ = 1 - dark_residence_time / (dark_residence_time + dark_tollerance) + + α = α₀ * (1 + β * exp(-PAR)) + + return μᵢ * f₁ * f₂ * light_limitation(μ, I, IChl, T, PAR, day_length, L, α) * L +end + +@inline function (μ::BaseProduction)(phyto, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + L, = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + + return μ(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) +end + +""" + NutrientLimitedProduction + +`BaseProduction` with light limitation moderated by nutrient availability. This is +the "origional" PISCES phytoplankton growth rate model. Growth rate is of the form: + +```math +μ = μ₁f₁(τᵈ)f₂(zₘₓₗ)(1-exp(-α θᶜʰˡ PAR / τ μ₀ L)) L. +``` + +Keyword Arguments +================= +- `base_growth_rate`: the base growth rate, μ₀, in (1/s) +- `temperatrue_sensetivity`: temperature sensetivity parameter, b, giving μ₁ = μ₀ bᵀ where T is temperature +- `dark_tollerance`: the time that the phytoplankton survives in darkness below the euphotic layer, τᵈ (s) +- `initial_slope_of_PI_curve`: the relationship between photosynthesis and irradiance, α₀ (1/W/m²) +- `low_light_adaptation`: factor increasing the sensetivity of photosynthesis to irradiance, β, + giving α = α₀(1 + exp(-PAR)), typically set to zero + +""" +@kwdef struct NutrientLimitedProduction{FT} <: BaseProduction + base_growth_rate :: FT = 0.6 / day # 1 / s + temperature_sensetivity :: FT = 1.066 # + dark_tollerance :: FT # s +initial_slope_of_PI_curve :: FT = 2.0 # + low_light_adaptation :: FT = 0.0 # +end + +@inline function light_limitation(μ::NutrientLimitedProduction, I, IChl, T, PAR, day_length, L, α) + μᵢ = base_production_rate(μ, T) + + θ = IChl / (12 * I + eps(0.0)) + + return 1 - exp(-α * θ * PAR / (day_length * μᵢ * L + eps(0.0))) +end + +""" + NutrientLimitedProduction + +`BaseProduction` with light limitation moderated by nutrient availability. This is +the "new production" PISCES phytoplankton growth rate model. Growth rate is of the form: + +```math +μ = μ₁f₁(τ)f₂(zₘₓₗ)(1-exp(-α θᶜʰˡ PAR / τ (bᵣ + μᵣ))) L. +``` + +Keyword Arguments +================= +- `base_growth_rate`: the base growth rate, μ₀, in (1/s) +- `temperatrue_sensetivity`: temperature sensetivity parameter, b, giving μ₁ = μ₀ bᵀ where T is temperature +- `dark_tollerance`: the time that the phytoplankton survives in darkness below the euphotic layer, τᵈ (s) +- `initial_slope_of_PI_curve`: the relationship between photosynthesis and irradiance, α₀ (1/W/m²) +- `low_light_adaptation`: factor increasing the sensetivity of photosynthesis to irradiance, β, + giving α = α₀(1 + exp(-PAR)), typically set to zero +- `basal_respiration_rate`: reference respiration rate, bᵣ (1/s) +- `reference_growth_rate`: reference growth rate, μᵣ (1/s) + +""" +@kwdef struct GrowthRespirationLimitedProduction{FT} <: BaseProduction + base_growth_rate :: FT = 0.6 / day # 1 / s + temperature_sensetivity :: FT = 1.066 # + dark_tollerance :: FT # s +initial_slope_of_PI_curve :: FT = 2.0 # + low_light_adaptation :: FT = 0.0 # + basal_respiration_rate :: FT = 0.033/day # 1 / s + reference_growth_rate :: FT = 1.0/day # 1 / s +end + +@inline function light_limitation(μ::GrowthRespirationLimitedProduction, I, IChl, T, PAR, day_length, L, α) + bᵣ = μ.basal_respiration_rate + μᵣ = μ.reference_growth_rate + + θ = IChl / (12 * I + eps(0.0)) + + return 1 - exp(-α * θ * PAR / (day_length * (bᵣ + μᵣ))) +end + +@inline function production_and_energy_assimilation_absorption_ratio(growth_rate, phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR, PAR₁, PAR₂, PAR₃, L) + α₀ = growth_rate.initial_slope_of_PI_curve + β = growth_rate.low_light_adaptation + + α = α₀ * (1 + β * exp(-PAR)) + + φ = bgc.latitude(y) + day_length = bgc.day_length(φ, t) + + f₁ = 1.5 * day_length / (day_length + 0.5day) + + μ = growth_rate(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) + μ̌ = μ / f₁ * day_length + + return μ, 12 * μ̌ * I / (α * IChl * PAR + eps(0.0)) * L # (1 / s, unitless) +end + +@inline function base_production_rate(growth_rate, T) + bₜ = growth_rate.temperature_sensetivity + μ₀ = growth_rate.base_growth_rate + + fₜ = bₜ ^ T + + return μ₀ * fₜ +end diff --git a/src/Models/AdvectedPopulations/PISCES/calcite.jl b/src/Models/AdvectedPopulations/PISCES/calcite.jl index c59ed1fc2..744f368e6 100644 --- a/src/Models/AdvectedPopulations/PISCES/calcite.jl +++ b/src/Models/AdvectedPopulations/PISCES/calcite.jl @@ -1,60 +1,79 @@ +""" + Calcite -#Calcium carbonate is assumed only to exist in the form of calcite. +Stores the parameter values for calcite (`CaCO₃`) evolution. -#This document contains functions for: - #R_CaCO₃ (eq77) - #production_of_sinking_calcite (eq76) - #Forcing for CaCO₃ (eq75) +Keyword Arguments +================= +- `base_rain_ratio`: the base fraction of Coccolithophores +- `base_dissolution_rate`: base rate of calcite dissolution (1/s) +- `dissolution_exponent`: exponent of calcite excess for dissolution rate -#A type of phytoplankton (coccolithophores) have shells made from calcium carbonate. Either through mortality or grazing, this may be routed to production of sinking calcite. - -#Dissolution rate of calcite -@inline function dissolution_of_calcite(CaCO₃, bgc, Ω) - #Calcite can break down into DIC. This describes the dissolution rate. - λ_CaCO₃ = bgc.dissolution_rate_of_calcite - nca = bgc.exponent_in_the_dissolution_rate_of_calcite - ΔCO₃²⁻ = max(0, 1 - Ω) - return λ_CaCO₃*(ΔCO₃²⁻)^nca #79 +""" +@kwdef struct Calcite{FT} + base_rain_ratio :: FT = 0.3 # + base_dissolution_rate :: FT = 0.197 / day # 1 / s + dissolution_exponent :: FT = 1.0 # end -#The rain ratio is a ratio of coccolithophores calcite to particles of organic carbon. Increases proportional to coccolithophores, and parametrised based on simple assumptions on coccolithophores growth. -@inline function rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc) - r_CaCO₃ = bgc.rain_ratio_parameter - Kₙₕ₄ᴾᵐⁱⁿ = bgc.min_half_saturation_const_for_ammonium.P - Sᵣₐₜᴾ = bgc.size_ratio_of_phytoplankton.P - Pₘₐₓ = bgc.threshold_concentration_for_size_dependency.P - P₁ = I₁(P, Pₘₐₓ) - P₂ = I₂(P, Pₘₐₓ) - Lₙᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[5] - Kₙₕ₄ᴾ = nutrient_half_saturation_const(Kₙₕ₄ᴾᵐⁱⁿ, P₁, P₂, Sᵣₐₜᴾ) - Lₗᵢₘᶜᵃᶜᵒ³ = min(Lₙᴾ, concentration_limitation(Fe, 6e-11), concentration_limitation(PO₄, Kₙₕ₄ᴾ)) - return (r_CaCO₃*Lₗᵢₘᶜᵃᶜᵒ³*T*max(1, P/2)*max(0, PAR - 1)*30*(1 + exp((-(T-10)^2)/25))*min(1, 50/(abs(zₘₓₗ) + eps(0.0)))/((0.1 + T)*(4 + PAR)*(30 + PAR))) #eq77 +@inline function (calcite::Calcite)(::Val{:CaCO₃}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + production = calcite_production(calcite, bgc, z, P, D, PChl, PFe, Z, M, POC, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + dissolution = calcite_dissolution(calcite, CaCO₃, Ω) + + return production - dissolution end -#Defined the production of sinking calcite. This is calculated as a ratio to carbon. -#Coccolithophores shells can be routed to sinking particles through mortality and as a proportion of grazed shells. The rain ratio then gives conversion to sinking calcite from carbon. -@inline function production_of_sinking_calcite(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, D, Z, M, POC, T, PAR, zₘₓₗ, z, bgc) - mᴾ = bgc.phytoplankton_mortality_rate.P - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - ηᶻ = bgc.fraction_of_calcite_not_dissolving_in_guts.Z - ηᴹ = bgc.fraction_of_calcite_not_dissolving_in_guts.M - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear +@inline function calcite_production(calcite, bgc, z, P, D, PChl, PFe, Z, M, POC, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + R = rain_ratio(calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + microzooplankton = specific_calcite_grazing_loss(bgc.microzooplankton, P, D, Z, POC, T) * Z + mesozooplankton = specific_calcite_grazing_loss(bgc.mesozooplankton, P, D, Z, POC, T) * M - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + linear_mortality, quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) + total_mortality = 0.5 * (linear_mortality + quadratic_mortality) - return rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc)*(ηᶻ*grazing_Z(P, D, POC, T, bgc)[2]*Z+ηᴹ*grazing_M(P, D, Z, POC, T, bgc)[2]*M + 0.5*(mᴾ*concentration_limitation(P, Kₘ)*P + sh*wᴾ*P^2)) #eq76 + return R * (microzooplankton + mesozooplankton + total_mortality) end +# should this be in the particles thing? +@inline function rain_ratio(calcite::Calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + r = calcite.base_rain_ratio + + # assuming this is a type in Aumont 2015 based on Aumont 2005 + L, = bgc.nanophytoplankton.nutrient_limitation(bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + + L_CaCO₃ = L # maybe this is wrong, can't find the reference, others had it as min(Lₙᴾ, concentration_limitation(Fe, 6e-11), concentration_limitation(PO₄, Kₙₕ₄ᴾ)) + + phytoplankton_concentration_factor = max(1, P / 2) + low_light_factor = max(0, PAR - 1) / (4 + PAR) + high_light_factor = 30 / (30 + PAR) -#Forcing for calcite -@inline function (bgc::PISCES)(::Val{:CaCO₃}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) + low_temperature_factor = max(0, T / (T + 0.1)) # modified from origional as it goes negative and does not achieve goal otherwise + high_temperature_factor = 1 + exp(-(T - 10)^2 / 25) - return (production_of_sinking_calcite(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, D, Z, M, POC, T, PAR, zₘₓₗ, z, bgc) - - dissolution_of_calcite(CaCO₃, bgc, Ω)*CaCO₃) #eq75, partial derivative omitted as sinking is accounted for in other parts of model -end \ No newline at end of file + depth_factor = min(1, -50/zₘₓₗ) + + return r * L_CaCO₃ * phytoplankton_concentration_factor * low_light_factor * high_light_factor * low_temperature_factor * high_temperature_factor * depth_factor +end + +@inline function calcite_dissolution(calcite, CaCO₃, Ω) + λ = calcite.base_dissolution_rate + nca = calcite.dissolution_exponent + + ΔCaCO₃ = max(0, 1 - Ω) + + return λ * ΔCaCO₃ ^ nca * CaCO₃ +end diff --git a/src/Models/AdvectedPopulations/PISCES/carbonate_system.jl b/src/Models/AdvectedPopulations/PISCES/carbonate_system.jl index 2325a11a4..e17cd5a26 100644 --- a/src/Models/AdvectedPopulations/PISCES/carbonate_system.jl +++ b/src/Models/AdvectedPopulations/PISCES/carbonate_system.jl @@ -1,101 +1,88 @@ -#This document contains functions for: - #Forcing for DIC. - #Forcing for Alk. - -#DIC is significant as required by phytoplankton for photosynthesis, and by calcifying organisms for calcite shells. - -@inline function (bgc::PISCES)(::Val{:DIC}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR², PAR³) - #Parameters - γᶻ, γᴹ = bgc.excretion_as_DOM - σᶻ, σᴹ = bgc.non_assimilated_fraction - eₘₐₓᶻ, eₘₐₓᴹ = bgc. max_growth_efficiency_of_zooplankton - αᴾ, αᴰ = bgc.initial_slope_of_PI_curve - - bFe = Fe - - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - #Growth rates for phytoplankton - φ = bgc.latitude - φ = latitude(φ, y) - - L_day = day_length(φ, t) - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR², PAR³, bgc) - PARᴰ = D_PAR(PAR₁, PAR², PAR³, bgc) - - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - - #Bacteria - zₘₐₓ = max(abs(zₘₓₗ), abs(zₑᵤ)) - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - return (γᶻ*(1 - eᶻ - σᶻ)*∑gᶻ*Z + γᴹ*(1 - eᴹ - σᴹ)*(∑gᴹ + ∑g_FFᴹ)*M + - γᴹ*upper_respiration(M, T, bgc) + - oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) + - denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) + - dissolution_of_calcite(CaCO₃, bgc, Ω)*CaCO₃ - - production_of_sinking_calcite(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, D, Z, M, POC, T, PAR, zₘₓₗ, z, bgc) - - μᴰ*D - μᴾ*P) #eq59 +""" + CarbonateSystem + +Default parameterisation for `DIC`` and `Alk`alinity evolution. +""" +struct CarbonateSystem end + +@inline function (carbonates::CarbonateSystem)(::Val{:DIC}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + microzooplankton_respiration = specific_inorganic_grazing_waste(bgc.microzooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * Z + mesozooplankton_respiration = specific_inorganic_grazing_waste(bgc.mesozooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * M + + zooplankton_respiration = microzooplankton_respiration + mesozooplankton_respiration + + upper_trophic_respiration = inorganic_upper_trophic_respiration_product(bgc.mesozooplankton, M, T) + + dissolved_degredation = bacterial_degradation(bgc.dissolved_organic_matter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + calcite_diss = calcite_dissolution(bgc.calcite, CaCO₃, Ω) + + calcite_prod = calcite_production(bgc.calcite, bgc, z, P, D, PChl, PFe, Z, M, POC, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + nanophytoplankton_consumption, = total_production(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + diatom_consumption, = total_production(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + consumption = nanophytoplankton_consumption + diatom_consumption + + return zooplankton_respiration + upper_trophic_respiration + dissolved_degredation + calcite_diss - calcite_prod - consumption end -@inline function (bgc::PISCES)(::Val{:Alk}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR², PAR³) # eq59 - #Parameters - θᴺᶜ = bgc.NC_redfield_ratio - rₙₒ₃¹ = bgc. CN_ratio_of_denitrification - rₙₕ₄¹ = bgc.CN_ratio_of_ammonification - γᶻ, γᴹ = bgc.excretion_as_DOM - σᶻ, σᴹ = bgc.non_assimilated_fraction - eₘₐₓᶻ, eₘₐₓᴹ = bgc. max_growth_efficiency_of_zooplankton - λₙₕ₄ = bgc.max_nitrification_rate - - bFe = Fe - - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - #Uptake rates of nitrogen and ammonium - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR², PAR³, bgc) - PARᴰ = D_PAR(PAR₁, PAR², PAR³, bgc) - - μₙₒ₃ᴾ = uptake_rate_nitrate_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₒ₃ᴰ = uptake_rate_nitrate_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - μₙₕ₄ᴾ = uptake_rate_ammonium_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₕ₄ᴰ = uptake_rate_ammonium_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - - #Bacteria - zₘₐₓ = max(abs(zₘₓₗ), abs(zₑᵤ)) - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - return (θᴺᶜ*oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) - + θᴺᶜ*(rₙₒ₃¹ + 1)*denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) - + θᴺᶜ*γᶻ*(1 - eᶻ - σᶻ)*∑gᶻ*Z + θᴺᶜ*γᴹ*(1 - eᴹ - σᴹ)*(∑gᴹ + ∑g_FFᴹ - + θᴺᶜ*γᴹ*upper_respiration(M, T, bgc))*M + θᴺᶜ*μₙₒ₃ᴾ*P + θᴺᶜ*μₙₒ₃ᴰ*D - + N_fixation(bFe, PO₄, T, P, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, PAR, bgc) - + 2*dissolution_of_calcite(CaCO₃, bgc, Ω)*CaCO₃ + θᴺᶜ*oxygen_conditions(O₂, bgc)*(rₙₕ₄¹ - 1)*λₙₕ₄*NH₄ - - θᴺᶜ*μₙₕ₄ᴾ*P - θᴺᶜ*μₙₕ₄ᴰ*D- 2*θᴺᶜ*nitrification(NH₄, O₂, λₙₕ₄, PAR, bgc) - - 2*production_of_sinking_calcite(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, D, Z, M, POC, T, PAR, zₘₓₗ, z, bgc)) #eq81 +@inline function (carbonates::CarbonateSystem)(::Val{:Alk}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + θ = bgc.nitrogen_redfield_ratio + + nitrate_production = bgc.nitrogen(Val(:NO₃), bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) * θ + + ammonia_production = bgc.nitrogen(Val(:NH₄), bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) * θ + + calcite_production = bgc.calcite(Val(:CaCO₃), bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + # I think there are typos in Aumount 2015 but this is what it should be + + return ammonia_production - nitrate_production - 2 * calcite_production end diff --git a/src/Models/AdvectedPopulations/PISCES/common.jl b/src/Models/AdvectedPopulations/PISCES/common.jl index eb25a8a01..fbf386bde 100644 --- a/src/Models/AdvectedPopulations/PISCES/common.jl +++ b/src/Models/AdvectedPopulations/PISCES/common.jl @@ -1,32 +1,80 @@ using KernelAbstractions: @kernel, @index +using Oceananigans.Fields: flatten_node using Oceananigans.Grids: znode, zspacing +import Oceananigans.Fields: flatten_node + +# TODO: move this to Oceananigans +@inline flatten_node(::Nothing, ::Nothing, z) = tuple(z) + @inline shear(z, zₘₓₗ, background_shear, mixed_layer_shear) = ifelse(z <= zₘₓₗ, background_shear, mixed_layer_shear) # Given as 1 in Aumont paper -@inline latitude(φ, y) = φ -@inline latitude(::Nothing, y) = y +""" + ModelLatitude + +Returns the latitude specified by the model grid (`y`). +""" +struct ModelLatitude end + +""" + PrescribedLatitude + +Returns the prescribed latitude rather than the model grid `y` position. +""" +struct PrescribedLatitude{FT} + latitude :: FT # ° +end + +@inline (pl::PrescribedLatitude)(y) = pl.latitude +@inline (::ModelLatitude)(y) = y -# we should probably extend this to use DateTime dates at some point -@inline function day_length(φ, t) +""" + day_length_function(φ, t) + +Returns the length of day in seconds at the latitude `φ`, `t`seconds after the start of the year. +""" +@inline function day_length_function(φ, t) # as per Forsythe et al., 1995 (https://doi.org/10.1016/0304-3800(94)00034-F) p = asind(0.39795 * cos(0.2163108 + 2 * atan(0.9671396 * tan(0.00860 * (floor(Int, t / day) - 186))))) - return 24 - 24 / 180 * acosd((sind(0.8333) + sind(φ) * sind(p)) / (cosd(φ) * cosd(p))) + return (24 - 24 / 180 * acosd((sind(0.8333) + sind(φ) * sind(p)) / (cosd(φ) * cosd(p)))) * day end +""" + DepthDependantSinkingSpeed(; minimum_speed = 30/day, + maximum_speed = 200/day, + maximum_depth = 500) + +Returns sinking speed for particles which sink at `minimum_speed` in the +surface ocean (the deepest of the mixed and euphotic layers), and accelerate +to `maximum_speed` below that depth and `maximum_depth`. +""" @kwdef struct DepthDependantSinkingSpeed{FT} - minimum_speed :: FT = 2/day - maximum_speed :: FT = 200/day - maximum_depth :: FT = 5000.0 + minimum_speed :: FT = 30/day # m/s - in NEMO the min and max speeds are both 50m/day + maximum_speed :: FT = 200/day # m/s + maximum_depth :: FT = 5000.0 # m end # I can't find any explanation as to why this might depend on the euphotic depth -function (p::DepthDependantSinkingSpeed)(i, j, k, grid, mixed_layer_depth, euphotic_depth) +@inline function (p::DepthDependantSinkingSpeed)(i, j, k, grid, mixed_layer_depth, euphotic_depth) zₘₓₗ = @inbounds mixed_layer_depth[i, j, k] zₑᵤ = @inbounds euphotic_depth[i, j, k] z = znode(i, j, k, grid, Center(), Center(), Center()) - return - p.minimum_speed + (p.maximum_speed - p.minimum_speed) * min(0, z - min(zₘₓₗ, zₑᵤ)) / 5000 + w = - p.minimum_speed + (p.maximum_speed - p.minimum_speed) * min(0, z - min(zₘₓₗ, zₑᵤ)) / 5000 + + return ifelse(k == grid.Nz + 1, 0, w) end + +# don't actually use this version but might be useful since we can do it +@inline (p::DepthDependantSinkingSpeed)(z, zₘₓₗ, zₑᵤ) = + ifelse(z < 0, - p.minimum_speed + (p.maximum_speed - p.minimum_speed) * min(0, z - min(zₘₓₗ, zₑᵤ)) / 5000, 0) + +@inline function anoxia_factor(bgc, O₂) + min_1 = bgc.first_anoxia_threshold + min_2 = bgc.second_anoxia_threshold + + return min(1, max(0, 0.4 * (min_1 - O₂) / (min_2 + O₂))) +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl b/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl index 112e97b77..12e0a0ca4 100644 --- a/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl +++ b/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl @@ -29,7 +29,7 @@ end z = znode(i, j, k, grid, Center(), Center(), Center()) - P = abs(z) * g_Earth * 1026 / 100000 # very rough - don't think we should bother integrating the actual density + P = abs(z) * g_Earth * 1026 / 100000 # rough but I don't think we should bother integrating the actual density @inbounds calcite_saturation[i, j, k] = CarbonChemistryModel.calcite_saturation(carbon_chemistry; DIC, T, S, Alk, P, silicate) end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/coupling_utils.jl b/src/Models/AdvectedPopulations/PISCES/coupling_utils.jl new file mode 100644 index 000000000..12eff15e5 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/coupling_utils.jl @@ -0,0 +1,42 @@ +import OceanBioME.Models.Sediments: nitrogen_flux, carbon_flux, remineralisation_receiver, sinking_tracers + +# sediment models +@inline redfield(val_name, bgc::PISCES, tracers) = bgc.nitrogen_redfield_ratio + +@inline nitrogen_flux(i, j, k, grid, advection, bgc::PISCES, tracers) = bgc.nitrogen_redfield_ratio * carbon_flux(i, j, k, grid, advection, bgc, tracers) + +@inline carbon_flux(i, j, k, grid, advection, bgc::PISCES, tracers) = sinking_flux(i, j, k, grid, adveciton, bgc, Val(:POC), tracers) + + sinking_flux(i, j, k, grid, adveciton, bgc, Val(:GOC), tracers) + +@inline remineralisation_receiver(::PISCES) = :NH₄ + +@inline sinking_tracers(::PISCES) = (:POC, :GOC, :SFe, :BFe, :PSi, :CaCO₃) # please list them here + +# light attenuation model +@inline chlorophyll(::PISCES, model) = model.tracers.PChl + model.tracers.DChl + +# negative tracer scaling +# TODO: deal with remaining (PChl, DChl, O₂, Alk) - latter two should never be near zero +@inline function conserved_tracers(bgc::PISCES; ntuple = false) + carbon = (:P, :D, :Z, :M, :DOC, :POC, :GOC, :DIC, :CaCO₃) + + # iron ratio for DOC might be wrong + iron = (tracers = (:PFe, :DFe, :Z, :M, :SFe, :BFe, :Fe), + scalefactors = (1, 1, bgc.microzooplankton.iron_ratio, bgc.mesozooplankton.iron_ratio, 1, 1, 1)) + + θ_PO₄ = bgc.phosphate_redfield_ratio + phosphate = (tracers = (:P, :D, :Z, :M, :DOC, :POC, :GOC, :PO₄), + scalefactors = (θ_PO₄, θ_PO₄, θ_PO₄, θ_PO₄, θ_PO₄, θ_PO₄, θ_PO₄, 1)) + + silicon = (:DSi, :Si, :PSi) + + θN = bgc.nitrogen_redfield_ratio + nitrogen = (tracers = (:NH₄, :NO₃, :P, :D, :Z, :M, :DOC, :POC, :GOC), + scalefactors = (1, 1, θN, θN, θN, θN, θN, θN, θN)) + + if ntuple + return (; carbon, iron, phosphate, silicon, nitrogen) + else + return (carbon, iron, phosphate, silicon, nitrogen) + end +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter.jl b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter.jl index d2e68a4c9..0f0e69894 100644 --- a/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter.jl +++ b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter.jl @@ -1,157 +1,152 @@ +""" + DissolvedOrganicMatter + +Parameterisation of dissolved organic matter which depends on a bacterial +concentration derived from the concentration of zooplankton. +""" +@kwdef struct DissolvedOrganicMatter{FT, AP} + remineralisation_rate :: FT = 0.3/day # 1 / s + microzooplankton_bacteria_concentration :: FT = 0.7 # + mesozooplankton_bacteria_concentration :: FT = 1.4 # + maximum_bacteria_concentration :: FT = 4.0 # mmol C / m³ + bacteria_concentration_depth_exponent :: FT = 0.684 # + reference_bacteria_concentration :: FT = 1.0 # mmol C / m³ + temperature_sensetivity :: FT = 1.066 # + doc_half_saturation_for_bacterial_activity :: FT = 417.0 # mmol C / m³ + nitrate_half_saturation_for_bacterial_activity :: FT = 0.03 # mmol N / m³ + ammonia_half_saturation_for_bacterial_activity :: FT = 0.003 # mmol N / m³ + phosphate_half_saturation_for_bacterial_activity :: FT = 0.003 # mmol P / m³ + iron_half_saturation_for_bacterial_activity :: FT = 0.01 # μmol Fe / m³ +# (1 / (mmol C / m³), 1 / (mmol C / m³), 1 / (mmol C / m³), 1 / (mmol C / m³) / s, 1 / (mmol C / m³) / s) + aggregation_parameters :: AP = (0.37, 102, 3530, 5095, 114) .* (10^-6 / day) + maximum_iron_ratio_in_bacteria :: FT = 0.06 # μmol Fe / mmol C + iron_half_saturation_for_bacteria :: FT = 0.3 # μmol Fe / m³ + maximum_bacterial_growth_rate :: FT = 0.6 / day # 1 / s +end + +@inline function (dom::DissolvedOrganicMatter)(::Val{:DOC}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + nanophytoplankton_exudation = dissolved_exudate(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + diatom_exudation = dissolved_exudate(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + phytoplankton_exudation = nanophytoplankton_exudation + diatom_exudation + + particulate_degredation = specific_degredation_rate(bgc.particulate_organic_matter, bgc, O₂, T) * POC + + respiration_product = dissolved_upper_trophic_respiration_product(bgc.mesozooplankton, M, T) + + microzooplankton_grazing_waste = specific_dissolved_grazing_waste(bgc.microzooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * Z + mesozooplankton_grazing_waste = specific_dissolved_grazing_waste(bgc.mesozooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * M + + grazing_waste = microzooplankton_grazing_waste + mesozooplankton_grazing_waste + + degredation = bacterial_degradation(dom, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + aggregation_to_particles, = aggregation(dom, bgc, z, DOC, POC, GOC, zₘₓₗ) + + return phytoplankton_exudation + particulate_degredation + respiration_product + grazing_waste - degredation - aggregation_to_particles +end + +@inline function bacteria_concentration(dom::DissolvedOrganicMatter, z, Z, M, zₘₓₗ, zₑᵤ) + bZ = dom.microzooplankton_bacteria_concentration + bM = dom.mesozooplankton_bacteria_concentration + a = dom.bacteria_concentration_depth_exponent + + zₘ = min(zₘₓₗ, zₑᵤ) + + surface_bacteria = min(4, bZ * Z + bM * M) + + depth_factor = (zₘ / z) ^ a + + return ifelse(z >= zₘ, 1, depth_factor) * surface_bacteria +end -#The carbon compartment of the model is composed of P, D, Z, M, DOC, POC, GOC, DIC and CaCO₃. Carbon is conserved within the model. -#These pools of carbon have complex interactions. -#Particles of carbon may degrade into smaller particles, or aggregate into larger particles. -#Phytoplankton uptake DIC for biological processes including photosynthese. -#Mortality returns carbon in biomasses to particles of carbon. -#Remineralisation processes break organic carbon down into inorganic carbon. -#Particles of carbon experience sinking, and this is significant in tracking carbon export to the deep ocean. - -#This document contains forcing equations for: - #P_up, R_up (eqs30a, 30b) - #Remin, Denit (eqs 33a, 33b) - #Bacteria population (eq35) - #Aggregation of DOC (eq36) - #Degradation rate of POM (applies to POC and GOC, eq38) - #Limiting nutrients for bacteris (eq34) - #Forcing for DOC - -#Remineralisation of DOM can be either oxic (Remin), or anoxic (Denit). This is regulated by oxygen_concentration. -#Remineralisation processes are responsible for breaking down organic matter back into inorganic forms. These terms contribute to forcing equations for inorganic nutrients. -#Remineralisation occurs in oxic waters. -@inline function oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) - O₂ᵘᵗ = bgc.OC_for_ammonium_based_processes - λ_DOC = bgc.remineralisation_rate_of_DOC - bₚ = bgc.temperature_sensitivity_of_growth - Bactᵣₑ = bgc.bacterial_reference - - Lₗᵢₘᵇᵃᶜᵗ = bacterial_activity(DOC, PO₄, NO₃, NH₄, bFe, bgc)[2] - - #min((O₂)/(O₂ᵘᵗ), λ_DOC*bₚ^T*(1 - oxygen_conditions(O₂, bgc)) * Lₗᵢₘᵇᵃᶜᵗ * (Bact)/(Bactᵣₑ) * DOC), definition from paper did not make sense with dimensions, modified below - return λ_DOC*bₚ^T*(1 - oxygen_conditions(O₂, bgc)) * Lₗᵢₘᵇᵃᶜᵗ * (Bact)/(Bactᵣₑ + eps(0.0)) * DOC #33a +@inline function bacteria_activity(dom::DissolvedOrganicMatter, DOC, NO₃, NH₄, PO₄, Fe) + K_DOC = dom.doc_half_saturation_for_bacterial_activity + K_NO₃ = dom.nitrate_half_saturation_for_bacterial_activity + K_NH₄ = dom.ammonia_half_saturation_for_bacterial_activity + K_PO₄ = dom.phosphate_half_saturation_for_bacterial_activity + K_Fe = dom.iron_half_saturation_for_bacterial_activity + + DOC_limit = DOC / (DOC + K_DOC) + + L_N = (K_NO₃ * NH₄ + K_NH₄ * NO₃) / (K_NO₃ * K_NH₄ + K_NO₃ * NH₄ + K_NH₄ * NO₃) + + L_PO₄ = PO₄ / (PO₄ + K_PO₄) + + L_Fe = Fe / (Fe + K_Fe) + + # assuming typo in paper otherwise it doesn't make sense to formulate L_NH₄ like this + limiting_quota = min(L_N, L_PO₄, L_Fe) + + return limiting_quota * DOC_limit end -#Denitrification is the remineralisation process in anoxic waters. -@inline function denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) - λ_DOC = bgc.remineralisation_rate_of_DOC - rₙₒ₃¹ = bgc.CN_ratio_of_denitrification - bₚ = bgc.temperature_sensitivity_of_growth - Bactᵣₑ = bgc.bacterial_reference +@inline function bacterial_degradation(dom::DissolvedOrganicMatter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + Bact_ref = dom.reference_bacteria_concentration + b = dom.temperature_sensetivity + λ = dom.remineralisation_rate + + f = b^T - Lₗᵢₘᵇᵃᶜᵗ = bacterial_activity(DOC, PO₄, NO₃, NH₄, bFe, bgc)[2] + Bact = bacteria_concentration(dom, z, Z, M, zₘₓₗ, zₑᵤ) - #min(NO₃/rₙₒ₃¹, λ_DOC*bₚ^T* oxygen_conditions(O₂, bgc)* Lₗᵢₘᵇᵃᶜᵗ*(Bact)/(Bactᵣₑ) * DOC), definition from paper did not make sense with dimensions, modified below - return λ_DOC*bₚ^T* oxygen_conditions(O₂, bgc)* Lₗᵢₘᵇᵃᶜᵗ*(Bact)/(Bactᵣₑ + eps(0.0)) * DOC #33b + LBact = bacteria_activity(dom, DOC, NO₃, NH₄, PO₄, Fe) + + return λ * f * LBact * Bact / Bact_ref * DOC # differes from Aumont 2015 since the dimensions don't make sense end -#Bacteria are responsible for carrying out biological remineralisation processes. They are represent in the following formulation, with biomass decreasing at depth. -@inline bacterial_biomass(zₘₐₓ, z, Z, M) = - ifelse(abs(z) <= zₘₐₓ, min(0.7 * (Z + 2M), 4), min(0.7 * (Z + 2M), 4) * (abs(zₘₐₓ/(z + eps(0.0))) ^ 0.683)) #35b - -#Bacterial activity parameterises remineralisation of DOC. It is dependent on nutrient availability, and remineraisation half saturation constant. -@inline function bacterial_activity(DOC, PO₄, NO₃, NH₄, bFe, bgc) - - Kₚₒ₄ᵇᵃᶜᵗ = bgc.PO4_half_saturation_const_for_DOC_remin - Kₙₒ₃ᵇᵃᶜᵗ = bgc.NO3_half_saturation_const_for_DOC_remin - Kₙₕ₄ᵇᵃᶜᵗ = bgc.NH4_half_saturation_const_for_DOC_remin - K_Feᵇᵃᶜᵗ = bgc.Fe_half_saturation_const_for_DOC_remin - K_DOC = bgc.half_saturation_const_for_DOC_remin - - L_DOCᵇᵃᶜᵗ = concentration_limitation(DOC, K_DOC) #34b - L_Feᵇᵃᶜᵗ = concentration_limitation(bFe, K_Feᵇᵃᶜᵗ) #34d - Lₚₒ₄ᵇᵃᶜᵗ = concentration_limitation(PO₄, Kₚₒ₄ᵇᵃᶜᵗ) #34e - - Lₙₕ₄ᵇᵃᶜᵗ = ammonium_limitation(NO₃, NH₄, Kₙₒ₃ᵇᵃᶜᵗ, Kₙₕ₄ᵇᵃᶜᵗ) #34g - Lₙₒ₃ᵇᵃᶜᵗ = nitrate_limitation(NO₃, NH₄, Kₙₒ₃ᵇᵃᶜᵗ, Kₙₕ₄ᵇᵃᶜᵗ) #34h - Lₙᵇᵃᶜᵗ = Lₙₒ₃ᵇᵃᶜᵗ + Lₙₕ₄ᵇᵃᶜᵗ #34f - Lₗᵢₘᵇᵃᶜᵗ = min(Lₙₕ₄ᵇᵃᶜᵗ, Lₚₒ₄ᵇᵃᶜᵗ, L_Feᵇᵃᶜᵗ) #34c - Lᵇᵃᶜᵗ = Lₗᵢₘᵇᵃᶜᵗ*L_DOCᵇᵃᶜᵗ #34a - - return Lᵇᵃᶜᵗ, Lₗᵢₘᵇᵃᶜᵗ +@inline function oxic_remineralisation(dom::DissolvedOrganicMatter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + ΔO₂ = anoxia_factor(bgc, O₂) + + degredation = bacterial_degradation(dom, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + return (1 - ΔO₂) * degredation end -#Aggregation processes for DOC. DOC can aggregate via turbulence and Brownian aggregation. These aggregated move to pools of larger particulates. -@inline function aggregation_process_for_DOC(DOC, POC, GOC, sh, bgc) - a₁ = bgc.aggregation_rate_of_DOC_to_POC_1 - a₂ = bgc.aggregation_rate_of_DOC_to_POC_2 - a₃ = bgc.aggregation_rate_of_DOC_to_GOC_3 - a₄ = bgc.aggregation_rate_of_DOC_to_POC_4 - a₅ = bgc.aggregation_rate_of_DOC_to_POC_5 +@inline function denitrifcation(dom::DissolvedOrganicMatter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + ΔO₂ = anoxia_factor(bgc, O₂) - Φ₁ᴰᴼᶜ = sh * (a₁*DOC + a₂*POC)*DOC #36a - Φ₂ᴰᴼᶜ = sh * (a₃*GOC) * DOC #36b - Φ₃ᴰᴼᶜ = (a₄*POC + a₅*DOC)*DOC #36c + degredation = bacterial_degradation(dom, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) - return Φ₁ᴰᴼᶜ, Φ₂ᴰᴼᶜ, Φ₃ᴰᴼᶜ + return ΔO₂ * degredation end -#Degradation rate of particles of carbon (refers to POC and GOC) -@inline function particles_carbon_degradation_rate(T, O₂, bgc) #has small magnitude as λₚₒ per day - λₚₒ= bgc.degradation_rate_of_POC - bₚ = bgc.temperature_sensitivity_of_growth - return λₚₒ*bₚ^T*(1 - 0.45*oxygen_conditions(O₂, bgc)) #38 +@inline function aggregation(dom::DissolvedOrganicMatter, bgc, z, DOC, POC, GOC, zₘₓₗ) + a₁, a₂, a₃, a₄, a₅ = dom.aggregation_parameters + + backgroound_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear + + shear = ifelse(z < zₘₓₗ, backgroound_shear, mixed_layer_shear) + + Φ₁ = shear * (a₁ * DOC + a₂ * POC) * DOC + Φ₂ = shear * (a₃ * GOC) * DOC + Φ₃ = (a₄ * POC + a₅ * DOC) * DOC + + return Φ₁ + Φ₂ + Φ₃, Φ₁, Φ₂, Φ₃ end -#Forcing for DOC -@inline function (bgc::PISCES)(::Val{:DOC}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - γᶻ = bgc.excretion_as_DOM.Z - γᴹ = bgc.excretion_as_DOM.M - σᶻ = bgc.non_assimilated_fraction.Z - σᴹ = bgc.non_assimilated_fraction.M - δᴾ = bgc.exudation_of_DOC.P - δᴰ = bgc.exudation_of_DOC.D - eₘₐₓᶻ = bgc.max_growth_efficiency_of_zooplankton.Z - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - αᴾ= bgc.initial_slope_of_PI_curve.P - αᴰ = bgc.initial_slope_of_PI_curve.D - wₚₒ = bgc.sinking_speed_of_POC - - φ = bgc.latitude - φ = latitude(φ, y) - - L_day = day_length(φ, t) - - g_FF = bgc.flux_feeding_rate - w_GOCᵐⁱⁿ = bgc.min_sinking_speed_of_GOC - bₘ = bgc.temperature_sensitivity_term.M - - ∑ᵢgᵢᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑ᵢgᵢᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - - w_GOC = sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) #41b - g_GOC_FFᴹ = g_FF*bₘ^T*w_GOC*GOC #29b - gₚₒ_FFᴹ = g_FF*bₘ^T*wₚₒ*POC - - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - Rᵤₚ = upper_respiration(M, T, bgc) - - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #41a - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - bFe = Fe #defined in previous PISCES model - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - - Remin = oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) - Denit = denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) - - Φ₁ᴰᴼᶜ, Φ₂ᴰᴼᶜ, Φ₃ᴰᴼᶜ = aggregation_process_for_DOC(DOC, POC, GOC, sh, bgc) - - return ((1 - γᶻ)*(1 - eᶻ - σᶻ)*∑ᵢgᵢᶻ*Z + (1 - γᴹ)*(1 - eᴹ - σᴹ)*(∑ᵢgᵢᴹ + g_GOC_FFᴹ + gₚₒ_FFᴹ)*M + - δᴰ*μᴰ*D + δᴾ*μᴾ*P + λₚₒ¹*POC + (1 - γᴹ)*Rᵤₚ - Remin - Denit - Φ₁ᴰᴼᶜ - Φ₂ᴰᴼᶜ - Φ₃ᴰᴼᶜ) #32 -end #changed this to include gₚₒ_FF \ No newline at end of file +@inline function bacterial_iron_uptake(dom::DissolvedOrganicMatter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + μ₀ = dom.maximum_bacterial_growth_rate + b = dom.temperature_sensetivity + θ = dom.iron_half_saturation_for_bacteria + K = dom.iron_half_saturation_for_bacteria + + μ = μ₀ * b^T + + Bact = bacteria_concentration(dom, z, Z, M, zₘₓₗ, zₑᵤ) + + L = bacteria_activity(dom, DOC, NO₃, NH₄, PO₄, Fe) + + return μ * L * θ * Fe / (Fe + K) * Bact +end diff --git a/src/Models/AdvectedPopulations/PISCES/group_methods.jl b/src/Models/AdvectedPopulations/PISCES/group_methods.jl new file mode 100644 index 000000000..50e505a65 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/group_methods.jl @@ -0,0 +1,160 @@ +# Please excuse this file, origionally each one was a single line like: +# (bgc::PISCES)(val_name::NANO_PHYTO, args...) = bgc.nanophytoplankton(val_name, bgc, args...) +# but that doesn't work on GPU because there are too many arguments +# see https://github.com/CliMA/Oceananigans.jl/discussions/3784 + +(bgc::PISCES)(val_name::NANO_PHYTO, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.nanophytoplankton(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::DIATOMS, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.diatoms(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:Z}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.microzooplankton(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:M}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.mesozooplankton(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:DOC}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.dissolved_organic_matter(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::PARTICLES, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.particulate_organic_matter(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + +(bgc::PISCES)(val_name::NITROGEN, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.nitrogen(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:Fe}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.iron(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:Si}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.silicate(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:CaCO₃}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.calcite(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:O₂}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.oxygen(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::Val{:PO₄}, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.phosphate(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + +(bgc::PISCES)(val_name::CARBON_SYSTEM, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃,NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) = + bgc.carbon_system(val_name, bgc, x, y, z, t, P, D, Z, M, PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) diff --git a/src/Models/AdvectedPopulations/PISCES/hack.jl b/src/Models/AdvectedPopulations/PISCES/hack.jl new file mode 100644 index 000000000..bb87d6e0c --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/hack.jl @@ -0,0 +1,18 @@ +using Oceananigans.Biogeochemistry: required_biogeochemical_tracers, required_biogeochemical_auxiliary_fields, extract_biogeochemical_fields +using Oceananigans.Grids: xnode, ynode, znode + +import Oceananigans.Biogeochemistry: biogeochemical_transition + +@inline function biogeochemical_transition(i, j, k, grid, bgc::PISCES, val_tracer_name, clock, fields) + tracer_names_to_extract = required_biogeochemical_tracers(bgc) + auxiliary_names_to_extract = required_biogeochemical_auxiliary_fields(bgc) + + tracer_fields_ijk = extract_biogeochemical_fields(i, j, k, grid, fields, tracer_names_to_extract) + auxiliary_fields_ijk = extract_biogeochemical_fields(i, j, k, grid, fields, auxiliary_names_to_extract) + + x = xnode(i, j, k, grid, Center(), Center(), Center()) + y = ynode(i, j, k, grid, Center(), Center(), Center()) + z = znode(i, j, k, grid, Center(), Center(), Center()) + + return bgc(val_tracer_name, x, y, z, clock.time, tracer_fields_ijk..., auxiliary_fields_ijk...) +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/iron.jl b/src/Models/AdvectedPopulations/PISCES/iron.jl index e8b0d68b9..824bd8209 100644 --- a/src/Models/AdvectedPopulations/PISCES/iron.jl +++ b/src/Models/AdvectedPopulations/PISCES/iron.jl @@ -1,118 +1,88 @@ -# This document contains functions for the following: - # free_organic_iron(eq65), dissolved free inorganic iron - # iron_colloid_aggregation_1, iron_colloid_aggregation_2, enhanced_scavenging, bacterial_uptake_Fe (eqs 61, 62, 63) - # Forcing for Fe (eq60) - - -#Iron is modelled in PISCES using simple chemistry model. Iron is assumed to be in the form of free organic iron Fe', and complexed iron FeL. -#Iron is explictly modelled in the following compartments of the model, Pᶠᵉ, Dᶠᵉ, SFe, BFe, Fe. In zooplankton a fixed iron carbon ratio is assumed, and iron implictly modelled in zooplankton in this way. Iron in bacteria is formulated through Bactfe. -#Iron is lost through Scav and enhanced_scavenging terms. Aside from this, iron is conserved, accounting for all other explicit and implicit compartments of iron - -#Determine concentration of free organic iron. This is the only form of iron assumed susceptible to scavenging. -@inline function free_organic_iron(Fe, DOC, T) - Lₜ = max(0.09*(DOC + 40) - 3, 0.6) # This may also be taken to be a constant parameter, total_concentration_of_iron_ligands - K_eqᶠᵉ = exp(16.27 - 1565.7/max(T + 273.15, 5)) #check this value - Δ = 1 + K_eqᶠᵉ*Lₜ - K_eqᶠᵉ*Fe - return (-Δ + sqrt(Δ^2 + 4*K_eqᶠᵉ*Fe))/(2*K_eqᶠᵉ + eps(0.0)) #eq65 +""" + SimpleIron(; excess_scavenging_enhancement = 1000) + +Parameterisation for iron evolution, not the "complex chemistry" model +of Aumount et al, 2015. Iron is scavenged (i.e. perminemtly removed from +the model) when the free iron concentration exeeds the ligand concentration +at a rate modified by `excess_scavenging_enhancement`. +""" +@kwdef struct SimpleIron{FT} + excess_scavenging_enhancement :: FT = 1000 # unitless end -#Colloids of iron may aggregate with DOM and be transferred to particulate pool -#iron_colloid_aggregation_1 is aggregation of colloids with DOC and POC. Routed to SFe. -@inline function iron_colloid_aggregation_1(sh, Fe, POC, DOC, T, bgc) - a₁ = bgc.aggregation_rate_of_DOC_to_POC_1 - a₂ = bgc.aggregation_rate_of_DOC_to_POC_2 - a₄ = bgc.aggregation_rate_of_DOC_to_POC_4 - a₅ = bgc.aggregation_rate_of_DOC_to_POC_5 - - FeL = Fe - free_organic_iron(Fe, DOC, T) #eq64 - Fe_coll = 0.5*FeL - return ((a₁*DOC + a₂*POC)*sh+a₄*POC + a₅*DOC)*Fe_coll #eq61a -end +@inline function (iron::SimpleIron)(::Val{:Fe}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) -#iron_colloid_aggregation_2 is aggregation of colloids with GOC. Routed to BFe. -@inline function iron_colloid_aggregation_2(sh, Fe, T, DOC, GOC, bgc) - a₃ = bgc.aggregation_rate_of_DOC_to_GOC_3 - FeL = Fe - free_organic_iron(Fe, DOC, T) - Fe_coll = 0.5*FeL - return a₃*GOC*sh*Fe_coll #eq61b -end + λ̄ = iron.excess_scavenging_enhancement + + λFe = iron_scavenging_rate(bgc.particulate_organic_matter, POC, GOC, CaCO₃, PSi) + + Fe′ = free_iron(iron, Fe, DOC, T) + total_ligand_concentration = max(0.6, 0.09 * (DOC + 40) - 3) + + # terminal process which removes iron from the ocean + ligand_aggregation = λ̄ * λFe * max(0, Fe - total_ligand_concentration) * Fe′ + + # other aggregation + colloidal_aggregation, = aggregation_of_colloidal_iron(iron, bgc.dissolved_organic_matter, bgc, z, DOC, POC, GOC, Fe, T, zₘₓₗ) + + aggregation = colloidal_aggregation + ligand_aggregation + + # scavening and bacterial uptake to particles + scav = λFe * (POC + GOC) * Fe′ + + BactFe = bacterial_iron_uptake(bgc.dissolved_organic_matter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + λPOC = specific_degredation_rate(bgc.particulate_organic_matter, bgc, O₂, T) -#When dissolved iron concentrations exceed total ligand concentrations scavenging is enhanced. -@inline function enhanced_scavenging(Fe, DOC, T, bgc) - λᶠᵉ = 1e-3 * bgc.slope_of_scavenging_rate_of_iron #parameter not defined in parameter list. Assumed scaled version λ_Fe to fit dimensions of Fe¹. - Lₜ = max(0.09*(DOC + 40) - 3, 0.6) - return λᶠᵉ*max(0, Fe - Lₜ)*free_organic_iron(Fe, DOC, T) #eq62 + # particle breakdown + particulate_degredation = λPOC * SFe + + # consumption + nanophytoplankton_consumption, = iron_uptake(bgc.nanophytoplankton, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T) + diatom_consumption, = iron_uptake(bgc.diatoms, bgc, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T) + + consumption = nanophytoplankton_consumption + diatom_consumption + + # grazing waste - this is the excess non assimilated into zooplankton when they consume iron rich phytoplankton + microzooplankton_waste = specific_non_assimilated_iron(bgc.microzooplankton, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) * Z + mesozooplankton_waste = specific_non_assimilated_iron(bgc.mesozooplankton, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) * M + + zooplankton_waste = microzooplankton_waste + mesozooplankton_waste + + # type in Aumount 2015, γ should not be present since DOC doesn't contain iron/there is no DOFe pool + respiration_product = upper_trophic_respiration_product(bgc.mesozooplankton, M, T) * bgc.mesozooplankton.iron_ratio + + return zooplankton_waste + respiration_product + particulate_degredation - consumption - scav - aggregation - BactFe end -#Formulation for bacterial uptake of iron. -@inline function bacterial_uptake_Fe(μₘₐₓ⁰, z, Z, M, Fe, DOC, PO₄, NO₃, NH₄, bFe, T, zₘₐₓ, bgc) - K_Feᴮ¹ = bgc.Fe_half_saturation_const_for_Bacteria - θₘₐₓᶠᵉᵇᵃᶜᵗ = bgc.max_FeC_ratio_of_bacteria - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - Lₗᵢₘᵇᵃᶜᵗ = bacterial_activity(DOC, PO₄, NO₃, NH₄, bFe, bgc)[2] - bₚ = bgc.temperature_sensitivity_of_growth - return μₘₐₓ⁰*(bₚ^T)*Lₗᵢₘᵇᵃᶜᵗ*θₘₐₓᶠᵉᵇᵃᶜᵗ*Fe*Bact/(K_Feᴮ¹ + Fe + eps(0.0)) #eq63 +@inline function free_iron(::SimpleIron, Fe, DOC, T) + # maybe some of these numbers should be parameters + ligands = max(0.6, 0.09 * (DOC + 40) - 3) + K = exp(16.27 - 1565.7 / max(T + 273.15, 5)) + Δ = 1 + K * ligands - K * Fe + + return (-Δ + √(Δ^2 + 4K * Fe)) / 2K end -@inline function (bgc::PISCES)(::Val{:Fe}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) #eq60 - #Parameters - σᶻ, σᴹ = bgc.non_assimilated_fraction - eₘₐₓᶻ, eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton - δᴾ, δᴰ = bgc.exudation_of_DOC - θₘₐₓᶠᵉᴾ, θₘₐₓᶠᵉᴰ = bgc.max_iron_quota - Sᵣₐₜᴾ, Sᵣₐₜᴰ = bgc.size_ratio_of_phytoplankton - K_Feᴾᶠᵉᵐⁱⁿ, K_Feᴰᶠᵉᵐⁱⁿ = bgc.min_half_saturation_const_for_iron_uptake - Pₘₐₓ, Dₘₐₓ = bgc.threshold_concentration_for_size_dependency - μₘₐₓ⁰ = bgc.growth_rate_at_zero - θᶠᵉᶻ = bgc.FeC_ratio_of_zooplankton - g_FF = bgc.flux_feeding_rate - bₘ = bgc.temperature_sensitivity_term.M - wₚₒ = bgc.sinking_speed_of_POC - - γᴹ = bgc.excretion_as_DOM.M #Removed γᴹ factor from upper_respiration to conserve iron implicitly lost through mesozooplankton quadratic mortality. - - bFe = Fe - - #Growth rate of iron biomass of phytoplankton - L_Feᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[6] - L_Feᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[6] - - μᴾᶠᵉ = phytoplankton_iron_biomass_growth_rate(P, Pᶠᵉ, θₘₐₓᶠᵉᴾ, Sᵣₐₜᴾ, K_Feᴾᶠᵉᵐⁱⁿ, Pₘₐₓ, L_Feᴾ, bFe, T, bgc) - μᴰᶠᵉ = phytoplankton_iron_biomass_growth_rate(D, Dᶠᵉ, θₘₐₓᶠᵉᴰ, Sᵣₐₜᴰ, K_Feᴰᶠᵉᵐⁱⁿ, Dₘₐₓ, L_Feᴰ, bFe, T, bgc) - - #Iron quotas - θᶠᵉᴾ = nutrient_quota(Pᶠᵉ, P) - θᶠᵉᴰ = nutrient_quota(Dᶠᵉ, D) - θᶠᵉᴾᴼᶜ = nutrient_quota(SFe, POC) - θᶠᵉᴳᴼᶜ = nutrient_quota(BFe, GOC) - - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ, gₚₒ_FF, g_GOC_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc) - w_GOC = sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) - - ∑θᶠᵉⁱgᵢᶻ = θᶠᵉᴾ*gₚᶻ + θᶠᵉᴰ*g_Dᶻ + θᶠᵉᴾᴼᶜ*gₚₒᶻ #over P, D, POC - ∑θᶠᵉⁱgᵢᴹ = θᶠᵉᴾ*gₚᴹ + θᶠᵉᴰ*g_Dᴹ + θᶠᵉᴾᴼᶜ*gₚₒᴹ + θᶠᵉᶻ*g_Zᴹ #graze on P, D, POC, Z - - #Iron in bacteria - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) - Bactfe = bacterial_uptake_Fe(μₘₐₓ⁰, z, Z, M, Fe, DOC, PO₄, NO₃, NH₄, bFe, T, zₘₐₓ, bgc) - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) #eₘₐₓᶻ used in paper but changed here to be consistent with eqs 24, 28 - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - - return (max(0, (1-σᶻ)*(∑θᶠᵉⁱgᵢᶻ/(∑gᶻ + eps(0.0))) - eᶻ*θᶠᵉᶻ)*∑gᶻ*Z - + max(0, (1-σᴹ)*(∑θᶠᵉⁱgᵢᴹ + θᶠᵉᴾᴼᶜ*gₚₒ_FF + θᶠᵉᴳᴼᶜ*g_GOC_FFᴹ )/(∑gᴹ+∑g_FFᴹ + eps(0.0)) - eᴹ*θᶠᵉᶻ)*(∑gᴹ+∑g_FFᴹ)*M - + θᶠᵉᶻ*upper_respiration(M, T, bgc) + λₚₒ¹*SFe - - (1 - δᴾ)*μᴾᶠᵉ*P - (1 - δᴰ)*μᴰᶠᵉ*D - - Fe_scavenging(POC, GOC, CaCO₃, PSi, D_dust, DOC, T, Fe, bgc) - iron_colloid_aggregation_1(sh, Fe, POC, DOC, T, bgc) - - iron_colloid_aggregation_2(sh, Fe, T, DOC, GOC, bgc) - enhanced_scavenging(Fe, DOC, T, bgc) - Bactfe) #eq60 +# this should be dispatched on an abstract type if we implement complex chemistry +@inline function aggregation_of_colloidal_iron(iron::SimpleIron, dom, bgc, z, DOC, POC, GOC, Fe, T, zₘₓₗ) + _, Φ₁, Φ₂, Φ₃ = aggregation(dom, bgc, z, DOC, POC, GOC, zₘₓₗ) + + Fe′ = free_iron(iron, Fe, DOC, T) + ligand_iron = Fe - Fe′ + colloidal_iron = 0.5 * ligand_iron + + CgFe1 = (Φ₁ + Φ₃) * colloidal_iron / (DOC + eps(0.0)) + CgFe2 = Φ₂ * colloidal_iron / (DOC + eps(0.0)) + + return CgFe1 + CgFe2, CgFe1, CgFe2 end diff --git a/src/Models/AdvectedPopulations/PISCES/iron_in_particles.jl b/src/Models/AdvectedPopulations/PISCES/iron_in_particles.jl index cc7279b84..b0c2081fb 100644 --- a/src/Models/AdvectedPopulations/PISCES/iron_in_particles.jl +++ b/src/Models/AdvectedPopulations/PISCES/iron_in_particles.jl @@ -1,124 +1,142 @@ -#This document contains functions for the following: - #Scav (eq50) - #Forcing equations for SFe and BFe. (eqs 48 and 49) -#We use the 2 compartment version of the model, with iron comparments for small and big particles. - -#Free form of iron is the only form susceptible to scavenging. We formulate the scavenging rate. -#Iron is scavenged by lithogenic and biogenic particles. Iron scavenged by POC and GOC routed to SFe and BFe, al other scavenging lost from the system. -@inline function Fe_scavenging_rate(POC, GOC, CaCO₃, PSi, D_dust, bgc) - λ_Feᵐⁱⁿ = bgc.min_scavenging_rate_of_iron - λ_Fe = bgc.slope_of_scavenging_rate_of_iron - λ_Feᵈᵘˢᵗ = bgc.scavenging_rate_of_iron_by_dust - w_dust = bgc.sinking_speed_of_dust - Dust = D_dust/(w_dust+ eps(0.0)) #eq84 - return λ_Feᵐⁱⁿ + λ_Fe*(POC + GOC + CaCO₃ + PSi) + λ_Feᵈᵘˢᵗ*Dust #eq50 -end +@inline function (poc::TwoCompartementParticulateOrganicMatter)(::Val{:SFe}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) -#Scavenging of free form of dissolved iron. -@inline Fe_scavenging(POC, GOC, CaCO₃, PSi, D_dust, DOC, T, Fe, bgc) = Fe_scavenging_rate(POC, GOC, CaCO₃, PSi, D_dust, bgc)*free_organic_iron(Fe, DOC, T) - -@inline function (bgc::PISCES)(::Val{:SFe}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - σᶻ = bgc.non_assimilated_fraction.Z - rᶻ = bgc.zooplankton_linear_mortality.Z - Kₘ = bgc.half_saturation_const_for_mortality - mᴾ, mᴰ = bgc.phytoplankton_mortality_rate - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - mᶻ = bgc.zooplankton_quadratic_mortality.Z - λ_Fe = bgc.slope_of_scavenging_rate_of_iron - κ_Bactˢᶠᵉ = bgc.coefficient_of_bacterial_uptake_of_iron_in_POC - wₚₒ = bgc.sinking_speed_of_POC - g_FF = bgc.flux_feeding_rate - b_Z, bₘ = bgc.temperature_sensitivity_term - μₘₐₓ⁰ = bgc.growth_rate_at_zero - θᶠᵉᶻ = bgc.FeC_ratio_of_zooplankton - - #Also required - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - Fe¹ = free_organic_iron(Fe, DOC, T) - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - bFe = Fe - - #Iron quotas - θᶠᵉᴾ = nutrient_quota(Pᶠᵉ, P) - θᶠᵉᴰ = nutrient_quota(Dᶠᵉ, D) - θᶠᵉᴾᴼᶜ = nutrient_quota(SFe, POC) + grazing_waste = specific_non_assimilated_iron_waste(bgc.microzooplankton, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) * Z + + # mortality terms + R_CaCO₃ = rain_ratio(bgc.calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + nanophytoplankton_linear_mortality, nanophytoplankton_quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + nanophytoplankton_mortality = (1 - 0.5 * R_CaCO₃) * (nanophytoplankton_linear_mortality + nanophytoplankton_quadratic_mortality) * PFe / (P + eps(0.0)) + + diatom_linear_mortality, = mortality(bgc.diatoms, bgc, z, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + diatom_mortality = 0.5 * diatom_linear_mortality * DFe / (D + eps(0.0)) + + microzooplankton_mortality = mortality(bgc.microzooplankton, bgc, Z, O₂, T) * bgc.microzooplankton.iron_ratio + + # degredation + λ = specific_degredation_rate(poc, bgc, O₂, T) + + large_particle_degredation = λ * BFe + degredation = λ * SFe + + # grazing + microzooplankton_grazing = particulate_grazing(bgc.microzooplankton, P, D, Z, POC, T) * Z + mesozooplankton_grazing = particulate_grazing(bgc.mesozooplankton, P, D, Z, POC, T) * M + + small_flux_feeding = specific_flux_feeding(bgc.mesozooplankton, POC, T, wPOC) * M + + grazing = (microzooplankton_grazing + mesozooplankton_grazing + small_flux_feeding) * SFe / (POC + eps(0.0)) + + # aggregation - #Grazing - grazingᶻ = grazing_Z(P, D, POC, T, bgc) - grazingᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑θᶠᵉⁱgᵢᶻ = θᶠᵉᴾ*grazingᶻ[2] + θᶠᵉᴰ*grazingᶻ[3] + θᶠᵉᴾᴼᶜ*grazingᶻ[4] #over P, D, POC - gₚₒ_FFᴹ = g_FF*(bₘ^T)*wₚₒ*POC + aggregation_to_large = aggregation(poc, bgc, z, POC, GOC, zₘₓₗ) + + total_aggregation = aggregation_to_large * SFe / (POC + eps(0.0)) + + # scavenging + λFe = iron_scavenging_rate(poc, POC, GOC, CaCO₃, PSi) - #Bacteria iron - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) - Bactfe = bacterial_uptake_Fe(μₘₐₓ⁰, z, Z, M, Fe, DOC, PO₄, NO₃, NH₄, bFe, T, zₘₐₓ, bgc) - - return (σᶻ*∑θᶠᵉⁱgᵢᶻ*Z - + θᶠᵉᶻ*(rᶻ*(b_Z^T)*(concentration_limitation(Z, Kₘ) + 3*oxygen_conditions(O₂, bgc))*Z + mᶻ*(b_Z^T)*(Z^2)) - + λₚₒ¹*BFe - + θᶠᵉᴾ*(1 - 0.5*rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc))*(mᴾ*concentration_limitation(P, Kₘ)*P + sh*wᴾ*P^2) - + θᶠᵉᴰ*0.5*mᴰ*concentration_limitation(D, Kₘ)*D + λ_Fe*POC*Fe¹ - + iron_colloid_aggregation_1(sh, Fe, POC, DOC, T, bgc) - λₚₒ¹*SFe - θᶠᵉᴾᴼᶜ*POC_aggregation(POC, GOC, sh, bgc) - - θᶠᵉᴾᴼᶜ*(grazingᴹ[4] + gₚₒ_FFᴹ)*M - + κ_Bactˢᶠᵉ*Bactfe - θᶠᵉᴾᴼᶜ*grazingᶻ[4]*Z) #eq48, partial derivative ommitted - - #Changes made from paper: - #3*oxygen_conditions added to zooplankton linear mortality terms. - #Z factor missing from final term. - -end - -@inline function (bgc::PISCES)(::Val{:BFe}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - σᴹ = bgc.non_assimilated_fraction.M - rᴹ = bgc.zooplankton_linear_mortality.M - mᴾ, mᴰ = bgc.phytoplankton_mortality_rate - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - λ_Fe = bgc.slope_of_scavenging_rate_of_iron - g_FF = bgc.flux_feeding_rate - wₚₒ = bgc.sinking_speed_of_POC - bₘ = bgc.temperature_sensitivity_term.M - κ_Bactᴮᶠᵉ = bgc.coefficient_of_bacterial_uptake_of_iron_in_GOC - θᶠᵉᶻ = bgc.FeC_ratio_of_zooplankton - μₘₐₓ⁰ = bgc.growth_rate_at_zero - - #Other required terms - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - Fe¹ = free_organic_iron(Fe, DOC, T) - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - bFe = Fe + Fe′ = free_iron(bgc.iron, Fe, DOC, T) + + scavenging = λFe * POC * Fe′ + + # bacterial uptake of dissolved iron + κ = poc.small_fraction_of_bacterially_consumed_iron + + BactFe = bacterial_iron_uptake(bgc.dissolved_organic_matter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + bacterial_assimilation = κ * BactFe + + # colloidal iron aggregation + _, colloidal_aggregation = aggregation_of_colloidal_iron(bgc.iron, bgc.dissolved_organic_matter, bgc, z, DOC, POC, GOC, Fe, T, zₘₓₗ) + + return (grazing_waste + + nanophytoplankton_mortality + diatom_mortality + microzooplankton_mortality + + large_particle_degredation + scavenging + bacterial_assimilation + colloidal_aggregation + - total_aggregation + - grazing - degredation) +end + + +@inline function (poc::TwoCompartementParticulateOrganicMatter)(::Val{:BFe}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + grazing_waste = specific_non_assimilated_iron_waste(bgc.mesozooplankton, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) * M + + # mortality terms + R_CaCO₃ = rain_ratio(bgc.calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + nanophytoplankton_linear_mortality, nanophytoplankton_quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + nanophytoplankton_mortality = 0.5 * R_CaCO₃ * (nanophytoplankton_linear_mortality + nanophytoplankton_quadratic_mortality) * PFe / (P + eps(0.0)) + + diatom_linear_mortality, diatom_quadratic_mortality = mortality(bgc.diatoms, bgc, z, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + diatom_mortality = (0.5 * diatom_linear_mortality + diatom_quadratic_mortality) * DFe / (D + eps(0.0)) + + mesozooplankton_mortality = linear_mortality(bgc.mesozooplankton, bgc, M, O₂, T) * bgc.mesozooplankton.iron_ratio + + # degredation + λ = specific_degredation_rate(poc, bgc, O₂, T) + + degredation = λ * BFe + + # grazing + grazing = specific_flux_feeding(bgc.mesozooplankton, GOC, T,wGOC) * M * BFe / (GOC + eps(0.0)) + + # aggregation + small_particle_aggregation = aggregation(poc, bgc, z, POC, GOC, zₘₓₗ) + + total_aggregation = small_particle_aggregation * SFe / (POC + eps(0.0)) + + # fecal pelet prodiction + fecal_pelet_production = upper_trophic_fecal_product(bgc.mesozooplankton, M, T) * bgc.mesozooplankton.iron_ratio + + # scavenging + λFe = iron_scavenging_rate(poc, POC, GOC, CaCO₃, PSi) - #Iron quotas - θᶠᵉᴾ = nutrient_quota(Pᶠᵉ, P) - θᶠᵉᴰ = nutrient_quota(Dᶠᵉ, D) - θᶠᵉᴾᴼᶜ = nutrient_quota(SFe, POC) - θᶠᵉᴳᴼᶜ = nutrient_quota(BFe, GOC) - - #Grazing - grazingᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑θᶠᵉⁱgᵢᴹ = θᶠᵉᴾ*grazingᴹ[2] + θᶠᵉᴰ*grazingᴹ[3] + θᶠᵉᴾᴼᶜ*grazingᴹ[4] + θᶠᵉᶻ*grazingᴹ[5] #graze on P, D, POC, Z - gₚₒ_FFᴹ = g_FF*bₘ^T*wₚₒ*POC - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #41a - w_GOC = sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) - g_GOC_FFᴹ = g_FF*bₘ^T*w_GOC*GOC - - return (σᴹ*(∑θᶠᵉⁱgᵢᴹ + θᶠᵉᴾᴼᶜ*gₚₒ_FFᴹ + θᶠᵉᴳᴼᶜ*g_GOC_FFᴹ)*M - + θᶠᵉᶻ*(rᴹ*(bₘ^T)*(concentration_limitation(M, Kₘ) + 3*oxygen_conditions(O₂, bgc))*M + production_of_fecal_pellets(M, T, bgc)) - + θᶠᵉᴾ*0.5*rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc)*(mᴾ*concentration_limitation(P, Kₘ)*P + sh*wᴾ*P^2) - + θᶠᵉᴰ*(0.5*mᴰ*concentration_limitation(D, Kₘ)*D + sh*wᴰ*D^2) - + κ_Bactᴮᶠᵉ*bacterial_uptake_Fe(μₘₐₓ⁰, z, Z, M, Fe, DOC, PO₄, NO₃, NH₄, bFe, T, zₘₐₓ, bgc) - + λ_Fe*GOC*Fe¹ + θᶠᵉᴾᴼᶜ*POC_aggregation(POC, GOC, sh, bgc) + iron_colloid_aggregation_2(sh, Fe, T, DOC, GOC, bgc) - - θᶠᵉᴳᴼᶜ* g_GOC_FFᴹ*M - λₚₒ¹*BFe) #eq49, partial derivative omitted - - #Changes made from paper: - #3*oxygen_conditions added to zooplankton linear mortality terms. + Fe′ = free_iron(bgc.iron, Fe, DOC, T) + + scavenging = λFe * GOC * Fe′ + + # bacterial uptake of dissolved iron + κ = poc.large_fraction_of_bacterially_consumed_iron + + BactFe = bacterial_iron_uptake(bgc.dissolved_organic_matter, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, T, zₘₓₗ, zₑᵤ) + + bacterial_assimilation = κ * BactFe + + # colloidal iron aggregation + _, _, colloidal_aggregation = aggregation_of_colloidal_iron(bgc.iron, bgc.dissolved_organic_matter, bgc, z, DOC, POC, GOC, Fe, T, zₘₓₗ) + + return (grazing_waste + + nanophytoplankton_mortality + diatom_mortality + mesozooplankton_mortality + + total_aggregation + fecal_pelet_production + scavenging + bacterial_assimilation + colloidal_aggregation + - grazing - degredation) +end + +@inline function iron_scavenging_rate(pom, POC, GOC, CaCO₃, PSi) + λ₀ = pom.minimum_iron_scavenging_rate + λ₁ = pom.load_specific_iron_scavenging_rate + + return λ₀ + λ₁ * (POC + GOC + CaCO₃ + PSi) end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_vertical_diffusivity.jl b/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl similarity index 52% rename from src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_vertical_diffusivity.jl rename to src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl index 32a220d58..13d893126 100644 --- a/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_vertical_diffusivity.jl +++ b/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl @@ -1,45 +1,91 @@ +using Oceananigans.Architectures: architecture +using Oceananigans.AbstractOperations: AbstractOperation +using Oceananigans.BoundaryConditions: fill_halo_regions! +using Oceananigans.Utils: launch! + +##### +##### generic integration +##### + +function compute_mixed_layer_mean!(Cₘₓₗ, mixed_layer_depth, C, grid) + arch = architecture(grid) + + launch!(arch, grid, :xy, _compute_mixed_layer_mean!, Cₘₓₗ, mixed_layer_depth, C, grid) + + fill_halo_regions!(Cₘₓₗ) + + return nothing +end + +compute_mixed_layer_mean!(Cₘₓₗ::AbstractOperation, mixed_layer_depth, C, grid) = nothing +compute_mixed_layer_mean!(Cₘₓₗ::ConstantField, mixed_layer_depth, C, grid) = nothing +compute_mixed_layer_mean!(Cₘₓₗ::ZeroField, mixed_layer_depth, C, grid) = nothing +compute_mixed_layer_mean!(Cₘₓₗ::Nothing, mixed_layer_depth, C, grid) = nothing + +@kernel function _compute_mixed_layer_mean!(Cₘₓₗ, mixed_layer_depth, C, grid) + i, j = @index(Global, NTuple) + + zₘₓₗ = @inbounds mixed_layer_depth[i, j, 1] + + @inbounds Cₘₓₗ[i, j, 1] = 0 + + integration_depth = 0 + + for k in grid.Nz:-1:1 + zₖ = znode(i, j, k, grid, Center(), Center(), Face()) + zₖ₊₁ = znode(i, j, k + 1, grid, Center(), Center(), Face()) + + Δzₖ = zₖ₊₁ - zₖ + Δzₖ₊₁ = ifelse(zₖ₊₁ > zₘₓₗ, zₖ₊₁ - zₘₓₗ, 0) + + Δz = ifelse(zₖ >= zₘₓₗ, Δzₖ, Δzₖ₊₁) + + Cₘₓₗ[i, j, 1] += C[i, j, k] * Δz + + integration_depth += Δz + end + + Cₘₓₗ[i, j, 1] /= integration_depth +end + +##### +##### Mean mixed layer diffusivity +##### -compute_mean_mixed_layer_vertical_diffusivity!(κ::ConstantField, mixed_layer_depth, model) = nothing compute_mean_mixed_layer_vertical_diffusivity!(κ, mixed_layer_depth, model) = - compute_mean_mixed_layer_vertical_diffusivity!(model.closure, κ, mixed_layer_depth, model.diffusivity_fields) + compute_mean_mixed_layer_vertical_diffusivity!(model.closure, κ, mixed_layer_depth, model.diffusivity_fields, model.grid) + +# need these to catch when model doesn't have closure (i.e. box model) +compute_mean_mixed_layer_vertical_diffusivity!(κ::ConstantField, mixed_layer_depth, model) = nothing +compute_mean_mixed_layer_vertical_diffusivity!(κ::ZeroField, mixed_layer_depth, model) = nothing +compute_mean_mixed_layer_vertical_diffusivity!(κ::Nothing, mixed_layer_depth, model) = nothing # if no closure is defined we just assume its pre-set compute_mean_mixed_layer_vertical_diffusivity!(closure::Nothing, mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, - diffusivity_fields) = nothing + diffusivity_fields, grid) = nothing -function compute_mean_mixed_layer_vertical_diffusivity!(closure, mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, diffusivity_fields) +function compute_mean_mixed_layer_vertical_diffusivity!(closure, mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, diffusivity_fields, grid) # this is going to get messy κ = phytoplankton_diffusivity(closure, diffusivity_fields) - launch!(arch, grid, :xy, _compute_mean_mixed_layer_vertical_diffusivity!, mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, κ) - - fill_halo_regions!(mean_mixed_layer_vertical_diffusivity) + compute_mixed_layer_mean!(mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, κ, grid) return nothing end -@kernel function _compute_mean_mixed_layer_vertical_diffusivity!(κₘₓₗ, mixed_layer_depth, κ) - i, j = @index(Global, NTuple) - - zₘₓₗ = @inbounds mixed_layer_depth[i, j] - - @inbounds κₘₓₗ[i, j] = 0 - - integration_depth = 0 +##### +##### Mean mixed layer light +##### - for k in grid.Nz:-1:1 - if znode(i, j, k, grid, Center(), Center(), Center()) > zₘₓₗ - Δz = zspacing(i, j, k, grid, Center(), Center(), Center()) - κₘₓₗ[i, j] += κ[i, j, k] * Δz # I think sometimes vertical diffusivity is face located? - integration_depth += Δz - end - end +compute_mean_mixed_layer_light!(mean_PAR, mixed_layer_depth, PAR, model) = + compute_mixed_layer_mean!(mean_PAR, mixed_layer_depth, PAR, model.grid) - κₘₓₗ[i, j] /= integration_depth -end +##### +##### Informaiton about diffusivity fields +##### # this does not belong here - lets add them when a particular closure is needed using Oceananigans.TurbulenceClosures: ScalarDiffusivity, ScalarBiharmonicDiffusivity, VerticalFormulation, ThreeDimensionalFormulation, formulation diff --git a/src/Models/AdvectedPopulations/PISCES/nitrate_ammonia.jl b/src/Models/AdvectedPopulations/PISCES/nitrate_ammonia.jl new file mode 100644 index 000000000..87492acc3 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/nitrate_ammonia.jl @@ -0,0 +1,109 @@ +""" + NitrateAmmonia + +A parameterisation for the evolution of nitrate (`NO₃`) and ammonia (`NH₄`) +where ammonia can be `nitrif`ied into nitrate, nitrate and ammonia are supplied +by the bacterial degredation of dissolved organic matter, and consumed by +phytoplankton. Additionally waste produces ammonia through various means. + +""" +@kwdef struct NitrateAmmonia{FT} + maximum_nitrifcation_rate :: FT = 0.05 / day # 1 / s + maximum_fixation_rate :: FT = 0.013 / day # mmol N / m³ (maybe shouldn't be a rate) + iron_half_saturation_for_fixation :: FT = 0.1 # μmol Fe / m³ + phosphate_half_saturation_for_fixation :: FT = 0.8 # mmol P / m³ + light_saturation_for_fixation :: FT = 50.0 # W / m² +end + +@inline function (nitrogen::NitrateAmmonia)(::Val{:NO₃}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + θ = bgc.nitrogen_redfield_ratio + + nitrif = nitrification(nitrogen, bgc, NH₄, O₂, mixed_layer_PAR) * θ + + remin = oxic_remineralisation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) * θ + + nanophytoplankton_consumption = nitrate_uptake(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + diatom_consumption = nitrate_uptake(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + consumption = (nanophytoplankton_consumption + diatom_consumption) * θ + + return nitrif + remin - consumption # an extra term is present in Aumount 2015 but I suspect it is a typo + # to conserve nitrogen I've dropped some ratios for denit etc, and now have bacterial_degregation go to denit in NO3 and remineralisation in NH4_half_saturation_const_for_DOC_remin + # need to check... +end + +@inline function (nitrogen::NitrateAmmonia)(::Val{:NH₄}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + θ = bgc.nitrogen_redfield_ratio + + nitrif = nitrification(nitrogen, bgc, NH₄, O₂, mixed_layer_PAR) * θ + + respiration_product = inorganic_upper_trophic_respiration_product(bgc.mesozooplankton, M, T) * θ + + microzooplankton_grazing_waste = specific_inorganic_grazing_waste(bgc.microzooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * Z + mesozooplankton_grazing_waste = specific_inorganic_grazing_waste(bgc.mesozooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * M + + grazing_waste = (microzooplankton_grazing_waste + mesozooplankton_grazing_waste) * θ + + denit = denitrifcation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) * θ + + nanophytoplankton_consumption = ammonia_uptake(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + diatom_consumption = ammonia_uptake(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + consumption = (nanophytoplankton_consumption + diatom_consumption) * θ + + fixation = nitrogen_fixation(nitrogen, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, PAR) + + # again an extra term is present in Aumount 2015 but I suspect it is a typo + return fixation + respiration_product + grazing_waste + denit - consumption - nitrif +end + +@inline function nitrification(nitrogen, bgc, NH₄, O₂, PAR) + λ = nitrogen.maximum_nitrifcation_rate + + ΔO₂ = anoxia_factor(bgc, O₂) + + return λ * NH₄ / (1 + PAR) * (1 - ΔO₂) +end + +@inline function nitrogen_fixation(nitrogen, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, PAR) + Nₘ = nitrogen.maximum_fixation_rate + K_Fe = nitrogen.iron_half_saturation_for_fixation + K_PO₄ = nitrogen.phosphate_half_saturation_for_fixation + E = nitrogen.light_saturation_for_fixation + + phyto = bgc.nanophytoplankton + + _, _, _, LN, _, _ = phyto.nutrient_limitation(bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + + fixation_limit = ifelse(LN >= 0.8, 0.01, 1 - LN) + + μ = base_production_rate(bgc.nanophytoplankton.growth_rate, T) + + growth_requirment = max(0, μ - 2.15) + + nutrient_limitation = min(Fe / (Fe + K_Fe), PO₄ / (PO₄ + K_PO₄)) + + light_limitation = 1 - exp(-PAR / E) + + return Nₘ * growth_requirment * fixation_limit * nutrient_limitation * light_limitation +end diff --git a/src/Models/AdvectedPopulations/PISCES/nitrates_ammonium.jl b/src/Models/AdvectedPopulations/PISCES/nitrates_ammonium.jl deleted file mode 100644 index c85463060..000000000 --- a/src/Models/AdvectedPopulations/PISCES/nitrates_ammonium.jl +++ /dev/null @@ -1,170 +0,0 @@ - -#We model the following nutrients in PISCES, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, O₂. -#What is not assimilated from grazing and not routed to particles, is shared between dissolved organic and inorganic matter. -#The first 5 terms of the NH₄, PO₄, DIC equations are the same up to a redfield ratio. These terms describe how carbon is routed to inorganic matter. Contribution to each compartment then set by redfield ratio. - -#This document contains functions for: - #uptake_rate_nitrate, uptake_rate_ammonium (eq8) - #oxygen_condition (eq57) - #Nitrif (eq56) - #N_fix (eq58) - #Forcing for NO₃ and NH₄ (eqs54, 55) - -#Processes in the nitrogen cycle are represented through forcing equations for NO₃ and NH₄. -#Atmospheric nitrogen fixed as NH₄. Nitrification converts ammonium to nitrates. -#Remin and denit terms are added from the remineralisation of DOM. In anoxic conditions, denitrification processes can occur where nitrates can oxidise ammonia, this is seen in 4th term of eq54. - -#Uptake rate of nitrate by phytoplankton -@inline function uptake_rate_nitrate_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - αᴾ = bgc.initial_slope_of_PI_curve.P - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) - Lₙₒ₃ᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[4] - Lₙₕ₄ᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[3] - return μᴾ * concentration_limitation(Lₙₒ₃ᴾ, Lₙₕ₄ᴾ) #eq8 -end - -#Uptake rate of ammonium by phytoplankton -@inline function uptake_rate_ammonium_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - αᴾ = bgc.initial_slope_of_PI_curve.P - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) - Lₙₒ₃ᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[4] - Lₙₕ₄ᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[3] - return μᴾ * concentration_limitation(Lₙₕ₄ᴾ, Lₙₒ₃ᴾ) #eq8 -end - -#Uptake rate of nitrate by diatoms -@inline function uptake_rate_nitrate_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - αᴰ = bgc.initial_slope_of_PI_curve.D - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - Lₙₒ₃ᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[4] - Lₙₕ₄ᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[3] - return μᴰ * concentration_limitation(Lₙₒ₃ᴰ, Lₙₕ₄ᴰ) #eq8 -end - -#Uptake rate of ammonium by diatoms -@inline function uptake_rate_ammonium_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - αᴰ = bgc.initial_slope_of_PI_curve.D - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - Lₙₒ₃ᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[4] - Lₙₕ₄ᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[3] - return μᴰ * concentration_limitation(Lₙₕ₄ᴰ, Lₙₒ₃ᴰ) #eq8 -end - -#Represents the oxygen conditions of the water. Is 0 for oxic waters, 1 for anoxic waters. -@inline function oxygen_conditions(O₂, bgc) - O₂ᵐⁱⁿ¹ = bgc.half_sat_const_for_denitrification1 - O₂ᵐⁱⁿ² = bgc.half_sat_const_for_denitrification2 - return min(1, max(0, 0.4*(O₂ᵐⁱⁿ¹ - O₂)/(O₂ᵐⁱⁿ²+O₂+eps(0.0)))) #eq57 -end - -#Nitrification converts ammonium to nitrates, dimensions molN/L -@inline nitrification(NH₄, O₂, λₙₕ₄, PAR, bgc) = λₙₕ₄*NH₄*(1-oxygen_conditions(O₂, bgc))/(1+PAR) #eq56a - -#Forcing for NO₃ -@inline function (bgc::PISCES)(::Val{:NO₃}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - λₙₕ₄ = bgc.max_nitrification_rate - θᴺᶜ = bgc.NC_redfield_ratio - Rₙₕ₄ = bgc.NC_stoichiometric_ratio_of_ANOTHERPLACEHOLDER - Rₙₒ₃ = bgc.NC_stoichiometric_ratio_of_dentitrification - - #Uptake of nitrate by phytoplankton and diatoms - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - - μₙₒ₃ᴾ = uptake_rate_nitrate_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₒ₃ᴰ = uptake_rate_nitrate_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - - #Bacteria - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #35a - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - bFe = Fe - - return (θᴺᶜ*(- μₙₒ₃ᴾ*P - μₙₒ₃ᴰ*D - - Rₙₒ₃*denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc)) - + nitrification(NH₄, O₂, λₙₕ₄, PAR, bgc) - Rₙₕ₄*λₙₕ₄*oxygen_conditions(O₂, bgc)*NH₄) - - #Changes made: - #In paper some dimensions of terms did not agree. Relevant terms have been multiplied by a redfield ratio to return in molN/L. -end - -#Nitrogen fixation fixes atmospheric nitrogen into inorganic form, NH₄ - -@inline function N_fixation(bFe, PO₄, T, P, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, PAR, bgc) # Returns in μmolN/L - N_fixᵐ = bgc.max_rate_of_nitrogen_fixation - K_Feᴰᶻ = bgc.Fe_half_saturation_constant_of_nitrogen_fixation - Kₚₒ₄ᴾᵐⁱⁿ = bgc.min_half_saturation_const_for_phosphate.P - E_fix = bgc.photosynthetic_parameter_of_nitrogen_fixation - μ⁰ₘₐₓ = bgc.growth_rate_at_zero - bₚ = bgc.temperature_sensitivity_of_growth - μₚ = μ⁰ₘₐₓ*(bₚ^T) - Lₙᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[5] - - Lₙᴰᶻ = ifelse(Lₙᴾ>=0.08, 0.01, 1 - Lₙᴾ) #eq 58 - - return (N_fixᵐ*max(0,μₚ - 2.15)*Lₙᴰᶻ*min(concentration_limitation(bFe, K_Feᴰᶻ), concentration_limitation(PO₄, Kₚₒ₄ᴾᵐⁱⁿ))*(1 - exp((-PAR/E_fix)))) #eq 58b -end - -#Forcing for NH₄, redfield conversion to model in molN/L. -@inline function (bgc::PISCES)(::Val{:NH₄}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - γᶻ = bgc.excretion_as_DOM.Z - σᶻ = bgc.non_assimilated_fraction.Z - γᴹ = bgc.excretion_as_DOM.M - σᴹ = bgc.non_assimilated_fraction.M - λₙₕ₄ = bgc.max_nitrification_rate - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - eₘₐₓᶻ = bgc.max_growth_efficiency_of_zooplankton.Z - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - θᴺᶜ = bgc.NC_redfield_ratio - - #Uptake rates of ammonium - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - - μₙₕ₄ᴾ = uptake_rate_ammonium_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₕ₄ᴰ = uptake_rate_ammonium_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - #Bacteria - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #35a - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - bFe = Fe - - return (θᴺᶜ*(γᶻ*(1-eᶻ-σᶻ)*∑gᶻ*Z + γᴹ*(1-eᴹ-σᴹ)*(∑gᴹ + ∑g_FFᴹ)*M + γᴹ*upper_respiration(M, T, bgc) - + oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) + denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) - - μₙₕ₄ᴾ*P - μₙₕ₄ᴰ*D) + N_fixation(bFe, PO₄, T, P, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, PAR, bgc) - - nitrification(NH₄, O₂, λₙₕ₄, PAR, bgc) - λₙₕ₄*oxygen_conditions(O₂, bgc)*NH₄) #eq55 - - #Changes made: - #In paper some dimensions of terms did not agree. Relevant terms have been multiplied by a redfield ratio to return in molN/L. -end diff --git a/src/Models/AdvectedPopulations/PISCES/nutrient_limitation.jl b/src/Models/AdvectedPopulations/PISCES/nutrient_limitation.jl new file mode 100644 index 000000000..1de3840e6 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/nutrient_limitation.jl @@ -0,0 +1,84 @@ +""" + NitrogenIronPhosphateSilicateLimitation + +Holds the parameters for growth limitation by nitrogen (NO₃ and NH₄), +iron (Fe), phosphate PO₄, and (optionally) silicate (Si) availability. + +Silicate limitation may be turned off (e.g. for nanophytoplankton) by +setting `silicate_limited=false`. +""" +@kwdef struct NitrogenIronPhosphateSilicateLimitation{FT, BT} + minimum_ammonium_half_saturation :: FT # mmol N / m³ + minimum_nitrate_half_saturation :: FT # mmol N / m³ + minimum_phosphate_half_saturation :: FT # mmol P / m³ + threshold_for_size_dependency :: FT = 1.0 # mmol C / m³ + size_ratio :: FT = 3.0 # + optimal_iron_quota :: FT = 0.007 # μmol Fe / mmol C + silicate_limited :: BT # Bool + minimum_silicate_half_saturation :: FT = 1.0 # mmol Si / m³ + silicate_half_saturation_parameter :: FT = 16.6 # mmol Si / m³ + half_saturation_for_iron_uptake :: FT # μmol Fe / m³ +end + +@inline function size_factor(L, I) + Iₘ = L.threshold_for_size_dependency + S = L.size_ratio + + I₁ = min(I, Iₘ) + I₂ = max(0, I - Iₘ) + + return (I₁ + S * I₂) / (I₁ + I₂ + eps(0.0)) +end + +@inline function (L::NitrogenIronPhosphateSilicateLimitation)(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + kₙₒ = L.minimum_nitrate_half_saturation + kₙₕ = L.minimum_ammonium_half_saturation + kₚ = L.minimum_phosphate_half_saturation + kₛᵢ = L.minimum_silicate_half_saturation + pk = L.silicate_half_saturation_parameter + + θₒ = L.optimal_iron_quota + + # quotas + θFe = ifelse(I == 0, 0, IFe / (I + eps(0.0))) + θChl = ifelse(I == 0, 0, IChl / (12 * I + eps(0.0))) + + K̄ = size_factor(L, I) + + Kₙₒ = kₙₒ * K̄ + Kₙₕ = kₙₕ * K̄ + Kₚ = kₚ * K̄ + + # nitrogen limitation + LNO₃ = nitrogen_limitation(NO₃, NH₄, Kₙₒ, Kₙₕ) + LNH₄ = nitrogen_limitation(NH₄, NO₃, Kₙₕ, Kₙₒ) + + LN = LNO₃ + LNH₄ + + # phosphate limitation + LPO₄ = PO₄ / (PO₄ + Kₚ + eps(0.0)) + + # iron limitation + # Flynn and Hipkin (1999) - photosphotosyntheis, respiration (?), nitrate reduction + θₘ = 10^3 * (0.0016 / 55.85 * 12 * θChl + 1.5 * 1.21e-5 * 14 / (55.85 * 7.625) * LN + 1.15e-4 * 14 / (55.85 * 7.625) * LNO₃) + + LFe = min(1, max(0, (θFe - θₘ) / θₒ)) + + # silicate limitation + KSi = kₛᵢ + 7 * Si′^2 / (pk^2 + Si′^2) + LSi = Si / (Si + KSi) + LSi = ifelse(L.silicate_limited, LSi, Inf) + + # don't always need the other arguments but they can be got like L, = ... or _, LFe = .. + return min(LN, LPO₄, LFe, LSi), LFe, LPO₄, LN, LNO₃, LNH₄ +end + +@inline nitrogen_limitation(N₁, N₂, K₁, K₂) = (K₂ * N₁) / (K₁ * K₂ + K₁ * N₂ + K₂ * N₁ + eps(0.0)) + +@inline function iron_uptake_limitation(L, I, Fe) + k = L.half_saturation_for_iron_uptake + + K = k * size_factor(L, I) + + return Fe / (Fe + K + eps(0.0)) +end diff --git a/src/Models/AdvectedPopulations/PISCES/oxygen.jl b/src/Models/AdvectedPopulations/PISCES/oxygen.jl index 884f41103..9cb7433b3 100644 --- a/src/Models/AdvectedPopulations/PISCES/oxygen.jl +++ b/src/Models/AdvectedPopulations/PISCES/oxygen.jl @@ -1,49 +1,55 @@ -#This document contains functions for: - #O₂ forcing (eq83) - -@inline function (bgc::PISCES)(::Val{:O₂}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - - O₂ᵘᵗ = bgc.OC_for_ammonium_based_processes - O₂ⁿⁱᵗ = bgc.OC_ratio_of_nitrification - γᶻ = bgc.excretion_as_DOM.Z - γᴹ = bgc.excretion_as_DOM.M - σᶻ = bgc.non_assimilated_fraction.Z - σᴹ = bgc.non_assimilated_fraction.M - λₙₕ₄ = bgc.max_nitrification_rate - bFe = Fe - θᴺᶜ = bgc.NC_redfield_ratio - #L_day - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - eₘₐₓᶻ = bgc.max_growth_efficiency_of_zooplankton.Z - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] - #g_Z not called - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #35a - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - #Uptake rates of nitrogen and ammonium - μₙₒ₃ᴾ = uptake_rate_nitrate_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₒ₃ᴰ = uptake_rate_nitrate_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) - μₙₕ₄ᴾ = uptake_rate_ammonium_P(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴾ, t_darkᴾ, Si̅, bgc) - μₙₕ₄ᴰ = uptake_rate_ammonium_D(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, T, zₘₓₗ, zₑᵤ, κ, L_day, PARᴰ, t_darkᴰ, Si̅, bgc) +""" + Oxygen + +Parameterisation for oxygen which is supplied by phyotsynthesis and denitrifcation, +and removed by various respiration terms and nitrifcation. +""" +@kwdef struct Oxygen{FT} + ratio_for_respiration :: FT = 133/122 # mol O₂ / mol C + ratio_for_nitrifcation :: FT = 32/122 # mol O₂ / mol C +end + +@inline function (oxy::Oxygen)(::Val{:O₂}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + θ_resp = oxy.ratio_for_respiration + θ_nitrif = oxy.ratio_for_nitrifcation + + microzooplankton_respiration = specific_inorganic_grazing_waste(bgc.microzooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * Z + mesozooplankton_respiration = specific_inorganic_grazing_waste(bgc.mesozooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * M + + zooplankton_respiration = θ_resp * (microzooplankton_respiration + mesozooplankton_respiration) + + upper_trophic_respiration = θ_resp * inorganic_upper_trophic_respiration_product(bgc.mesozooplankton, M, T) + + remin = (θ_resp + θ_nitrif) * oxic_remineralisation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + denit = θ_resp * denitrifcation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + + nitrif = θ_nitrif * nitrification(bgc.nitrogen, bgc, NH₄, O₂, mixed_layer_PAR) - return (O₂ᵘᵗ*(μₙₕ₄ᴾ*P + μₙₕ₄ᴰ*D) + (O₂ᵘᵗ + O₂ⁿⁱᵗ)*(μₙₒ₃ᴾ*P + μₙₒ₃ᴰ*D) + O₂ⁿⁱᵗ*(1/θᴺᶜ)*N_fixation(bFe, PO₄, T, P, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, PAR, bgc) - - O₂ᵘᵗ*γᶻ*(1 - eᶻ - σᶻ)*∑gᶻ*Z - O₂ᵘᵗ*γᴹ*(1 - eᴹ - σᴹ)*(∑gᴹ + ∑g_FFᴹ)*M - O₂ᵘᵗ*γᴹ*upper_respiration(M, T, bgc) - - O₂ᵘᵗ*oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) - O₂ⁿⁱᵗ*(1/θᴺᶜ)*nitrification(NH₄, O₂, λₙₕ₄, PAR, bgc)) -end \ No newline at end of file + nanophytoplankton_nitrate_consumption = nitrate_uptake(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + diatom_nitrate_consumption = nitrate_uptake(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + nitrate_consumption = (θ_resp + θ_nitrif) * (nanophytoplankton_nitrate_consumption + diatom_nitrate_consumption) + + nanophytoplankton_ammonia_consumption = ammonia_uptake(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + diatom_ammonia_consumption = ammonia_uptake(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + ammonia_consumption = θ_resp * (nanophytoplankton_ammonia_consumption + diatom_ammonia_consumption) + + photosynthesis = nitrate_consumption + ammonia_consumption + + fixation = θ_nitrif * nitrogen_fixation(bgc.nitrogen, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, PAR) + + return photosynthesis + fixation - zooplankton_respiration - upper_trophic_respiration - nitrif - remin - denit +end diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_carbon.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_carbon.jl new file mode 100644 index 000000000..2e3ad8b74 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_carbon.jl @@ -0,0 +1,104 @@ +@inline function (poc::TwoCompartementParticulateOrganicMatter)(::Val{:POC}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + grazing_waste = specific_non_assimilated_waste(bgc.microzooplankton, P, D, Z, POC, GOC, T, wPOC, wGOC) * Z + + # mortality terms + R_CaCO₃ = rain_ratio(bgc.calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + nanophytoplankton_linear_mortality, nanophytoplankton_quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + nanophytoplankton_mortality = (1 - 0.5 * R_CaCO₃) * (nanophytoplankton_linear_mortality + nanophytoplankton_quadratic_mortality) + + diatom_linear_mortality, = mortality(bgc.diatoms, bgc, z, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + diatom_mortality = 0.5 * diatom_linear_mortality + + microzooplankton_mortality = mortality(bgc.microzooplankton, bgc, Z, O₂, T) + + # degredation + λ = specific_degredation_rate(poc, bgc, O₂, T) + + large_particle_degredation = λ * GOC + degredation = λ * POC + + # grazing + microzooplankton_grazing = particulate_grazing(bgc.microzooplankton, P, D, Z, POC, T) * Z + mesozooplankton_grazing = particulate_grazing(bgc.mesozooplankton, P, D, Z, POC, T) * M + + small_flux_feeding = specific_flux_feeding(bgc.mesozooplankton, POC, T, wPOC) * M + + grazing = microzooplankton_grazing + mesozooplankton_grazing + small_flux_feeding + + # aggregation + _, Φ₁, _, Φ₃ = aggregation(bgc.dissolved_organic_matter, bgc, z, DOC, POC, GOC, zₘₓₗ) + dissolved_aggregation = Φ₁ + Φ₃ + + aggregation_to_large = aggregation(poc, bgc, z, POC, GOC, zₘₓₗ) + + total_aggregation = dissolved_aggregation - aggregation_to_large + + return (grazing_waste + + nanophytoplankton_mortality + diatom_mortality + microzooplankton_mortality + + large_particle_degredation + total_aggregation + - grazing - degredation) +end + +@inline function (poc::TwoCompartementParticulateOrganicMatter)(::Val{:GOC}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + grazing_waste = specific_non_assimilated_waste(bgc.mesozooplankton, P, D, Z, POC, GOC, T, wPOC, wGOC) * M + + # mortality terms + R_CaCO₃ = rain_ratio(bgc.calcite, bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, PAR) + + nanophytoplankton_linear_mortality, nanophytoplankton_quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + nanophytoplankton_mortality = 0.5 * R_CaCO₃ * (nanophytoplankton_linear_mortality + nanophytoplankton_quadratic_mortality) + + diatom_linear_mortality, diatom_quadratic_mortality = mortality(bgc.diatoms, bgc, z, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + diatom_mortality = 0.5 * diatom_linear_mortality + diatom_quadratic_mortality + + mesozooplankton_mortality = linear_mortality(bgc.mesozooplankton, bgc, M, O₂, T) + + # degredation + λ = specific_degredation_rate(poc, bgc, O₂, T) + + degredation = λ * GOC + + # grazing + grazing = specific_flux_feeding(bgc.mesozooplankton, GOC, T, wGOC) * M + + # aggregation + _, _, dissolved_aggregation = aggregation(bgc.dissolved_organic_matter, bgc, z, DOC, POC, GOC, zₘₓₗ) + + small_particle_aggregation = aggregation(poc, bgc, z, POC, GOC, zₘₓₗ) + + total_aggregation = dissolved_aggregation + small_particle_aggregation + + # fecal pelet prodiction + fecal_pelet_production = upper_trophic_fecal_product(bgc.mesozooplankton, M, T) + + return (grazing_waste + + nanophytoplankton_mortality + diatom_mortality + mesozooplankton_mortality + + total_aggregation + fecal_pelet_production + - grazing + - degredation) +end diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter.jl index ca81062f1..0dd3f9ac7 100644 --- a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter.jl +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter.jl @@ -1,92 +1,49 @@ - -#Particles of carbon are significant as sink and export carbon to the deep ocean. -#GOC are faster sinking particles with variable sinking speed. - -# This documeent contains functions for: - # Φ (eq39) - # POC, GOC (eqs37, 40) - -#Aggregation of POC due to turbulence and differential settling -@inline function POC_aggregation(POC, GOC, sh, bgc) - a₆ = bgc.aggregation_rate_of_POC_to_GOC_6 - a₇ = bgc.aggregation_rate_of_POC_to_GOC_7 - a₈ = bgc.aggregation_rate_of_POC_to_GOC_8 - a₉ = bgc.aggregation_rate_of_POC_to_GOC_9 - - return sh*a₆*POC^2 + sh*a₇*POC*GOC + a₈*POC*GOC + a₉*POC^2 #eq39 +""" + TwoCompartementParticulateOrganicMatter + +A quota parameterisation for particulate organic matter with two size classes, +each with carbon and iron compartements, and a silicate compartement for the +large size class. + +Confusingly we decided to name these compartmenets `POC` and `GOC` for the small +and large carbon classes, `SFe` and `BFe` for the small and ̶l̶a̶r̶g̶e̶ big iron +compartements, and `PSi` for the ̶l̶a̶r̶g̶e̶ particulate silicon (*not* the +phytoplankton silicon). +""" +@kwdef struct TwoCompartementParticulateOrganicMatter{FT, AP} + temperature_sensetivity :: FT = 1.066 # + base_breakdown_rate :: FT = 0.025 / day # 1 / s +# (1 / (mmol C / m³), 1 / (mmol C / m³), 1 / (mmol C / m³) / s, 1 / (mmol C / m³) / s) + aggregation_parameters :: AP = (25.9, 4452, 3.3, 47.1) .* (10^-6 / day) + minimum_iron_scavenging_rate :: FT = 3e-5/day # 1 / s + load_specific_iron_scavenging_rate :: FT = 0.005/day # 1 / (mmol C / m³) / s + small_fraction_of_bacterially_consumed_iron :: FT = 0.5 # + large_fraction_of_bacterially_consumed_iron :: FT = 0.5 # + base_liable_silicate_fraction :: FT = 0.5 # + fast_dissolution_rate_of_silicate :: FT = 0.025/day # 1 / s + slow_dissolution_rate_of_silicate :: FT = 0.003/day # 1 / s end -#Forcing for POC -@inline function (bgc::PISCES)(::Val{:POC}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - σᶻ = bgc.non_assimilated_fraction.Z - mᴾ, mᴰ = bgc.phytoplankton_mortality_rate - mᶻ = bgc.zooplankton_quadratic_mortality.Z - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - wₚₒ = bgc.sinking_speed_of_POC - rᶻ = bgc.zooplankton_linear_mortality.Z - Kₘ = bgc.half_saturation_const_for_mortality - b_Z, bₘ = bgc.temperature_sensitivity_term - g_FF = bgc.flux_feeding_rate +@inline function specific_degredation_rate(poc::TwoCompartementParticulateOrganicMatter, bgc, O₂, T) + λ₀ = poc.base_breakdown_rate + b = poc.temperature_sensetivity - #Grazing - grazing = grazing_Z(P, D, POC, T, bgc) - ∑gᶻ = grazing[1] - gₚₒᶻ = grazing[4] - gₚₒᴹ = grazing_M(P, D, Z, POC, T, bgc)[4] - gₚₒ_FFᴹ = g_FF*(bₘ^T)*wₚₒ*POC #29a - - #Aggregation - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + ΔO₂ = anoxia_factor(bgc, O₂) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - Φ₁ᴰᴼᶜ = aggregation_process_for_DOC(DOC, POC, GOC, sh, bgc)[1] - Φ₃ᴰᴼᶜ = aggregation_process_for_DOC(DOC, POC, GOC, sh, bgc)[3] - Φ = POC_aggregation(POC, GOC, sh, bgc) - - R_CaCO₃ = rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc) - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - - return (σᶻ*∑gᶻ*Z + 0.5*mᴰ*concentration_limitation(D, Kₘ)*D + rᶻ*(b_Z^T)*(concentration_limitation(Z, Kₘ) - + 3*oxygen_conditions(O₂, bgc))*Z + mᶻ*(b_Z^T)*Z^2 - + (1 - 0.5*R_CaCO₃)*(mᴾ*concentration_limitation(P, Kₘ)*P + sh*wᴾ*P^2) - + λₚₒ¹*GOC + Φ₁ᴰᴼᶜ + Φ₃ᴰᴼᶜ - (gₚₒᴹ + gₚₒ_FFᴹ)*M - gₚₒᶻ*Z - λₚₒ¹*POC - Φ) #eq37, partial derivative ommitted as included elsewhere in OceanBioME + return λ₀ * b^T * (1 - 0.45 * ΔO₂) end -#Forcing for GOC -@inline function (bgc::PISCES)(::Val{:GOC}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - σᴹ = bgc.non_assimilated_fraction.M - mᴾ, mᴰ = bgc.phytoplankton_mortality_rate - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - rᴹ = bgc.zooplankton_linear_mortality.M - Kₘ = bgc.half_saturation_const_for_mortality - bₘ = bgc.temperature_sensitivity_term.M - g_FF = bgc.flux_feeding_rate - wₘₐₓᴰ = bgc.max_quadratic_mortality_of_diatoms - wₚₒ = bgc.sinking_speed_of_POC +@inline function aggregation(poc::TwoCompartementParticulateOrganicMatter, bgc, z, POC, GOC, zₘₓₗ) + a₁, a₂, a₃, a₄ = poc.aggregation_parameters - #Grazing - w_GOC = sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) - ∑gᴹ = grazing_M(P, D, Z, POC, T, bgc)[1] - ∑g_FFᴹ, gₚₒ_FFᴹ, g_GOC_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc) + backgroound_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear - #Aggregation - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + shear = ifelse(z < zₘₓₗ, backgroound_shear, mixed_layer_shear) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - Φ = POC_aggregation(POC, GOC, sh, bgc) - Φ₂ᴰᴼᶜ = aggregation_process_for_DOC(DOC, POC, GOC, sh, bgc)[2] - - Pᵤₚᴹ = production_of_fecal_pellets(M, T, bgc) - R_CaCO₃ = rain_ratio(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, Fe, T, PAR, zₘₓₗ, bgc) - λₚₒ¹ = particles_carbon_degradation_rate(T, O₂, bgc) - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) + return shear * (a₁ * POC^2 + a₂ * POC * GOC) + a₃ * POC * GOC + a₄ * POC^2 +end - return (σᴹ*(∑gᴹ + ∑g_FFᴹ)*M + rᴹ*(bₘ^T)*(concentration_limitation(M, Kₘ) + 3*oxygen_conditions(O₂, bgc))*M - + Pᵤₚᴹ + 0.5*R_CaCO₃*(mᴾ*concentration_limitation(P, Kₘ)*P + sh*wᴾ*P^2) + 0.5*mᴰ*concentration_limitation(D, Kₘ)*D - + sh*D^2*wᴰ + Φ + Φ₂ᴰᴼᶜ - g_GOC_FFᴹ*M - λₚₒ¹*GOC) #eq40, partial derivative ommitted as included elsewhere in OceanBioME -end \ No newline at end of file +include("particulate_organic_carbon.jl") +include("iron_in_particles.jl") +include("silicon_in_particles.jl") \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/phosphates.jl b/src/Models/AdvectedPopulations/PISCES/phosphates.jl index 3e03c5f14..b75b8fefc 100644 --- a/src/Models/AdvectedPopulations/PISCES/phosphates.jl +++ b/src/Models/AdvectedPopulations/PISCES/phosphates.jl @@ -1,51 +1,39 @@ -#This document contains functions for: - #PO₄ forcing (eq59), multiplied by redfield ratio to return in μmolP/L - -@inline function (bgc::PISCES)(::Val{:PO₄}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - γᶻ = bgc.excretion_as_DOM.Z - σᶻ = bgc.non_assimilated_fraction.Z - γᴹ = bgc.excretion_as_DOM.M - σᴹ = bgc.non_assimilated_fraction.M - αᴾ = bgc.initial_slope_of_PI_curve.P - αᴰ = bgc.initial_slope_of_PI_curve.D - eₘₐₓᶻ = bgc.max_growth_efficiency_of_zooplankton.Z - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - θᴾᶜ = bgc.PC_redfield_ratio - - bFe = Fe - - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] - - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - - #Bacteria - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) #35a - Bact = bacterial_biomass(zₘₐₓ, z, Z, M) - - #Growth rates for phytoplankton - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - - return (θᴾᶜ*(γᶻ*(1-eᶻ-σᶻ)*∑gᶻ*Z + γᴹ*(1 - eᴹ - σᴹ)*(∑gᴹ + ∑g_FFᴹ)*M + γᴹ*upper_respiration(M, T, bgc) - + oxic_remineralization(O₂, NO₃, PO₄, NH₄, DOC, T, bFe, Bact, bgc) + denitrification(NO₃, PO₄, NH₄, DOC, O₂, T, bFe, Bact, bgc) - - μᴾ*P - μᴰ*D)) #eq59 -end \ No newline at end of file +""" + Phosphate + +Evolution of phosphate (PO₄). +""" +struct Phosphate end + +@inline function (phosphate::Phosphate)(::Val{:PO₄}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + θ = bgc.phosphate_redfield_ratio + + microzooplankton_grazing_waste = specific_inorganic_grazing_waste(bgc.microzooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * Z + mesozooplankton_grazing_waste = specific_inorganic_grazing_waste(bgc.mesozooplankton, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) * M + + grazing_waste = microzooplankton_grazing_waste + mesozooplankton_grazing_waste + + respiration_product = inorganic_upper_trophic_respiration_product(bgc.mesozooplankton, M, T) + + remineralisation = oxic_remineralisation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + + denit = denitrifcation(bgc.dissolved_organic_matter, bgc, z, Z, M, DOC, NO₃, NH₄, PO₄, Fe, O₂, T, zₘₓₗ, zₑᵤ) + + nanophytoplankton_consumption, = total_production(bgc.nanophytoplankton, bgc, y, t, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + diatom_consumption, = total_production(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + + consumption = (nanophytoplankton_consumption + diatom_consumption) + + return θ * (grazing_waste + respiration_product + remineralisation + denit - consumption) +end diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton.jl index df4cfa7d2..a794f540e 100644 --- a/src/Models/AdvectedPopulations/PISCES/phytoplankton.jl +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton.jl @@ -1,442 +1,296 @@ +include("base_production.jl") +include("nutrient_limitation.jl") + +""" + MixedMondoPhytoplankton + +Holds the parameters for the PISCES mixed mondo phytoplankton +parameterisation where nutrient limitation is modelled using the +mondo approach for nitrate (NO₃), ammonia (NH₄), phosphate (PO₄), +and silicate (Si), but the quota approach is used for iron (Fe) +and light (PAR). + +Therefore each class has a carbon compartement (generically `I`), +chlorophyll (`IChl`), and iron (`IFe`), and may also have silicate +(`ISi`) if the `nutrient_limitation` specifies that the growth is +silicate limited, despite the fact that the silicate still limits +the growth in a mondo fashion. + +The `growth_rate` may be different parameterisations, currently +either `NutrientLimitedProduction` or +`GrowthRespirationLimitedProduction`, which represent the typical +and `newprod` versions of PISCES. +""" +@kwdef struct MixedMondoPhytoplankton{GR, NL, FT} + growth_rate :: GR + nutrient_limitation :: NL + + exudated_fracton :: FT = 0.05 # + + blue_light_absorption :: FT # + green_light_absorption :: FT # + red_light_absorption :: FT # + + mortality_half_saturation :: FT = 0.2 # mmol C / m³ + linear_mortality_rate :: FT = 0.01 / day # 1 / s + + base_quadratic_mortality :: FT = 0.01 / day # 1 / s / (mmol C / m³) + maximum_quadratic_mortality :: FT # 1 / s / (mmol C / m³) - zero for nanophytoplankton + + minimum_chlorophyll_ratio :: FT = 0.0033 # mg Chl / mg C + maximum_chlorophyll_ratio :: FT # mg Chl / mg C + + maximum_iron_ratio :: FT = 0.06 # μmol Fe / mmol C + + silicate_half_saturation :: FT = 2.0 # mmol Si / m³ + enhanced_silicate_half_saturation :: FT = 20.9 # mmol Si / m³ + optimal_silicate_ratio :: FT = 0.159 # mmol Si / mmol C +end -#PISCES is an implementation of PISCES-v2 described in the 2015 Aumont paper. -#Where possible conventions have been kept in line with the original paper, including notation and equation groupings. -#Some changes have been made due to queries with the original paper, but changes and justification for these have been noted. +@inline phytoplankton_concentration(::NANO_PHYTO, P, D) = P +@inline phytoplankton_concentration(::DIATOMS, P, D) = D -#This document contains equations for: - #nutrient_quotas - #concentration_limitation for determining Monod nutrient limitation terms +@inline phytoplankton_grazing(::NANO_PHYTO, args...) = nanophytoplankton_grazing(args...) +@inline phytoplankton_grazing(::DIATOMS, args...) = diatom_grazing(args...) - #shear_rate - #day_length - #day_dependent_growth_rate (eq3a), depth_dependent_growth_rate (eq3d), t_dark (eq3b) - #Half saturation constants - #P_nutrient_limitation (eq6a), D_nutrient_limitation (eq11a) +@inline function (phyto::MixedMondoPhytoplankton)(val_name::Union{Val{:P}, Val{:D}}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, # we should get rid of DSi and the rest of the Si since it doesn't do anything... + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + # production + δ = phyto.exudated_fracton - #minimum_iron_quota + I = phytoplankton_concentration(val_name, P, D) + IChl = phytoplankton_concentration(val_name, PChl, DChl) + IFe = phytoplankton_concentration(val_name, PFe, DFe) - #P_PAR - #phytoplankton_iron_biomass_growth_rate - #phytoplankton_growth_rate - #nutrient limitation - #variation_in_SiC_ratio - #D_quadratic_mortality - #Forcing equations + μI, L = total_production(phyto, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) -@inline nutrient_quota(a, b) = ifelse(b == 0, 0, a/(b + eps(0.0))) # this shouldn't be needed as I should be zero if J is zero + production = (1 - δ) * μI -@inline concentration_limitation(a, b) = a/(a + b) + # mortality + linear_mortality, quadratic_mortality = mortality(phyto, bgc, z, I, zₘₓₗ, L) -#Expresses growth rate with dependency on day length -@inline day_dependent_growth_rate(L_day) = 1.5*concentration_limitation(L_day, 0.5) #eq 3a + # grazing + gZ = phytoplankton_grazing(val_name, bgc.microzooplankton, P, D, Z, POC, T) + gM = phytoplankton_grazing(val_name, bgc.mesozooplankton, P, D, Z, POC, T) -@inline function depth_dependent_growth_rate(κ, zₘₓₗ, zₑᵤ, t_darkᴵ) - t_dark = (max(0, zₑᵤ - zₘₓₗ)) ^ 2 / κ + grazing = gZ * Z + gM * M - return 1 - t_dark / (t_dark + t_darkᴵ) #eq 3d + return production - linear_mortality - quadratic_mortality - grazing end -#The minimum iron quota is the sum of the three demands for iron in phytoplankton (photosynthesis, respiration, nitrate reduction) -@inline minimum_iron_quota(I, Iᶜʰˡ, Lₙᴵ, Lₙₒ₃ᴵ) = 0.0016/(55.85) * nutrient_quota(Iᶜʰˡ, I) + 1.21e-5*14*Lₙᴵ/(55.85*7.625)*1.5+1.15e-4*14*Lₙₒ₃ᴵ/(55.85*7.625) #eq 20 -> Lₙ could be meant to be L_NH₄? +@inline function (phyto::MixedMondoPhytoplankton)(val_name::Union{Val{:PChl}, Val{:DChl}}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, # we should get rid of DSi and the rest of the Si since it doesn't do anything... + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) -#Different size classes of phytoplankton, have different half-saturation constants due to varying surface area to volume ratios. -#Generally increased biomass corresponds to larger size classes. -#Half saturation constants have biomass dependency. -@inline I₁(I, Iₘₐₓ) = min(I, Iₘₐₓ) #eq 7a -@inline I₂(I, Iₘₐₓ) = max(0, I - Iₘₐₓ) #eq 7b -@inline nutrient_half_saturation_const(Kᵢᴶᵐⁱⁿ, J₁, J₂, Sᵣₐₜᴶ) = Kᵢᴶᵐⁱⁿ* (J₁ + Sᵣₐₜᴶ* J₂)/(J₁ + J₂ + eps(0.0)) #eq 7c + I = phytoplankton_concentration(val_name, P, D) + IChl = phytoplankton_concentration(val_name, PChl, DChl) + IFe = phytoplankton_concentration(val_name, PFe, DFe) -#Light absorption by phytoplankton. Visible light split into 3 wavebands, where light absorption of each waveband controlled by coefficient. -# I don't think this adds up to match the chlorophyll absorption assumed by the light model -@inline function P_PAR(PAR₁, PAR₂, PAR₃, bgc) - β₁ᴾ = bgc.absorption_in_the_blue_part_of_light.P - β₂ᴾ = bgc.absorption_in_the_green_part_of_light.P - β₃ᴾ = bgc.absorption_in_the_red_part_of_light.P + # production + δ = phyto.exudated_fracton - return β₁ᴾ*PAR₁ + β₂ᴾ*PAR₂ + β₃ᴾ*PAR₃ -end + θ₀ = phyto.minimum_chlorophyll_ratio + θ₁ = phyto.maximum_chlorophyll_ratio -@inline function D_PAR(PAR₁, PAR₂, PAR₃, bgc) - β₁ᴰ = bgc.absorption_in_the_blue_part_of_light.D - β₂ᴰ = bgc.absorption_in_the_green_part_of_light.D - β₃ᴰ = bgc.absorption_in_the_red_part_of_light.D + L, = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) - return β₁ᴰ*PAR₁ + β₂ᴰ*PAR₂ + β₃ᴰ*PAR₃ -end + μ, ρ = production_and_energy_assimilation_absorption_ratio(phyto.growth_rate, phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR, PAR₁, PAR₂, PAR₃, L) -#The growth rate of the iron biomass of phytoplankton. -@inline function phytoplankton_iron_biomass_growth_rate(I, Iᶠᵉ, θₘₐₓᶠᵉᴵ, Sᵣₐₜᴵ, K_Feᴵᶠᵉᵐⁱⁿ, Iₘₐₓ, L_Feᴵ, bFe, T, bgc) - μ⁰ₘₐₓ = bgc.growth_rate_at_zero - bₚ = bgc.temperature_sensitivity_of_growth + production = (1 - δ) * 12 * (θ₀ + (θ₁ - θ₀) * ρ) * μ * I - μₚ = μ⁰ₘₐₓ*(bₚ^T) #4b + # mortality + θChl = IChl / (12 * I + eps(0.0)) - I₂ = max(0, I - Iₘₐₓ) #18c - I₁ = I - I₂ #18c + linear_mortality, quadratic_mortality = mortality(phyto, bgc, z, I, zₘₓₗ, L) - K_Feᴵᶠᵉ = K_Feᴵᶠᵉᵐⁱⁿ*(I₁ + Sᵣₐₜᴵ*I₂)/(I₁+I₂+eps(0.0)) #18b + linear_mortality *= θChl * 12 + quadratic_mortality *= θChl * 12 + + # grazing - Lₗᵢₘ₁ᴵᶠᵉ = concentration_limitation(bFe, K_Feᴵᶠᵉ) #18a - #Lₗᵢₘ₂ᴵᶠᵉ = (4 - 4.5*L_Feᴵ)/(L_Feᴵ + 0.5) #Formulation given in paper does not vary between 1 and 4 as claimed - Lₗᵢₘ₂ᴵᶠᵉ = (4 - 2*L_Feᴵ)/(L_Feᴵ + 1) #19, reformulation is between given bounds + gZ = phytoplankton_grazing(val_name, bgc.microzooplankton, P, D, Z, POC, T) + gM = phytoplankton_grazing(val_name, bgc.mesozooplankton, P, D, Z, POC, T) - return θₘₐₓᶠᵉᴵ*Lₗᵢₘ₁ᴵᶠᵉ*Lₗᵢₘ₂ᴵᶠᵉ*(1 - (nutrient_quota(Iᶠᵉ, I))/(θₘₐₓᶠᵉᴵ + eps(0.0)))/(1.05 - (nutrient_quota(Iᶠᵉ, I))/(θₘₐₓᶠᵉᴵ + eps(0.0)))*μₚ #eq17 + grazing = (gZ * Z + gM * M) * θChl * 12 + + return production - linear_mortality - quadratic_mortality - grazing end -#Growth rates of phytoplankton depend on limiting nutrients, day length, and light absorbtion of phytoplankton. -@inline function phytoplankton_growth_rate(I, Iᶜʰˡ, PARᴵ, L_day, T, αᴵ, Lₗᵢₘᴵ, zₘₓₗ, zₑᵤ, κ, t_darkᴵ, bgc) - μ⁰ₘₐₓ = bgc.growth_rate_at_zero - bₚ = bgc.temperature_sensitivity_of_growth +@inline function (phyto::MixedMondoPhytoplankton)(val_name::Union{Val{:PFe}, Val{:DFe}}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + I = phytoplankton_concentration(val_name, P, D) + IChl = phytoplankton_concentration(val_name, PChl, DChl) + IFe = phytoplankton_concentration(val_name, PFe, DFe) + + # production + production, L = iron_uptake(phyto, bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T) + + # mortality + linear_mortality, quadratic_mortality = mortality(phyto, bgc, z, I, zₘₓₗ, L) + + linear_mortality *= IFe / (I + eps(0.0)) + quadratic_mortality *= IFe / (I + eps(0.0)) + + # grazing + gZ = phytoplankton_grazing(val_name, bgc.microzooplankton, P, D, Z, POC, T) + gM = phytoplankton_grazing(val_name, bgc.mesozooplankton, P, D, Z, POC, T) - μₚ = μ⁰ₘₐₓ*(bₚ^T) #eq 4b + grazing = (gZ * Z + gM * M) * IFe / (I + eps(0.0)) - return μₚ * day_dependent_growth_rate(L_day) * depth_dependent_growth_rate(κ, zₘₓₗ, zₑᵤ, t_darkᴵ) * (1-exp(-αᴵ*(nutrient_quota(Iᶜʰˡ,I))*PARᴵ/(L_day*μₚ*Lₗᵢₘᴵ + eps(0.0)))) * Lₗᵢₘᴵ #eq2b + return production - linear_mortality - quadratic_mortality - grazing end +@inline function iron_uptake(phyto::MixedMondoPhytoplankton, bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T) + δ = phyto.exudated_fracton + θFeₘ = phyto.maximum_iron_ratio -#Nutrient limitation terms. -#Nutrient and phosphate limitations are based on Monod parametrisations, iron on quota parametrisations. -@inline ammonium_limitation(NO₃, NH₄, Kₙₒ₃ᴵ, Kₙₕ₄ᴵ) = Kₙₒ₃ᴵ*NH₄/(Kₙₒ₃ᴵ*Kₙₕ₄ᴵ+Kₙₕ₄ᴵ*NO₃+Kₙₒ₃ᴵ*NH₄ + eps(0.0)) #eq 6d -@inline nitrate_limitation(NO₃, NH₄, Kₙₒ₃ᴵ, Kₙₕ₄ᴵ) = Kₙₕ₄ᴵ*NO₃/(Kₙₒ₃ᴵ*Kₙₕ₄ᴵ+Kₙₕ₄ᴵ*NO₃+Kₙₒ₃ᴵ*NH₄ + eps(0.0)) #eq 6e -@inline iron_limitation(I, Iᶠᵉ, θₒₚₜᶠᵉᴵ, θₘᵢₙᶠᵉᴵ) = min(1, max(0, (nutrient_quota(Iᶠᵉ, I) - θₘᵢₙᶠᵉᴵ)/(θₒₚₜᶠᵉᴵ + eps(0.0)))) #eq 6f - -#Determines individual nutrient limitation terms, and overall limiting nutrients. -@inline function P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc) - #Parameters - θₒₚₜᶠᵉᵖ = bgc.optimal_iron_quota.P - Sᵣₐₜᴾ = bgc.size_ratio_of_phytoplankton.P - Kₙₒ₃ᴾᵐⁱⁿ = bgc.min_half_saturation_const_for_nitrate.P - Kₙₕ₄ᴾᵐⁱⁿ = bgc.min_half_saturation_const_for_ammonium.P - Kₚₒ₄ᴾᵐⁱⁿ = bgc.min_half_saturation_const_for_phosphate.P - Pₘₐₓ = bgc.threshold_concentration_for_size_dependency.P - - #Half saturation constants - P₁ = I₁(P, Pₘₐₓ) - P₂ = I₂(P, Pₘₐₓ) - Kₙₒ₃ᴾ = nutrient_half_saturation_const(Kₙₒ₃ᴾᵐⁱⁿ, P₁, P₂, Sᵣₐₜᴾ) - Kₙₕ₄ᴾ = nutrient_half_saturation_const(Kₙₕ₄ᴾᵐⁱⁿ, P₁, P₂, Sᵣₐₜᴾ) - Kₚₒ₄ᴾ = nutrient_half_saturation_const(Kₚₒ₄ᴾᵐⁱⁿ, P₁, P₂, Sᵣₐₜᴾ) - - #Nutrient limitation terms (for phosphate, ammonium, nitrate, iron) - Lₚₒ₄ᴾ = concentration_limitation(PO₄, Kₚₒ₄ᴾ) #6b - Lₙₕ₄ᴾ = ammonium_limitation(NO₃, NH₄, Kₙₒ₃ᴾ, Kₙₕ₄ᴾ) - Lₙₒ₃ᴾ = nitrate_limitation(NO₃, NH₄, Kₙₒ₃ᴾ, Kₙₕ₄ᴾ) - Lₙᴾ = Lₙₒ₃ᴾ + Lₙₕ₄ᴾ #6c - θₘᵢₙᶠᵉᵖ = minimum_iron_quota(P, Pᶜʰˡ, Lₙₕ₄ᴾ, Lₙₒ₃ᴾ) #changed from Lₙᴾ to Lₙₕ₄ᴾ - L_Feᴾ = iron_limitation(P, Pᶠᵉ, θₒₚₜᶠᵉᵖ, θₘᵢₙᶠᵉᵖ) - - return min(Lₚₒ₄ᴾ, Lₙᴾ, L_Feᴾ), Lₚₒ₄ᴾ, Lₙₕ₄ᴾ, Lₙₒ₃ᴾ, Lₙᴾ, L_Feᴾ #6a -end + θFe = IFe / (I + eps(0.0)) # μmol Fe / mmol C -#Same for Lₗᵢₘᴰ -@inline function D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - #Parameters - θₒₚₜᶠᵉᴰ = bgc.optimal_iron_quota.D - Sᵣₐₜᴰ = bgc.size_ratio_of_phytoplankton.D - Kₙₒ₃ᴰᵐⁱⁿ = bgc.min_half_saturation_const_for_nitrate.D - Kₙₕ₄ᴰᵐⁱⁿ = bgc.min_half_saturation_const_for_ammonium.D - Kₚₒ₄ᴰᵐⁱⁿ = bgc.min_half_saturation_const_for_phosphate.D - Kₛᵢᴰᵐⁱⁿ = bgc.min_half_saturation_const_for_silicate - Kₛᵢ = bgc.parameter_for_half_saturation_const - Dₘₐₓ = bgc.threshold_concentration_for_size_dependency.D - - #Half saturation constants - D₁ = I₁(D, Dₘₐₓ) - D₂ = I₂(D, Dₘₐₓ) - Kₙₒ₃ᴰ = nutrient_half_saturation_const(Kₙₒ₃ᴰᵐⁱⁿ, D₁, D₂, Sᵣₐₜᴰ) - Kₙₕ₄ᴰ = nutrient_half_saturation_const(Kₙₕ₄ᴰᵐⁱⁿ, D₁, D₂, Sᵣₐₜᴰ) - Kₚₒ₄ᴰ = nutrient_half_saturation_const(Kₚₒ₄ᴰᵐⁱⁿ, D₁, D₂, Sᵣₐₜᴰ) - - #Nutrient limitation terms (for phosphate, ammonium, nitrate, iron, silicate) - Lₚₒ₄ᴰ = concentration_limitation(PO₄, Kₚₒ₄ᴰ) #6b - Lₙₕ₄ᴰ = ammonium_limitation(NO₃, NH₄, Kₙₒ₃ᴰ, Kₙₕ₄ᴰ) - Lₙₒ₃ᴰ = nitrate_limitation(NO₃, NH₄, Kₙₒ₃ᴰ, Kₙₕ₄ᴰ) - Lₙᴰ = Lₙₒ₃ᴰ + Lₙₕ₄ᴰ #6c - θₘᵢₙᶠᵉᴰ = minimum_iron_quota(D, Dᶜʰˡ, Lₙₕ₄ᴰ, Lₙₒ₃ᴰ) #changed from n to NH₄ - L_Feᴰ = iron_limitation(D, Dᶠᵉ ,θₒₚₜᶠᵉᴰ, θₘᵢₙᶠᵉᴰ) - Kₛᵢᴰ = Kₛᵢᴰᵐⁱⁿ + 7*Si̅^2 / (Kₛᵢ^2 + Si̅^2 + eps(0.0)) #12 - Lₛᵢᴰ = concentration_limitation(Si, Kₛᵢᴰ) #11b - - return min(Lₚₒ₄ᴰ, Lₙᴰ, L_Feᴰ, Lₛᵢᴰ), Lₚₒ₄ᴰ, Lₙₕ₄ᴰ, Lₙₒ₃ᴰ, Lₙᴰ, L_Feᴰ, Lₛᵢᴰ #11a -end + L, LFe = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + μᵢ = base_production_rate(phyto.growth_rate, T) -@inline function variation_in_SiC_ratio(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, μᴰ, T, φ, Si̅, bgc) - θₘˢⁱᴰ = bgc.optimal_SiC_uptake_ratio_of_diatoms - μ⁰ₘₐₓ = bgc.growth_rate_at_zero - Kₛᵢ¹ = bgc.parameter_for_SiC.one - Kₛᵢ² = bgc.parameter_for_SiC.two - bₚ = bgc.temperature_sensitivity_of_growth + L₁ = iron_uptake_limitation(phyto.nutrient_limitation, I, Fe) # assuming bFe = Fe - Lₗᵢₘᴰ, Lₚₒ₄ᴰ, Lₙₕ₄ᴰ, Lₙₒ₃ᴰ, Lₙᴰ, Lₛᵢᴰ, L_Feᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - - μₚ = μ⁰ₘₐₓ*(bₚ^T) - - Lₗᵢₘ₁ᴰˢⁱ = concentration_limitation(Si, Kₛᵢ¹) #23c - Lₗᵢₘ₂ᴰˢⁱ = ifelse(φ < 0, (concentration_limitation((Si)^3, (Kₛᵢ²)^3)), 0) #23d - - Fₗᵢₘ₁ᴰˢⁱ = min((μᴰ)/(μₚ*Lₗᵢₘᴰ + eps(0.0)), Lₚₒ₄ᴰ, Lₙᴰ, L_Feᴰ) #23a - Fₗᵢₘ₂ᴰˢⁱ = min(1, 2.2*max(0, Lₗᵢₘ₁ᴰˢⁱ - 0.5)) #23b + L₂ = 4 - 4.5 * LFe / (LFe + 1) # typo in Aumount 2015 - return θₘˢⁱᴰ*Lₗᵢₘ₁ᴰˢⁱ*min(5.4, ((4.4*exp(-4.23*Fₗᵢₘ₁ᴰˢⁱ)*Fₗᵢₘ₂ᴰˢⁱ + 1)*(1 + 2*Lₗᵢₘ₂ᴰˢⁱ))) #22 + return (1 - δ) * θFeₘ * L₁ * L₂ * max(0, (1 - θFe / θFeₘ) / (1.05 - θFe / θFeₘ)) * μᵢ * I, L end -#Diatoms have variable quadratic mortality term. -@inline function D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - wₘₐₓᴰ = bgc.max_quadratic_mortality_of_diatoms - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - return wᴾ + wₘₐₓᴰ*(1-Lₗᵢₘᴰ) #eq13 -end +@inline function (phyto::MixedMondoPhytoplankton)(::Val{:DSi}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + # production + production, L = silicate_uptake(phyto, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) -#Phytoplankton forcing -@inline function (bgc::PISCES)(::Val{:P}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴾ = bgc.exudation_of_DOC.P - mᴾ = bgc.phytoplankton_mortality_rate.P - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - αᴾ = bgc.initial_slope_of_PI_curve.P + # mortality + linear_mortality, quadratic_mortality = mortality(phyto, bgc, z, D, zₘₓₗ, L) - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + linear_mortality *= DSi / (D + eps(0.0)) + quadratic_mortality *= DSi / (D + eps(0.0)) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) + # grazing + gZ = diatom_grazing(bgc.microzooplankton, P, D, Z, POC, T) + gM = diatom_grazing(bgc.mesozooplankton, P, D, Z, POC, T) - #L_day - φ = bgc.latitude - φ = latitude(φ, y) + grazing = (gZ * Z + gM * M) * DSi / (D + eps(0.0)) + return production - linear_mortality - quadratic_mortality - grazing +end - L_day = day_length(φ, t) - #Grazing - gₚᶻ = grazing_Z(P, D, POC, T, bgc)[2] - gₚᴹ = grazing_M(P, D, Z, POC, T, bgc)[2] +@inline function silicate_uptake(phyto::MixedMondoPhytoplankton, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + δ = phyto.exudated_fracton + + K₁ = phyto.silicate_half_saturation + K₂ = phyto.enhanced_silicate_half_saturation + θ₀ = phyto.optimal_silicate_ratio - #Phytoplankton growth - Lₗᵢₘᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) + L, LFe, LPO₄, LN = phyto.nutrient_limitation(bgc, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′) - return (1-δᴾ)*μᴾ*P - mᴾ*concentration_limitation(P, Kₘ)*P - sh*wᴾ*P^2 - gₚᶻ*Z - gₚᴹ*M #eq 1 -end + μ = phyto.growth_rate(phyto, bgc, y, t, D, DChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) -#Diatom forcing -@inline function (bgc::PISCES)(::Val{:D}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴰ = bgc.exudation_of_DOC.D - mᴰ = bgc.phytoplankton_mortality_rate.D - Kₘ = bgc.half_saturation_const_for_mortality - αᴰ = bgc.initial_slope_of_PI_curve.D + μᵢ = base_production_rate(phyto.growth_rate, T) - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + L₁ = Si / (Si + K₁ + eps(0.0)) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) + # enhanced silication in southern ocean + φ = bgc.latitude(y) + + L₂ = ifelse(φ < 0, Si^3 / (Si^3 + K₂^3), 0) - #L_day - φ = bgc.latitude - φ = latitude(φ, y) + F₁ = min(μ / (μᵢ * L + eps(0.0)), LFe, LPO₄, LN) + F₂ = min(1, 2.2 * max(0, L₁ - 0.5)) - L_day = day_length(φ, t) - #Grazing - g_Dᶻ = grazing_Z(P, D, POC, T, bgc)[3] - g_Dᴹ = grazing_M(P, D, Z, POC, T, bgc)[3] + θ₁ = θ₀ * L₁ * min(5.4, (4.4 * exp(-4.23 * F₁) * F₂ + 1) * (1 + 2 * L₂)) - #Nutrient limitation - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] + return (1 - δ) * θ₁ * μ * D, L +end - #Also required - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) #13 +@inline function dissolved_exudate(phyto::MixedMondoPhytoplankton, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + δ = phyto.exudated_fracton - return (1-δᴰ)*μᴰ*D - mᴰ*concentration_limitation(D, Kₘ)*D - sh*wᴰ*D^2 - g_Dᶻ*Z - g_Dᴹ*M #eq 9 + μI, = total_production(phyto, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + return δ * μI end -#Forcing for chlorophyll biomass of nanophytoplankton -@inline function (bgc::PISCES)(::Val{:Pᶜʰˡ}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴾ = bgc.exudation_of_DOC.P - αᴾ = bgc.initial_slope_of_PI_curve.P - θₘᵢₙᶜʰˡ = bgc.min_ChlC_ratios_of_phytoplankton - θₘₐₓᶜʰˡᴾ = bgc.max_ChlC_ratios_of_phytoplankton.P - mᴾ = bgc.phytoplankton_mortality_rate.P - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton +@inline function mortality(phyto::MixedMondoPhytoplankton, bgc, z, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + L, = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear + return mortality(phyto, bgc, z, I, zₘₓₗ, L) +end - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) +@inline function mortality(phyto::MixedMondoPhytoplankton, bgc, z, I, zₘₓₗ, L) + K = phyto.mortality_half_saturation + m = phyto.linear_mortality_rate - #L_day - φ = bgc.latitude - φ = latitude(φ, y) + background_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear + linear_mortality = m * I / (I + K) * I - L_day = day_length(φ, t) - #Grazing - gₚᶻ = grazing_Z(P, D, POC, T, bgc)[2] - gₚᴹ = grazing_M(P, D, Z, POC, T, bgc)[2] + w₀ = phyto.base_quadratic_mortality + w₁ = phyto.maximum_quadratic_mortality - #Phytoplankton growth - t_darkᴾ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.P - Lₗᵢₘᴾ= P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[1] - PARᴾ = P_PAR(PAR₁, PAR₂, PAR₃, bgc) - μᴾ = phytoplankton_growth_rate(P, Pᶜʰˡ, PARᴾ, L_day, T, αᴾ, Lₗᵢₘᴾ, zₘₓₗ, zₑᵤ, κ, t_darkᴾ, bgc) + w = w₀ + w₁ * 0.25 * (1 - L^2) / (0.25 + L^2) + + shear = ifelse(z < zₘₓₗ, background_shear, mixed_layer_shear) - μ̌ᴾ = μᴾ / day_dependent_growth_rate(L_day) #15b - ρᴾᶜʰˡ = 144*μ̌ᴾ * P / (αᴾ* Pᶜʰˡ* ((PARᴾ)/(L_day + eps(0.0))) + eps(0.0)) #15a + quadratic_mortality = shear * w * I^2 - return ((1-δᴾ)*(12*θₘᵢₙᶜʰˡ + (θₘₐₓᶜʰˡᴾ - θₘᵢₙᶜʰˡ)*ρᴾᶜʰˡ)*μᴾ*P - mᴾ*concentration_limitation(P, Kₘ)*Pᶜʰˡ - - sh*wᴾ*P*Pᶜʰˡ - nutrient_quota(Pᶜʰˡ, P)*gₚᶻ*Z - nutrient_quota(Pᶜʰˡ, P)*gₚᴹ*M) #14 + return linear_mortality, quadratic_mortality end -#Forcing for chlorophyll biomass of diatoms -@inline function (bgc::PISCES)(::Val{:Dᶜʰˡ}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴰ = bgc.exudation_of_DOC.D - αᴰ = bgc.initial_slope_of_PI_curve.D - θₘᵢₙᶜʰˡ = bgc.min_ChlC_ratios_of_phytoplankton - θₘₐₓᶜʰˡᴰ = bgc.max_ChlC_ratios_of_phytoplankton.D - mᴰ = bgc.phytoplankton_mortality_rate.D - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - wₘₐₓᴰ = bgc.max_quadratic_mortality_of_diatoms +@inline function nitrate_uptake(phyto::MixedMondoPhytoplankton, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + L, _, _, LN, L_NO₃ = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) - #L_day - φ = bgc.latitude - φ = latitude(φ, y) + μ = phyto.growth_rate(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) * I + return μ * L_NO₃ / (LN + eps(0.0)) +end - L_day = day_length(φ, t) - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear +@inline function ammonia_uptake(phyto::MixedMondoPhytoplankton, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + L, _, _, LN, _, L_NH₄ = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - - #Grazing - g_Dᶻ = grazing_Z(P, D, POC, T, bgc)[3] - g_Dᴹ = grazing_M(P, D, Z, POC, T, bgc)[3] - - #Diatom growth - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - - μ̌ᴰ = μᴰ / (day_dependent_growth_rate(L_day) + eps(0.0)) #15b - ρᴰᶜʰˡ = 144*μ̌ᴰ * D / (αᴰ* Dᶜʰˡ* ((PARᴰ)/(L_day + eps(0.0))) + eps(0.0)) #15a - - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) #13 - - return ((1-δᴰ)*(12*θₘᵢₙᶜʰˡ + (θₘₐₓᶜʰˡᴰ - θₘᵢₙᶜʰˡ)*ρᴰᶜʰˡ)*μᴰ*D - - mᴰ*concentration_limitation(D, Kₘ)*Dᶜʰˡ - sh*wᴰ*D*Dᶜʰˡ - - nutrient_quota(Dᶜʰˡ, D)*g_Dᶻ*Z - nutrient_quota(Dᶜʰˡ, D)*g_Dᴹ*M) #14 -end + μ = phyto.growth_rate(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) * I -#Forcing for iron biomass of nanophytoplankton -@inline function (bgc::PISCES)(::Val{:Pᶠᵉ}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴾ = bgc.exudation_of_DOC.P - θₘₐₓᶠᵉᵖ = bgc.max_iron_quota.P - mᴾ = bgc.phytoplankton_mortality_rate.P - Kₘ = bgc.half_saturation_const_for_mortality - wᴾ = bgc.min_quadratic_mortality_of_phytoplankton - Sᵣₐₜᴾ = bgc.size_ratio_of_phytoplankton.P - K_Feᴾᶠᵉᵐⁱⁿ = bgc.min_half_saturation_const_for_iron_uptake.P # this seems wrong as doesn't quite match parameter list - Pₘₐₓ = bgc.threshold_concentration_for_size_dependency.P - - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - - #Grazing - gₚᶻ = grazing_Z(P, D, POC, T, bgc)[2] - gₚᴹ = grazing_M(P, D, Z, POC, T, bgc)[2] - - #Phytoplankton iron growth - L_Feᴾ = P_nutrient_limitation(P, PO₄, NO₃, NH₄, Pᶜʰˡ, Pᶠᵉ, bgc)[6] - bFe = Fe #defined in previous PISCES model - μᴾᶠᵉ = phytoplankton_iron_biomass_growth_rate(P, Pᶠᵉ, θₘₐₓᶠᵉᵖ, Sᵣₐₜᴾ, K_Feᴾᶠᵉᵐⁱⁿ, Pₘₐₓ, L_Feᴾ, bFe, T, bgc) - - return ((1-δᴾ)*μᴾᶠᵉ*P - mᴾ*concentration_limitation(P, Kₘ)*Pᶠᵉ - sh*wᴾ*P*Pᶠᵉ - - nutrient_quota(Pᶠᵉ, P)*gₚᶻ*Z - nutrient_quota(Pᶠᵉ, P)*gₚᴹ*M ) #16 + return μ * L_NH₄ / (LN + eps(0.0)) end -#Forcing for chlorophyll biomass of diatoms -@inline function (bgc::PISCES)(::Val{:Dᶠᵉ}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - δᴰ = bgc.exudation_of_DOC.D - θₘₐₓᶠᵉᴰ = bgc.max_iron_quota.D - mᴰ = bgc.phytoplankton_mortality_rate.D - Kₘ = bgc.half_saturation_const_for_mortality - Sᵣₐₜᴰ = bgc.size_ratio_of_phytoplankton.D - Dₘₐₓ = bgc.threshold_concentration_for_size_dependency.D - K_Feᴰᶠᵉᵐⁱⁿ = bgc.min_half_saturation_const_for_iron_uptake.D - - #Also required - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) #13 - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - - #Limiting nutrients - L = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - Lₗᵢₘᴰ = L[1] - L_Feᴰ = L[6] - - #Grazing - g_Dᶻ = grazing_Z(P, D, POC, T, bgc)[3] - g_Dᴹ = grazing_M(P, D, Z, POC, T, bgc)[3] - - #Diatom iron growth - bFe = Fe - μᴰᶠᵉ = phytoplankton_iron_biomass_growth_rate(D, Dᶠᵉ, θₘₐₓᶠᵉᴰ, Sᵣₐₜᴰ, K_Feᴰᶠᵉᵐⁱⁿ, Dₘₐₓ, L_Feᴰ, bFe, T, bgc) - - return ((1-δᴰ)*μᴰᶠᵉ*D - mᴰ*concentration_limitation(D, Kₘ)*Dᶠᵉ - sh*wᴰ*D*Dᶠᵉ - - nutrient_quota(Dᶠᵉ, D)*g_Dᶻ*Z - nutrient_quota(Dᶠᵉ, D)*g_Dᴹ*M) #16 -end +@inline function total_production(phyto::MixedMondoPhytoplankton, bgc, y, t, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, T, Si′, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) + L, = phyto.nutrient_limitation(bgc, I, IChl, IFe, NO₃, NH₄, PO₄, Fe, Si, Si′) -#Forcing equations for silicon biomass of diatoms -@inline function (bgc::PISCES)(::Val{:Dˢⁱ}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) #φ is latitude - #Parameters - δᴰ = bgc.exudation_of_DOC.D - mᴰ = bgc.phytoplankton_mortality_rate.D - Kₘ = bgc.half_saturation_const_for_mortality - αᴰ = bgc.initial_slope_of_PI_curve.D - - #L_day - φ = bgc.latitude - φ = latitude(φ, y) - - - L_day = day_length(φ, t) - #Grazing - g_Dᶻ = grazing_Z(P, D, POC, T, bgc)[3] - g_Dᴹ = grazing_M(P, D, Z, POC, T, bgc)[3] - - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - - #Diatom growth - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - - #Also required - θₒₚₜˢⁱᴰ = variation_in_SiC_ratio(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, μᴰ, T, φ, Si̅, bgc) - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) #13 - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear - - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - - return (θₒₚₜˢⁱᴰ*(1-δᴰ)*μᴰ*D - nutrient_quota(Dˢⁱ, D)*g_Dᴹ*M - nutrient_quota(Dˢⁱ, D)*g_Dᶻ*Z - - mᴰ*concentration_limitation(D, Kₘ)*Dˢⁱ - sh*wᴰ*D*Dˢⁱ) #21 -end \ No newline at end of file + return phyto.growth_rate(phyto, bgc, y, t, I, IChl, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃, L) * I, L +end diff --git a/src/Models/AdvectedPopulations/PISCES/show_methods.jl b/src/Models/AdvectedPopulations/PISCES/show_methods.jl new file mode 100644 index 000000000..80c2ce4cf --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/show_methods.jl @@ -0,0 +1,100 @@ +summary(::PISCES) = string("PISCES biogeochemical model") + +function show(io::IO, bgc::PISCES) + + FT = typeof(bgc.background_shear) + + output = "Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) model {$FT}" + + output *= "\n Nanophytoplankton: $(summary(bgc.nanophytoplankton))" + + output *= "\n Diatoms: $(summary(bgc.diatoms))" + + output *= "\n Microzooplankton: $(summary(bgc.microzooplankton))" + + output *= "\n Mesozooplankton: $(summary(bgc.mesozooplankton))" + + output *= "\n Microzooplankton: $(summary(bgc.microzooplankton))" + + output *= "\n Dissolved organic matter: $(summary(bgc.dissolved_organic_matter))" + + output *= "\n Particulate organic matter: $(summary(bgc.particulate_organic_matter))" + + output *= "\n Nitrogen: $(summary(bgc.nitrogen))" + + output *= "\n Iron: $(summary(bgc.iron))" + + output *= "\n Silicate: $(summary(bgc.silicate))" + + output *= "\n Oxygen: $(summary(bgc.oxygen))" + + output *= "\n Phosphate: $(summary(bgc.phosphate))" + + output *= "\n Calcite: $(summary(bgc.calcite))" + + output *= "\n Carbon system: $(summary(bgc.carbon_system))" + + output *= "\n Latitude: $(summary(bgc.latitude))" + + output *= "\n Day length: $(nameof(bgc.day_length))" + + output *= "\n Mixed layer depth: $(summary(bgc.mixed_layer_depth))" + + output *= "\n Euphotic depth: $(summary(bgc.euphotic_depth))" + + output *= "\n Silicate climatology: $(summary(bgc.silicate_climatology))" + + output *= "\n Mixed layer mean diffusivity: $(summary(bgc.mean_mixed_layer_vertical_diffusivity))" + + output *= "\n Mixed layer mean light: $(summary(bgc.mean_mixed_layer_light))" + + output *= "\n Carbon chemistry: $(summary(bgc.carbon_chemistry))" + + output *= "\n Calcite saturation: $(summary(bgc.calcite_saturation))" + + output *= "\n Sinking velocities:" + output *= "\n Small particles: $(summary(bgc.sinking_velocities.POC))" + output *= "\n Large particles: $(summary(bgc.sinking_velocities.GOC))" + + print(io, output) + + return nothing +end + +function summary(phyto::MixedMondoPhytoplankton{<:Any, <:Any, FT}) where FT + growth_rate = summary(phyto.growth_rate) + nutrient_limitation = summary(phyto.nutrient_limitation) + + names = "\n I (mmol C / m³), IChl (mg Chl / m³), IFe (μmol Fe / m³)" + + if phyto.nutrient_limitation.silicate_limited + names *= ", ISi (mmol Si / m³)" + end + + return string("MixedMondoPhytoplankton{$(growth_rate), $(nutrient_limitation), $FT} - "*names) +end + +summary(::NutrientLimitedProduction) = string("NutrientLimitedProduction") +summary(::GrowthRespirationLimitedProduction) = string("NutrientLimitedProduction") + +summary(::NitrogenIronPhosphateSilicateLimitation) = string("NitrogenIronPhosphateSilicateLimitation") + +summary(::Zooplankton{FT}) where FT = string("Zooplankton{$FT} - I (mmol C / m³)") + +summary(::DissolvedOrganicMatter{FT}) where FT = string("DissolvedOrganicMatter{$FT} - DOC (mmol C / m³)") +summary(::TwoCompartementParticulateOrganicMatter{FT}) where FT = + string("TwoCompartementParticulateOrganicMatter{$FT} - + POC (mmol C / m³), GOC (mmol C / m³), SFe (μmol Fe / m³), BFe (μmol Fe / m³), PSi (mmol Si / m³)") + +summary(::NitrateAmmonia{FT}) where FT = string("NitrateAmmonia{$FT} - NO₃ (mmol N / m³), NH₄(mmol N / m³)") +summary(::SimpleIron{FT}) where FT = string("SimpleIron{$FT} - Fe (μmol Fe / m³)") +summary(::Oxygen{FT}) where FT = string("Oxygen{$FT} - O₂ (mmol O₂ / m³)") +summary(::Silicate) = string("Silicate - Si (mmol Si / m³)") +summary(::Phosphate) = string("Phosphate - PO₄ (mmol P / m³)") +summary(::Calcite{FT}) where FT = string("Calcite{$FT} - CaCO₃ (mmol C / m³)") +summary(::CarbonateSystem) = string("CarbonateSystem - DIC (mmol C / m³), Alk (mequiv / m³)") + +summary(::ModelLatitude) = string("ModelLatitude") +summary(lat::PrescribedLatitude{FT}) where FT = string("PrescribedLatitude $(lat.latitude)° {FT}") + +# TODO: add show methods \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/silicon.jl b/src/Models/AdvectedPopulations/PISCES/silicon.jl index 7fcfed4c1..2b95e40ef 100644 --- a/src/Models/AdvectedPopulations/PISCES/silicon.jl +++ b/src/Models/AdvectedPopulations/PISCES/silicon.jl @@ -1,27 +1,25 @@ -#The silicon compartment of the model is composed of Dˢⁱ, Si, PSi. Silicon is a limiting nutrient for diatoms, but not phytoplankton. -#Silicon is conserved in the model. +""" + Silicate -# This documentation contains functions for: - #Si (eq74) +Parameterisation for silicate (Si) which is consumed by diatoms +and dissolutioned from particles. +""" +struct Silicate end -@inline function (bgc::PISCES)(::Val{:Si}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) #eq74 - #Parameters - δᴰ = bgc.exudation_of_DOC.D - αᴰ = bgc.initial_slope_of_PI_curve.D - t_darkᴰ = bgc.mean_residence_time_of_phytoplankton_in_unlit_mixed_layer.D - Dissₛᵢ = bgc.dissolution_rate_of_silicon +@inline function (silicate::Silicate)(::Val{:Si}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) - λₚₛᵢ¹ = PSi_dissolution_rate(zₘₓₗ, zₑᵤ, z, T, Si, bgc) - - #L_day - φ = bgc.latitude - φ = latitude(φ, y) + consumption, = silicate_uptake(bgc.diatoms, bgc, y, t, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, T, zₘₓₗ, zₑᵤ, κ, PAR₁, PAR₂, PAR₃) - L_day = day_length(φ, t) - #Diatom growth - Lₗᵢₘᴰ = D_nutrient_limitation(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc)[1] - PARᴰ = D_PAR(PAR₁, PAR₂, PAR₃, bgc) - μᴰ = phytoplankton_growth_rate(D, Dᶜʰˡ, PARᴰ, L_day, T, αᴰ, Lₗᵢₘᴰ, zₘₓₗ, zₑᵤ, κ, t_darkᴰ, bgc) - - return λₚₛᵢ¹*Dissₛᵢ*PSi - variation_in_SiC_ratio(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, μᴰ, T, φ, Si̅, bgc)*(1-δᴰ)*μᴰ*D #eq74 -end \ No newline at end of file + dissolution = particulate_silicate_dissolution(bgc.particulate_organic_matter, z, PSi, Si, T, zₘₓₗ, zₑᵤ, wGOC) + + return dissolution - consumption +end diff --git a/src/Models/AdvectedPopulations/PISCES/silicon_in_particles.jl b/src/Models/AdvectedPopulations/PISCES/silicon_in_particles.jl index 22e21d23b..85b713a26 100644 --- a/src/Models/AdvectedPopulations/PISCES/silicon_in_particles.jl +++ b/src/Models/AdvectedPopulations/PISCES/silicon_in_particles.jl @@ -1,47 +1,52 @@ +@inline function (poc::TwoCompartementParticulateOrganicMatter)(::Val{:PSi}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + # diatom grazing + gZ = diatom_grazing(bgc.microzooplankton, P, D, Z, POC, T) + gM = diatom_grazing(bgc.mesozooplankton, P, D, Z, POC, T) + + grazing = (gZ * Z + gM * M) * DSi / (D + eps(0.0)) + + # diatom mortality + diatom_linear_mortality, diatom_quadratic_mortality = mortality(bgc.diatoms, bgc, z, D, DChl, DFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + diatom_mortality = (diatom_linear_mortality + diatom_quadratic_mortality) * DSi / (D + eps(0.0)) + + # dissolution + dissolution = particulate_silicate_dissolution(poc, z, PSi, Si, T, zₘₓₗ, zₑᵤ, wGOC) + + return grazing + diatom_mortality - dissolution +end -#This document contains functions for: - #λₚₛᵢ¹ (eq52, parametrisation of dissolution rate of PSi) - #Forcing for PSi (eq51) +@inline function particulate_silicate_dissolution(poc, z, PSi, Si, T, zₘₓₗ, zₑᵤ, wGOC) + λₗ = poc.fast_dissolution_rate_of_silicate + λᵣ = poc.slow_dissolution_rate_of_silicate -#Diatom frustule made from 2 silica phases, which dissolve at different rates. We formulate the proportion of fastest dissolving pahse as a function of depth. -@inline function labile_phase(zₘₓₗ, zₑᵤ, λₚₛᵢˡᵃᵇ, λₚₛᵢʳᵉᶠ, z, bgc) - χ_lab⁰ = bgc.proportion_of_the_most_labile_phase_in_PSi - zₘₐₓ = max(abs(zₘₓₗ), abs(zₑᵤ)) + χ = particulate_silicate_liable_fraction(poc, z, zₘₓₗ, zₑᵤ, wGOC) - if abs(z) <= zₘₐₓ - return χ_lab⁰ - else - return χ_lab⁰*exp(-(λₚₛᵢˡᵃᵇ - λₚₛᵢʳᵉᶠ)*((abs(z)-zₘₐₓ)/(sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) + eps(0.0)))) #eq53 - end -end + λ₀ = χ * λₗ + (1 - χ) * λᵣ -#PSi dissolves to Si. Dissolution rate has following formulation. This is a function of Si and T. -@inline function PSi_dissolution_rate(zₘₓₗ, zₑᵤ, z, T, Si, bgc) - λₚₛᵢˡᵃᵇ = bgc.fast_dissolution_rate_of_PSi #Discrepancy in labelling. Assumed these are λₚₛᵢᶠᵃˢᵗ, λₚₛᵢˢˡᵒʷ from parameter list. - λₚₛᵢʳᵉᶠ = bgc.slow_dissolution_rate_of_PSi - - Si_eq = 10^(6.44 - 968/(T + 273.15)) #eq52 - Siₛₐₜ = (Si_eq - Si)/(Si_eq + eps(0.0)) - λₚₛᵢ = labile_phase(zₘₓₗ, zₑᵤ, λₚₛᵢˡᵃᵇ, λₚₛᵢʳᵉᶠ, z, bgc)*λₚₛᵢˡᵃᵇ + (1 - labile_phase(zₘₓₗ, zₑᵤ, λₚₛᵢˡᵃᵇ, λₚₛᵢʳᵉᶠ, z, bgc))*λₚₛᵢʳᵉᶠ #eq53b + equilibrium_silicate = 10^(6.44 - 968 / (T + 273.15)) + silicate_saturation = (equilibrium_silicate - Si) / equilibrium_silicate - return λₚₛᵢ*(0.225*(1 + T/15)*Siₛₐₜ + 0.775*(((1 + T/400)^4)*Siₛₐₜ)^9) -end + λ = λ₀ * (0.225 * (1 + T/15) * silicate_saturation + 0.775 * ((1 + T/400)^4 * silicate_saturation)^9) -#Forcing for PSi -@inline function (bgc::PISCES)(::Val{:PSi}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - Kₘ = bgc.half_saturation_const_for_mortality - Dissₛᵢ = bgc.dissolution_rate_of_silicon - mᴰ = bgc.phytoplankton_mortality_rate.D + return λ * PSi # assuming the Diss_Si is typo in Aumont 2015, consistent with Aumont 2005 +end - #Also required - τ₀ = bgc.background_shear - τₘₓₗ = bgc.mixed_layer_shear +@inline function particulate_silicate_liable_fraction(poc, z, zₘₓₗ, zₑᵤ, wGOC) + χ₀ = poc.base_liable_silicate_fraction + λₗ = poc.fast_dissolution_rate_of_silicate + λᵣ = poc.slow_dissolution_rate_of_silicate - sh = shear(z, zₘₓₗ, τ₀, τₘₓₗ) - wᴰ = D_quadratic_mortality(D, PO₄, NO₃, NH₄, Si, Dᶜʰˡ, Dᶠᵉ, Si̅, bgc) - θˢⁱᴰ = nutrient_quota(Dˢⁱ, D) + zₘ = min(zₘₓₗ, zₑᵤ) - return (θˢⁱᴰ*grazing_M(P, D, Z, POC, T, bgc)[3]*M + θˢⁱᴰ*grazing_Z(P, D, POC, T, bgc)[3]*Z - + mᴰ*concentration_limitation(D, Kₘ)*Dˢⁱ + sh*wᴰ*D*Dˢⁱ - PSi_dissolution_rate(zₘₓₗ, zₑᵤ, z, T, Si, bgc)*Dissₛᵢ*PSi) #removed θˢⁱᴰ from third term, to conserve silicon -end \ No newline at end of file + return χ₀ * ifelse(z >= zₘ, 1, exp((λₗ - λᵣ) * (zₘ - z) / wGOC)) +end diff --git a/src/Models/AdvectedPopulations/PISCES/update_state.jl b/src/Models/AdvectedPopulations/PISCES/update_state.jl new file mode 100644 index 000000000..c864af135 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/update_state.jl @@ -0,0 +1,18 @@ +function update_biogeochemical_state!(model, bgc::PISCES) + # this should come from utils + #update_mixed_layer_depth!(bgc, model) + + PAR = biogeochemical_auxiliary_fields(model.biogeochemistry.light_attenuation).PAR + + compute_euphotic_depth!(bgc.euphotic_depth, PAR) + + compute_mean_mixed_layer_vertical_diffusivity!(bgc.mean_mixed_layer_vertical_diffusivity, bgc.mixed_layer_depth, model) + + compute_mean_mixed_layer_light!(bgc.mean_mixed_layer_light, bgc.mixed_layer_depth, PAR, model) + + compute_calcite_saturation!(bgc.carbon_chemistry, bgc.calcite_saturation, model) + + #update_silicate_climatology!(bgc, model) + + return nothing +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton.jl index 45a4b8285..0b3443125 100644 --- a/src/Models/AdvectedPopulations/PISCES/zooplankton.jl +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton.jl @@ -1,164 +1,293 @@ -# This document contains functions for: +""" + Zooplankton + +The PISCES zooplankton growth model where each class has preferences +for grazing on nanophytoplankton (P), diatoms (D), microzooplankton (Z), +and particulate organic matter (POC), and can flux feed on sinking +particulates (POC and GOC). + +This model assumes a fixed ratio for all other elements (i.e. N, P, Fe). +""" +@kwdef struct Zooplankton{FT} + temperature_sensetivity :: FT = 1.079 # + maximum_grazing_rate :: FT # 1 / s + + preference_for_nanophytoplankton :: FT # + preference_for_diatoms :: FT # + preference_for_particulates :: FT # + preference_for_zooplankton :: FT # + + food_threshold_concentration :: FT = 0.3 # mmol C / m³ + specific_food_thresehold_concentration :: FT = 0.001 # mmol C / m³ + + grazing_half_saturation :: FT = 20.0 # mmol C / m³ + + maximum_flux_feeding_rate :: FT # m / (mmol C / m³) + + iron_ratio :: FT # μmol Fe / mmol C + + minimum_growth_efficiency :: FT # + non_assililated_fraction :: FT = 0.3 # + + mortality_half_saturation :: FT = 0.2 # mmol C / m³ + quadratic_mortality :: FT # 1 / (mmol C / m³) / s + linear_mortality :: FT # 1 / s + + dissolved_excretion_fraction :: FT = 0.6 # + undissolved_calcite_fraction :: FT # +end + +@inline zooplankton_concentration(::Val{:Z}, Z, M) = Z +@inline zooplankton_concentration(::Val{:M}, Z, M) = M + +@inline function specific_grazing(zoo::Zooplankton, P, D, Z, POC, T) + g₀ = zoo.maximum_grazing_rate + b = zoo.temperature_sensetivity + pP = zoo.preference_for_nanophytoplankton + pD = zoo.preference_for_diatoms + pPOC = zoo.preference_for_particulates + pZ = zoo.preference_for_zooplankton + J = zoo.specific_food_thresehold_concentration + K = zoo.grazing_half_saturation + + food_threshold_concentration = zoo.food_threshold_concentration + + base_grazing_rate = g₀ * b ^ T + + food_availability = pP * P + pD * D + pPOC * POC + pZ * Z + + weighted_food_availability = pP * max(0, P - J) + pD * max(0, D - J) + pPOC * max(0, POC - J) + pZ * max(0, Z - J) + + concentration_limited_grazing = max(0, weighted_food_availability - min(weighted_food_availability / 2, food_threshold_concentration)) + + total_specific_grazing = base_grazing_rate * concentration_limited_grazing / (K + food_availability) + + phytoplankton_grazing = pP * max(0, P - J) * total_specific_grazing / (weighted_food_availability + eps(0.0)) + diatom_grazing = pD * max(0, D - J) * total_specific_grazing / (weighted_food_availability + eps(0.0)) + particulate_grazing = pPOC * max(0, POC - J) * total_specific_grazing / (weighted_food_availability + eps(0.0)) + zooplankton_grazing = pZ * max(0, Z - J) * total_specific_grazing / (weighted_food_availability + eps(0.0)) + + return total_specific_grazing, phytoplankton_grazing, diatom_grazing, particulate_grazing, zooplankton_grazing +end + +@inline function specific_flux_feeding(zoo::Zooplankton, POC, T, w) + g₀ = zoo.maximum_flux_feeding_rate + b = zoo.temperature_sensetivity + + base_flux_feeding_rate = g₀ * b ^ T + + return base_flux_feeding_rate * w * POC +end + +@inline function (zoo::Zooplankton)(val_name::Union{Val{:Z}, Val{:M}}, bgc, + x, y, z, t, + P, D, Z, M, + PChl, DChl, PFe, DFe, DSi, + DOC, POC, GOC, + SFe, BFe, PSi, + NO₃, NH₄, PO₄, Fe, Si, + CaCO₃, DIC, Alk, + O₂, T, S, + zₘₓₗ, zₑᵤ, Si′, Ω, κ, mixed_layer_PAR, wPOC, wGOC, PAR, PAR₁, PAR₂, PAR₃) + + I = zooplankton_concentration(val_name, Z, M) + # grazing - # gross growth efficiency - # Z and M forcing - - # Checked all eqns - # Simplifications possible - # Could simplify eₙᴶ functions - -#Mesozooplankton are grazed by upper trophic levels. Carbon is returned to the system through fecal pellets and respiration. -#Respiration and excretion from upper trophic levels. -@inline function upper_respiration(M, T, bgc) #third term has small magnitude, as mᴹ per day - σᴹ = bgc.non_assimilated_fraction.M - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - mᴹ = bgc.zooplankton_quadratic_mortality.M - bₘ = bgc.temperature_sensitivity_term.M - return (1 - σᴹ - eₘₐₓᴹ)*(1/(1-eₘₐₓᴹ))*mᴹ*(bₘ^T)*M^2 #30b -end - -#Fecal pellets from upper trophic levels -@inline function production_of_fecal_pellets(M, T, bgc) - σᴹ = bgc.non_assimilated_fraction.M - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - mᴹ = bgc.zooplankton_quadratic_mortality.M - bₘ = bgc.temperature_sensitivity_term.M - return σᴹ*mᴹ*(1/(1-eₘₐₓᴹ))*(bₘ^T)*M^2 #30a -end - -#Zooplankton graze on P, D, POC. We return a list of grazing of Z on each prey and a sum of grazing terms. -@inline function grazing_Z(P, D, POC, T, bgc) - pₚᶻ = bgc.preference_for_nanophytoplankton.Z - p_Dᶻ = bgc.preference_for_diatoms.Z - pₚₒᶻ = bgc.preference_for_POC.Z - Jₜₕᵣₑₛₕᶻ = bgc.specific_food_thresholds_for_microzooplankton - Fₜₕᵣₑₛₕᶻ = bgc.food_threshold_for_zooplankton.Z - gₘₐₓᶻ = bgc.max_grazing_rate.Z - K_Gᶻ = bgc.half_saturation_const_for_grazing.Z - b_Z = bgc.temperature_sensitivity_term.Z - - F = pₚᶻ*max(0, P - Jₜₕᵣₑₛₕᶻ) + p_Dᶻ*max(0, D - Jₜₕᵣₑₛₕᶻ) + pₚₒᶻ*max(0, POC - Jₜₕᵣₑₛₕᶻ) - Fₗᵢₘ = max(0, F - min(0.5*F, Fₜₕᵣₑₛₕᶻ)) - - grazing_arg = gₘₐₓᶻ*(b_Z^T)*(Fₗᵢₘ)/((F + eps(0.0))*(K_Gᶻ + pₚᶻ*P + p_Dᶻ*D + pₚₒᶻ*POC + eps(0.0))) - - gₚᶻ = (pₚᶻ*max(0, P - Jₜₕᵣₑₛₕᶻ))*grazing_arg #26a - g_Dᶻ = (p_Dᶻ*max(0, D - Jₜₕᵣₑₛₕᶻ))*grazing_arg #26a - gₚₒᶻ = (pₚₒᶻ*max(0, POC - Jₜₕᵣₑₛₕᶻ))*grazing_arg #26a - ∑gᶻ= gₚᶻ + g_Dᶻ + gₚₒᶻ #Sum grazing rates on each prey species for microzooplankton - - return ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ #eq 26a -end - -#Mesozooplankton graze on P, D, POC, Z. We return a list of grazing of M on each prey and a sum of grazing terms. -@inline function grazing_M(P, D, Z, POC, T, bgc) #eq 26a - pₚᴹ = bgc.preference_for_nanophytoplankton.M - p_Dᴹ = bgc.preference_for_diatoms.M - pₚₒᴹ = bgc.preference_for_POC.M - p_Zᴹ = bgc.preference_for_microzooplankton - Jₜₕᵣₑₛₕᴹ = bgc.specific_food_thresholds_for_mesozooplankton - Fₜₕᵣₑₛₕᴹ = bgc.food_threshold_for_zooplankton.M - gₘₐₓᴹ = bgc.max_grazing_rate.M - K_Gᴹ = bgc.half_saturation_const_for_grazing.M - bₘ = bgc.temperature_sensitivity_term.M - - F = pₚᴹ*max(0, P - Jₜₕᵣₑₛₕᴹ) + p_Dᴹ*max(0, D - Jₜₕᵣₑₛₕᴹ) + pₚₒᴹ*max(0, POC - Jₜₕᵣₑₛₕᴹ) + p_Zᴹ*max(0, Z - Jₜₕᵣₑₛₕᴹ) - Fₗᵢₘ = max(0, F - min(0.5*F, Fₜₕᵣₑₛₕᴹ)) + total_specific_grazing, gP, gD, gPOC, gZ = specific_grazing(zoo, P, D, Z, POC, T) - grazing_arg = gₘₐₓᴹ*(bₘ^T)*(Fₗᵢₘ)/((F + eps(0.0))*(K_Gᴹ + pₚᴹ*P + p_Dᴹ*D + pₚₒᴹ*POC + p_Zᴹ*Z + eps(0.0))) + grazing = total_specific_grazing * I - gₚᴹ = (pₚᴹ*max(0, P - Jₜₕᵣₑₛₕᴹ))*grazing_arg #26a - g_Dᴹ = (p_Dᴹ*max(0, D - Jₜₕᵣₑₛₕᴹ))*grazing_arg #26a - gₚₒᴹ = (pₚₒᴹ*max(0, POC - Jₜₕᵣₑₛₕᴹ))*grazing_arg #26a - g_Zᴹ = (p_Zᴹ*max(0, Z - Jₜₕᵣₑₛₕᴹ))*grazing_arg #26a - ∑gᴹ = gₚᴹ + g_Dᴹ + gₚₒᴹ + g_Zᴹ #Sum grazing rates on each prey species for mesozooplankton - - return ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ + # flux feeding + small_flux_feeding = specific_flux_feeding(zoo, POC, T, wPOC) + large_flux_feeding = specific_flux_feeding(zoo, GOC, T, wGOC) + + flux_feeding = (small_flux_feeding + large_flux_feeding) * I + + # grazing mortality + specific_grazing_mortality = zooplankton_grazing_mortality(val_name, bgc.mesozooplankton, P, D, Z, POC, T) + + grazing_mortality = specific_grazing_mortality * M + + # mortality + total_mortality = mortality(zoo, bgc, I, O₂, T) + + growth_efficiency = grazing_growth_efficiency(zoo, P, D, PFe, DFe, POC, SFe, total_specific_grazing, gP, gD, gPOC, gZ) + + return growth_efficiency * (grazing + flux_feeding) - grazing_mortality - total_mortality +end + +@inline function nanophytoplankton_grazing(zoo::Zooplankton, P, D, Z, POC, T) + _, g = specific_grazing(zoo, P, D, Z, POC, T) + + return g +end + +@inline function diatom_grazing(zoo::Zooplankton, P, D, Z, POC, T) + _, _, g = specific_grazing(zoo, P, D, Z, POC, T) + + return g end -#GOC has variable sinking speed. -@inline function sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) - zₘₐₓ = max(abs(zₑᵤ), abs(zₘₓₗ)) - w_GOCᵐⁱⁿ = bgc.min_sinking_speed_of_GOC - return w_GOCᵐⁱⁿ + (200/day - w_GOCᵐⁱⁿ)*(max(0, abs(z)-abs(zₘₐₓ)))/(5000) #41b +@inline function particulate_grazing(zoo::Zooplankton, P, D, Z, POC, T) + _, _, _, g = specific_grazing(zoo, P, D, Z, POC, T) + + return g +end + +@inline function zooplankton_grazing(zoo::Zooplankton, P, D, Z, POC, T) + _, _, _, _, g = specific_grazing(zoo, P, D, Z, POC, T) + + return g +end + +@inline zooplankton_grazing_mortality(val_name, zoo, P, D, Z, POC, T) = 0 +@inline zooplankton_grazing_mortality(::Val{:Z}, zoo, P, D, Z, POC, T) = zooplankton_grazing(zoo, P, D, Z, POC, T) + +@inline function dissolved_upper_trophic_respiration_product(zoo, M, T) + γ = zoo.dissolved_excretion_fraction + + R = upper_trophic_respiration_product(zoo, M, T) + + return (1 - γ) * R +end + +@inline function inorganic_upper_trophic_respiration_product(zoo, M, T) + γ = zoo.dissolved_excretion_fraction + + R = upper_trophic_respiration_product(zoo, M, T) + + return γ * R end -#Return flux feeding of mesozooplankton on POC and GOC, as well as a sum of flux feeding. -@inline function flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc) #eq29 - wₚₒ = bgc.sinking_speed_of_POC - g_FF = bgc.flux_feeding_rate - bₘ = bgc.temperature_sensitivity_term.M +@inline function upper_trophic_waste(zoo, M, T) + e₀ = zoo.minimum_growth_efficiency + b = zoo.temperature_sensetivity + m₀ = zoo.quadratic_mortality - w_GOC = sinking_speed_of_GOC(z, zₑᵤ, zₘₓₗ, bgc) + temperature_factor = b^T - gₚₒ_FFᴹ = g_FF*(bₘ^T)*wₚₒ*POC #29a - g_GOC_FFᴹ = g_FF*(bₘ^T)*w_GOC*GOC #29b - ∑g_FFᴹ = g_GOC_FFᴹ + gₚₒ_FFᴹ - return ∑g_FFᴹ, gₚₒ_FFᴹ, g_GOC_FFᴹ + return 1 / (1 - e₀) * m₀ * temperature_factor * M^2 end -#Gross growth efficiency is formulated to be called with either Z or M. However grazing on Z is only relevant for M, so pass zero when computing gross growth efficiency for Z. -@inline function nutrient_quality(gₚᴶ, g_Dᴶ, gₚₒᴶ, g_Zᴹ, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) - θᴺᶜ = bgc.NC_redfield_ratio - θᶠᵉᶻ = bgc.FeC_ratio_of_zooplankton #Assumed the same for both types of zooplankton +@inline upper_trophic_respiration_product(zoo, M, T) = + (1 - zoo.minimum_growth_efficiency - zoo.non_assililated_fraction) * upper_trophic_waste(zoo, M, T) + +@inline upper_trophic_fecal_product(zoo, M, T) = + zoo.non_assililated_fraction * upper_trophic_waste(zoo, M, T) + +@inline function grazing_growth_efficiency(zoo, P, D, PFe, DFe, POC, SFe, g, gP, gD, gPOC, gZ) + θFe = zoo.iron_ratio + e₀ = zoo.minimum_growth_efficiency + σ = zoo.non_assililated_fraction + + iron_grazing = PFe / (P + eps(0.0)) * gP + DFe / (D + eps(0.0)) * gD + SFe / (POC + eps(0.0)) * gPOC + θFe * gZ + + iron_grazing_ratio = iron_grazing / (θFe * g + eps(0.0)) + + food_quality = min(1, iron_grazing_ratio) + + return food_quality * min(e₀, (1 - σ) * iron_grazing_ratio) +end + +@inline function specific_excretion(zoo, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) + σ = zoo.non_assililated_fraction + + total_specific_grazing, gP, gD, gPOC, gZ = specific_grazing(zoo, P, D, Z, POC, T) - ∑ᵢθᴺᴵgᵢᴶ = θᴺᶜ*gₚᴶ + θᴺᶜ*g_Dᴶ + θᴺᶜ*gₚₒᴶ + θᴺᶜ*g_Zᴹ - ∑ᵢθᶠᵉᴵgᵢᴶ = nutrient_quota(Pᶠᵉ, P)*gₚᴶ + nutrient_quota(Dᶠᵉ, D)*g_Dᴶ + nutrient_quota(SFe, POC)*gₚₒᴶ + θᶠᵉᶻ*g_Zᴹ - ∑ᵢgᵢᴶ = gₚᴶ + g_Dᴶ + gₚₒᴶ + g_Zᴹ + small_flux_feeding = specific_flux_feeding(zoo, POC, T, wPOC) + large_flux_feeding = specific_flux_feeding(zoo, GOC, T, wGOC) + + total_specific_flux_feeding = small_flux_feeding + large_flux_feeding + + e = grazing_growth_efficiency(zoo, P, D, PFe, DFe, POC, SFe, total_specific_grazing, gP, gD, gPOC, gZ) + + return (1 - e - σ) * (total_specific_grazing + total_specific_flux_feeding) +end + +@inline specific_dissolved_grazing_waste(zoo, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) = + (1 - zoo.dissolved_excretion_fraction) * specific_excretion(zoo, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) + +@inline specific_inorganic_grazing_waste(zoo, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) = + zoo.dissolved_excretion_fraction * specific_excretion(zoo, P, D, PFe, DFe, Z, POC, GOC, SFe, T, wPOC, wGOC) + +@inline function specific_non_assimilated_waste(zoo, P, D, Z, POC, GOC, T, wPOC, wGOC) + σ = zoo.non_assililated_fraction + + g, = specific_grazing(zoo, P, D, Z, POC, T) + + small_flux_feeding = specific_flux_feeding(zoo, POC, T, wPOC) + large_flux_feeding = specific_flux_feeding(zoo, GOC, T, wGOC) - return min(1, (∑ᵢθᴺᴵgᵢᴶ)/(θᴺᶜ*∑ᵢgᵢᴶ + eps(0.0)), (∑ᵢθᶠᵉᴵgᵢᴶ)/(θᶠᵉᶻ*∑ᵢgᵢᴶ + eps(0.0))) #27a + return σ * (g + small_flux_feeding + large_flux_feeding) end +@inline function specific_non_assimilated_iron_waste(zoo, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) + _, gP, gD, gPOC, gZ = specific_grazing(zoo, P, D, Z, POC, T) -@inline function growth_efficiency(eₘₐₓᴶ, σᴶ, gₚᴶ, g_Dᴶ, gₚₒᴶ, g_Zᴹ, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) + θᶻ = bgc.microzooplankton.iron_ratio + σ = zoo.non_assililated_fraction - θᶠᵉᶻ = bgc.FeC_ratio_of_zooplankton #Assumed the same for both types of zooplankton + small_flux_feeding = specific_flux_feeding(zoo, POC, T, wPOC) + large_flux_feeding = specific_flux_feeding(zoo, GOC, T, wGOC) + + return σ * (gP * PFe / (P + eps(0.0)) + gD * DFe / (D + eps(0.0)) + gPOC * SFe / (POC + eps(0.0)) + gZ * θᶻ + + small_flux_feeding * SFe / (POC + eps(0.0)) + large_flux_feeding * BFe / (GOC + eps(0.0))) +end - ∑ᵢθᶠᵉᴵgᵢᴶ = nutrient_quota(Pᶠᵉ, P)*gₚᴶ + nutrient_quota(Dᶠᵉ, D)*g_Dᴶ + nutrient_quota(SFe, POC)*gₚₒᴶ + θᶠᵉᶻ*g_Zᴹ - ∑ᵢgᵢᴶ = gₚᴶ + g_Dᴶ + gₚₒᴶ + g_Zᴹ +@inline function specific_non_assimilated_iron(zoo, bgc, P, D, PFe, DFe, Z, POC, GOC, SFe, BFe, T, wPOC, wGOC) + θ = zoo.iron_ratio + θᶻ = bgc.microzooplankton.iron_ratio - eₙᴶ = nutrient_quality(gₚᴶ, g_Dᴶ, gₚₒᴶ, g_Zᴹ, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) #27a + σ = zoo.non_assililated_fraction - return eₙᴶ*min(eₘₐₓᴶ, (1 - σᴶ)* (∑ᵢθᶠᵉᴵgᵢᴶ)/(θᶠᵉᶻ*∑ᵢgᵢᴶ + eps(0.0))) #27b + g, gP, gD, gPOC, gZ = specific_grazing(zoo, P, D, Z, POC, T) + + small_flux_feeding = specific_flux_feeding(zoo, POC, T, wPOC) + large_flux_feeding = specific_flux_feeding(zoo, GOC, T, wGOC) + + total_iron_consumed = (gP * PFe / (P + eps(0.0)) + gD * DFe / (D + eps(0.0)) + gZ * θᶻ + + (gPOC + small_flux_feeding) * SFe / (POC + eps(0.0)) + + large_flux_feeding * BFe / (GOC + eps(0.0))) + + + grazing_iron_ratio = (1 - σ) * total_iron_consumed / (g + small_flux_feeding + large_flux_feeding + eps(0.0)) + + growth_efficiency = grazing_growth_efficiency(zoo, P, D, PFe, DFe, POC, SFe, g, gP, gD, gPOC, gZ) * θ + + non_assimilated_iron_ratio = max(0, grazing_iron_ratio - growth_efficiency) + + return non_assimilated_iron_ratio * g end +@inline function specific_calcite_grazing_loss(zoo, P, D, Z, POC, T) + η = zoo.undissolved_calcite_fraction + + _, gP = specific_grazing(zoo, P, D, Z, POC, T) -@inline function (bgc::PISCES)(::Val{:Z}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) #args not correct - #Parameters - mᶻ = bgc.zooplankton_quadratic_mortality.Z - b_Z = bgc.temperature_sensitivity_term.Z - Kₘ = bgc.half_saturation_const_for_mortality - rᶻ = bgc.zooplankton_linear_mortality.Z - eₘₐₓᶻ = bgc.max_growth_efficiency_of_zooplankton.Z - σᶻ = bgc.non_assimilated_fraction.Z + return η * gP +end - #Grazing - ∑gᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ = grazing_Z(P, D, POC, T, bgc) - g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc)[5] +@inline function mortality(zoo::Zooplankton, bgc, I, O₂, T) + b = zoo.temperature_sensetivity + m₀ = zoo.quadratic_mortality + Kₘ = zoo.mortality_half_saturation + r = zoo.linear_mortality - #Gross growth efficiency - eᶻ = growth_efficiency(eₘₐₓᶻ, σᶻ, gₚᶻ, g_Dᶻ, gₚₒᶻ, 0, Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) + temperature_factor = b^T - return (eᶻ*(gₚᶻ + g_Dᶻ + gₚₒᶻ)*Z - g_Zᴹ*M - mᶻ*(b_Z^T)*Z^2 - - rᶻ*(b_Z^T)*(concentration_limitation(Z, Kₘ) + 3*oxygen_conditions(O₂, bgc))*Z) #24 + concentration_factor = I / (I + Kₘ) + + return temperature_factor * I * (m₀ * I + r * (concentration_factor + 3 * anoxia_factor(bgc, O₂))) end -@inline function (bgc::PISCES)(::Val{:M}, x, y, z, t, P, D, Z, M, Pᶜʰˡ, Dᶜʰˡ, Pᶠᵉ, Dᶠᵉ, Dˢⁱ, DOC, POC, GOC, SFe, BFe, PSi, NO₃, NH₄, PO₄, Fe, Si, CaCO₃, DIC, Alk, O₂, T, zₘₓₗ, zₑᵤ, Si̅, D_dust, Ω, κ, PAR, PAR₁, PAR₂, PAR₃) - #Parameters - mᴹ = bgc.zooplankton_quadratic_mortality.M - bₘ = bgc.temperature_sensitivity_term.M - rᴹ = bgc.zooplankton_linear_mortality.M - Kₘ = bgc.half_saturation_const_for_mortality - eₘₐₓᴹ = bgc.max_growth_efficiency_of_zooplankton.M - σᴹ = bgc.non_assimilated_fraction.M +@inline function linear_mortality(zoo::Zooplankton, bgc, I, O₂, T) + b = zoo.temperature_sensetivity + Kₘ = zoo.mortality_half_saturation + r = zoo.linear_mortality - #Grazing - ∑gᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ = grazing_M(P, D, Z, POC, T, bgc) - ∑g_FFᴹ = flux_feeding(z, zₑᵤ, zₘₓₗ, T, POC, GOC, bgc)[1] + temperature_factor = b^T - #Gross growth efficiency - eᴹ = growth_efficiency(eₘₐₓᴹ, σᴹ, gₚᴹ, g_Dᴹ, gₚₒᴹ, g_Zᴹ,Pᶠᵉ, Dᶠᵉ, SFe, P, D, POC, bgc) + concentration_factor = I / (I + Kₘ) - return (eᴹ*(gₚᴹ + g_Dᴹ + gₚₒᴹ + ∑g_FFᴹ + g_Zᴹ)*M - mᴹ*(bₘ^T)*M^2 - - rᴹ*(bₘ^T)*(concentration_limitation(M, Kₘ) + 3*oxygen_conditions(O₂, bgc))*M) #28 -end \ No newline at end of file + return temperature_factor * I * r * (concentration_factor + 3 * anoxia_factor(bgc, O₂)) +end diff --git a/src/Models/CarbonChemistry/calcite_concentration.jl b/src/Models/CarbonChemistry/calcite_concentration.jl index 752f2d58b..8f2ca007f 100644 --- a/src/Models/CarbonChemistry/calcite_concentration.jl +++ b/src/Models/CarbonChemistry/calcite_concentration.jl @@ -75,7 +75,7 @@ function calcite_saturation(cc::CarbonChemistry; upper_pH_bound, lower_pH_bound) - KSP = cc.calcite_solubility(T, S; P) + KSP = cc.calcite_solubility(T+273.15, S; P) # not confident these all have the right units return calcium_ion_concentration * CO₃²⁻ / KSP diff --git a/src/Models/Models.jl b/src/Models/Models.jl index b9977327c..81c5f7cf5 100644 --- a/src/Models/Models.jl +++ b/src/Models/Models.jl @@ -5,7 +5,7 @@ export Sediments export NPZD, NutrientPhytoplanktonZooplanktonDetritus, LOBSTER, - PISCES + PISCES, DepthDependantSinkingSpeed, PrescribedLatitude, ModelLatitude export SLatissima diff --git a/src/Models/Sediments/Sediments.jl b/src/Models/Sediments/Sediments.jl index 044df4956..e8d4e7f28 100644 --- a/src/Models/Sediments/Sediments.jl +++ b/src/Models/Sediments/Sediments.jl @@ -9,7 +9,7 @@ using OceanBioME: Biogeochemistry, BoxModelGrid using Oceananigans using Oceananigans.Architectures: device, architecture, on_architecture using Oceananigans.Utils: launch! -using Oceananigans.Advection: advective_tracer_flux_z +using Oceananigans.Advection: advective_tracer_flux_z, TracerAdvection using Oceananigans.Units: day using Oceananigans.Fields: ConstantField using Oceananigans.Biogeochemistry: biogeochemical_drift_velocity @@ -71,7 +71,8 @@ end @inline sinking_advection(bgc, advection::NamedTuple) = advection[sinking_tracers(bgc)] @inline advection_scheme(advection, val_tracer) = advection -@inline advection_scheme(advection::NamedTuple, val_tracer::Val{T}) where T = advection[T] +@inline advection_scheme(advection::NamedTuple, val_tracer::Val{T}) where T = advection_scheme(advection[T], val_tracer) +@inline advection_scheme(advection::TracerAdvection, val_tracer) = advection.z @inline function sinking_flux(i, j, k, grid, advection, val_tracer::Val{T}, bgc, tracers) where T return - advective_tracer_flux_z(i, j, k, grid, advection_scheme(advection, val_tracer), biogeochemical_drift_velocity(bgc, val_tracer).w, tracers[T]) / diff --git a/src/OceanBioME.jl b/src/OceanBioME.jl index 43ebd5f45..c5b87e0e6 100644 --- a/src/OceanBioME.jl +++ b/src/OceanBioME.jl @@ -4,8 +4,9 @@ between ocean biogeochemistry, carbonate chemistry, and physics. """ module OceanBioME -# Biogeochemistry models +# Biogeochemistry models and useful things export Biogeochemistry, LOBSTER, PISCES, NutrientPhytoplanktonZooplanktonDetritus, NPZD, redfield +export DepthDependantSinkingSpeed, PrescribedLatitude, ModelLatitude # Macroalgae models export SLatissima @@ -170,11 +171,11 @@ Returns the redfield ratio of `tracer_name` from `bgc` when it is constant acros @inline redfield(val_tracer_name, bgc, tracers) = redfield(val_tracer_name, bgc) """ - conserved_tracers(model::UnderlyingBiogeochemicalModel) + conserved_tracers(model::UnderlyingBiogeochemicalModel, args...; kwargs...) Returns the names of tracers which together are conserved in `model` """ -conserved_tracers(model::Biogeochemistry) = conserved_tracers(model.underlying_biogeochemistry) +conserved_tracers(model::Biogeochemistry, args...; kwargs...) = conserved_tracers(model.underlying_biogeochemistry, args...; kwargs...) summary(bgc::Biogeochemistry) = string("Biogeochemical model based on $(summary(bgc.underlying_biogeochemistry))") show(io::IO, model::Biogeochemistry) = @@ -182,9 +183,10 @@ show(io::IO, model::Biogeochemistry) = " Light attenuation: ", summary(model.light_attenuation), "\n", " Sediment: ", summary(model.sediment), "\n", " Particles: ", summary(model.particles), "\n", - " Modifiers: ", summary(model.modifiers)) + " Modifiers: ", modifier_summary(model.modifiers)) -summary(modifiers::Tuple) = tuple([summary(modifier) for modifier in modifiers]) +modifier_summary(modifier) = summary(modifier) +modifier_summary(modifiers::Tuple) = tuple([summary(modifier) for modifier in modifiers]...) include("Utils/Utils.jl") include("Light/Light.jl") diff --git a/src/Utils/negative_tracers.jl b/src/Utils/negative_tracers.jl index 80c384f81..052350577 100644 --- a/src/Utils/negative_tracers.jl +++ b/src/Utils/negative_tracers.jl @@ -86,19 +86,42 @@ function ScaleNegativeTracers(tracers; scalefactors = ones(length(tracers)), inv end """ - ScaleNegativeTracers(model::UnderlyingBiogeochemicalModel; warn = false) + ScaleNegativeTracers(bgc::AbstractBiogeochemistry; warn = false) -Construct a modifier to scale the conserved tracers in `model`. +Construct a modifier to scale the conserved tracers in `bgc` biogeochemistry. If `warn` is true then scaling will raise a warning. """ function ScaleNegativeTracers(bgc::AbstractBiogeochemistry, grid; invalid_fill_value = NaN, warn = false) tracers = conserved_tracers(bgc) + + return ScaleNegativeTracers(tracers, grid; invalid_fill_value, warn) +end + +# for when `conserved_tracers` just returns a tuple of symbols +function ScaleNegativeTracers(tracers, grid; invalid_fill_value = NaN, warn = false) + scalefactors = on_architecture(architecture(grid), ones(length(tracers))) + + return ScaleNegativeTracers(tracers, scalefactors, invalid_fill_value, warn) +end + +function ScaleNegativeTracers(tracers::NTuple{<:Any, Symbol}, grid; invalid_fill_value = NaN, warn = false) scalefactors = on_architecture(architecture(grid), ones(length(tracers))) return ScaleNegativeTracers(tracers, scalefactors, invalid_fill_value, warn) end +# multiple conserved groups +ScaleNegativeTracers(tracers::Tuple, grid;invalid_fill_value = NaN, warn = false) = + tuple(map(tn -> ScaleNegativeTracers(tn, grid; invalid_fill_value, warn), tracers)...) + +function ScaleNegativeTracers(tracers::NamedTuple, grid; invalid_fill_value = NaN, warn = false) + scalefactors = on_architecture(architecture(grid), [tracers.scalefactors...]) + tracer_names = tracers.tracers + + return ScaleNegativeTracers(tracer_names, scalefactors, invalid_fill_value, warn) +end + summary(scaler::ScaleNegativeTracers) = string("Mass conserving negative scaling of $(scaler.tracers)") show(io::IO, scaler::ScaleNegativeTracers) = print(io, string(summary(scaler), "\n", "└── Scalefactors: $(scaler.scalefactors)")) @@ -106,13 +129,15 @@ show(io::IO, scaler::ScaleNegativeTracers) = print(io, string(summary(scaler), " function update_biogeochemical_state!(model, scale::ScaleNegativeTracers) workgroup, worksize = work_layout(model.grid, :xyz) - dev = device(model.grid.architecture) + dev = device(architecture(model)) scale_for_negs_kernel! = scale_for_negs!(dev, workgroup, worksize) - tracers_to_scale = Tuple(model.tracers[tracer_name] for tracer_name in keys(scale.tracers)) + tracers_to_scale = Tuple(model.tracers[tracer_name] for tracer_name in scale.tracers) scale_for_negs_kernel!(tracers_to_scale, scale.scalefactors, scale.invalid_fill_value) + + return nothing end @kernel function scale_for_negs!(tracers, scalefactors, invalid_fill_value) @@ -130,17 +155,13 @@ end end end - t < 0 && (t = invalid_fill_value) + t = ifelse(t < 0, invalid_fill_value, t) for tracer in tracers value = @inbounds tracer[i, j, k] - - if value > 0 - value *= t / p - else - value = 0 - end - @inbounds tracer[i, j, k] = value + new_value = ifelse(!isfinite(value) | (value > 0), value * t / p, 0) + + @inbounds tracer[i, j, k] = new_value end end \ No newline at end of file diff --git a/src/Utils/sinking_velocity_fields.jl b/src/Utils/sinking_velocity_fields.jl index 257ef1b13..3e305ced7 100644 --- a/src/Utils/sinking_velocity_fields.jl +++ b/src/Utils/sinking_velocity_fields.jl @@ -1,10 +1,11 @@ -using Oceananigans.Fields: ZFaceField, AbstractField, location, Center, Face +using Oceananigans.Fields: ZFaceField, AbstractField, location, Center, Face, compute! using Oceananigans.Forcings: maybe_constant_field using Oceananigans.Grids: AbstractGrid import Adapt: adapt_structure, adapt -const valid_sinking_velocity_locations = ((Center, Center, Face), (Nothing, Nothing, Face), (Nothing, Nothing, Nothing)) # nothings for constant fields +# nothings for constant fields +const valid_sinking_velocity_locations = ((Center, Center, Face), (Nothing, Nothing, Face), (Nothing, Nothing, Nothing)) function setup_velocity_fields(drift_speeds, grid::AbstractGrid, open_bottom; smoothing_distance = 2) drift_velocities = [] @@ -20,6 +21,7 @@ function setup_velocity_fields(drift_speeds, grid::AbstractGrid, open_bottom; sm open_bottom || @warn "The sinking velocity provided for $name is a field and therefore `open_bottom=false` can't be enforced automatically" + compute!(w) w_field = w else @warn "Sinking speed provided for $name was not a number or field so may be unsiutable" diff --git a/test/runtests.jl b/test/runtests.jl index 44cde13f6..b127fd242 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,7 @@ include("test_light.jl") include("test_slatissima.jl") include("test_LOBSTER.jl") include("test_NPZD.jl") +include("test_PISCES.jl") include("test_gasexchange_carbon_chem.jl") include("test_sediments.jl") diff --git a/test/test_NPZD.jl b/test/test_NPZD.jl index 3de374dd2..1941c734f 100644 --- a/test/test_NPZD.jl +++ b/test/test_NPZD.jl @@ -4,16 +4,13 @@ using OceanBioME: NutrientPhytoplanktonZooplanktonDetritus using Oceananigans function test_NPZD(grid, sinking, open_bottom) - PAR = CenterField(grid) - + if sinking model = NonhydrostaticModel(;grid, - biogeochemistry = NutrientPhytoplanktonZooplanktonDetritus(;grid, open_bottom), - auxiliary_fields = (; PAR)) + biogeochemistry = NutrientPhytoplanktonZooplanktonDetritus(;grid, open_bottom)) else model = NonhydrostaticModel(;grid, - biogeochemistry = NutrientPhytoplanktonZooplanktonDetritus(;grid, sinking_speeds = NamedTuple()), - auxiliary_fields = (; PAR)) + biogeochemistry = NutrientPhytoplanktonZooplanktonDetritus(;grid, sinking_speeds = NamedTuple())) end # correct tracers and auxiliary fields have been setup, and order has not changed @@ -21,7 +18,7 @@ function test_NPZD(grid, sinking, open_bottom) @test Oceananigans.Biogeochemistry.required_biogeochemical_tracers(model.biogeochemistry) == required_tracers @test all(tracer ∈ keys(model.tracers) for tracer in required_tracers) - @test :PAR ∈ keys(model.auxiliary_fields) + @test :PAR ∈ keys(Oceananigans.Biogeochemistry.biogeochemical_auxiliary_fields(model.biogeochemistry)) # checks model works with zero values time_step!(model, 1.0) diff --git a/test/test_PISCES.jl b/test/test_PISCES.jl new file mode 100644 index 000000000..6dc01c288 --- /dev/null +++ b/test/test_PISCES.jl @@ -0,0 +1,174 @@ +include("dependencies_for_runtests.jl") + +using Oceananigans.Architectures: on_architecture +using Oceananigans.Fields: ConstantField, FunctionField + +using OceanBioME.Models.PISCESModel: SimpleIron, NitrateAmmonia + +const PISCES_INITIAL_VALUES = (P = 7, D = 7, Z = 0.5, M = 0.5, PChl = 1.5, DChl = 1.5, PFe = 35e-3, DFe = 35e-3, DSi = 1, + NO₃ = 6, NH₄ = 1, PO₄ = 1, Fe = 1, Si = 7, CaCO₃ = 10^-3, + DIC = 2200, Alk = 2400, O₂ = 240, T = 10, S = 35) + + +function set_PISCES_initial_values!(tracers; values = PISCES_INITIAL_VALUES) + for (name, field) in pairs(tracers) + if name in keys(values) + set!(field, values[name]) + else + set!(field, 0) + end + end + + return nothing +end + +value(field; indices = (1, 1, 1)) = on_architecture(CPU(), interior(field, indices...))[1] + +function test_PISCES_conservation() # only on CPU please + validation_warning = "This implementation of PISCES is in early development and has not yet been validated" + + grid = BoxModelGrid(; z = -5) + + PAR₁ = ConstantField(100) + PAR₂ = ConstantField(100) + PAR₃ = ConstantField(100) + PAR = ConstantField(300) + + mixed_layer_depth = ConstantField(-10) + euphotic_depth = ConstantField(-10) + euphotic_depth = ConstantField(-10) + mean_mixed_layer_vertical_diffusivity = ConstantField(1) + mean_mixed_layer_light = ConstantField(300) + + light_attenuation = PrescribedPhotosyntheticallyActiveRadiation((; PAR, PAR₁, PAR₂, PAR₃)) + + biogeochemistry = @test_warn validation_warning PISCES(; grid, + sinking_speeds = (POC = 0, GOC = 0), + light_attenuation, + mixed_layer_depth, + euphotic_depth, + mean_mixed_layer_light, + mean_mixed_layer_vertical_diffusivity, + # turn off permanent iron removal and nitrogen fixaiton + iron = SimpleIron(0), + nitrogen = NitrateAmmonia(maximum_fixation_rate = 0.0)) + + model = BoxModel(; grid, biogeochemistry) + + # checks model works with zero values + time_step!(model, 1.0) + + # and that they all return zero + @test all([all(!(name in (:T, :S)) | (Array(interior(values))[1] .== 0)) for (name, values) in pairs(model.fields)]) + + # set some semi-realistic conditions and check conservation + set_PISCES_initial_values!(model.fields) + + time_step!(model, 1.0) + + conserved_tracers = OceanBioME.conserved_tracers(biogeochemistry; ntuple = true) + + total_carbon_tendencies = sum(map(f -> value(f), model.timestepper.Gⁿ[conserved_tracers.carbon])) + total_iron_tendencies = sum([value(model.timestepper.Gⁿ[n]) * sf for (n, sf) in zip(conserved_tracers.iron.tracers, conserved_tracers.iron.scalefactors)]) + total_silicon_tendencies = sum(map(f -> value(f), model.timestepper.Gⁿ[conserved_tracers.silicon])) + total_phosphate_tendencies = sum([value(model.timestepper.Gⁿ[n]) * sf for (n, sf) in zip(conserved_tracers.phosphate.tracers, conserved_tracers.phosphate.scalefactors)]) + total_nitrogen_tendencies = sum([value(model.timestepper.Gⁿ[n]) * sf for (n, sf) in zip(conserved_tracers.nitrogen.tracers, conserved_tracers.nitrogen.scalefactors)]) + + # should these be exactly zero? + @test isapprox(total_carbon_tendencies, 0, atol = 10^-20) + @test isapprox(total_iron_tendencies, 0, atol = 10^-21) + @test isapprox(total_silicon_tendencies, 0, atol = 10^-21) + @test isapprox(total_phosphate_tendencies, 0, atol = 10^-21) + @test isapprox(total_nitrogen_tendencies, 0, atol = 10^-21) + + return nothing +end + +@inline total_light(z) = 3light(z) +@inline light(z) = ifelse(z <= 0, exp(z/10), 2-exp(-z/10)) # so we get a value boundary condition like normal PAR fields +@inline κ_func(z) = ifelse(z > -25, 2, 1) + +function test_PISCES_update_state(arch) + # TODO: implement and test mixed layer depth computaiton elsewhere + + grid = RectilinearGrid(arch, topology = (Flat, Flat, Bounded), size = (10, ), extent = (100, )) + + PAR₁ = PAR₂ = PAR₃ = FunctionField{Center, Center, Center}(light, grid) + PAR = FunctionField{Center, Center, Center}(total_light, grid) + + mixed_layer_depth = ConstantField(-25) + + light_attenuation = PrescribedPhotosyntheticallyActiveRadiation((; PAR, PAR₁, PAR₂, PAR₃)) + + biogeochemistry = PISCES(; grid, + light_attenuation, + mixed_layer_depth) + + closure = ScalarDiffusivity(ν = 1e-5, κ = FunctionField{Center, Center, Center}(κ_func, grid)) + + model = NonhydrostaticModel(; grid, biogeochemistry, closure) # this updates the biogeochemical state + + # checked and at very high resolution this converges to higher tollerance + @test isapprox(on_architecture(CPU(), biogeochemistry.underlying_biogeochemistry.mean_mixed_layer_light)[1, 1, 1], 3 * 10 / 25 * (1 - exp(-25/10)), rtol = 0.1) + @test isapprox(on_architecture(CPU(), biogeochemistry.underlying_biogeochemistry.mean_mixed_layer_vertical_diffusivity)[1, 1, 1], 2, rtol = 0.1) + + # test should be elsewhere + @test on_architecture(CPU(), biogeochemistry.underlying_biogeochemistry.euphotic_depth)[1, 1, 1] ≈ -10 * log(1000) + + @test_nowarn time_step!(model, 1) +end + +function test_PISCES_negativity_protection(arch) + grid = RectilinearGrid(arch, topology = (Flat, Flat, Bounded), size = (10, ), extent = (100, )) + + PAR₁ = PAR₂ = PAR₃ = FunctionField{Center, Center, Center}(light, grid) + PAR = FunctionField{Center, Center, Center}(total_light, grid) + + mixed_layer_depth = ConstantField(-25) + + light_attenuation = PrescribedPhotosyntheticallyActiveRadiation((; PAR, PAR₁, PAR₂, PAR₃)) + + biogeochemistry = PISCES(; grid, + light_attenuation, + mixed_layer_depth, + scale_negatives = true) + + model = NonhydrostaticModel(; grid, biogeochemistry) + + set!(model, P = -1, D = 1, Z = 1, M = 1, DOC = 1, POC = 1, GOC = 1, DIC = 1, CaCO₃ = 1, PO₄ = 1) + + # got rid of the negative + @test on_architecture(CPU(), interior(model.tracers.P, 1, 1, 1))[1] == 0 + + # correctly conserved mass + @test all(map(t -> on_architecture(CPU(), interior(t, 1, 1, 1))[1] ≈ 7/8, model.tracers[(:D, :Z, :M, :DOC, :POC, :GOC, :DIC, :CaCO₃)])) + + # didn't touch the others + @test on_architecture(CPU(), interior(model.tracers.PO₄, 1, 1, 1))[1] == 1 + + # failed to scale silcate since nothing else in its group was available + set!(model, Si = -1, DSi = 0.1) + + @test isnan(on_architecture(CPU(), interior(model.tracers.DSi, 1, 1, 1))[1]) + + # this is actually going to cause failures in conserving other groups because now we'll have less carbon etc from the M and Z... + set!(model, Fe = -1, Z = 1000, M = 0) + + @test on_architecture(CPU(), interior(model.tracers.Fe, 1, 1, 1))[1] == 0 + @test on_architecture(CPU(), interior(model.tracers.Z, 1, 1, 1))[1] ≈ 900 +end + +@testset "PISCES" begin + if architecture isa CPU + @info "Testing PISCES element conservation (C, Fe, P, Si)" + test_PISCES_conservation() + #test_PISCES_box_model() #TODO + end + + @info "Testing PISCES auxiliary field computation and timestepping" + test_PISCES_update_state(architecture) + + test_PISCES_negativity_protection(architecture) + + #test_PISCES_setup(grid) # maybe should test everything works with all the different bits??? +end \ No newline at end of file diff --git a/validation/PISCES/box.jl b/validation/PISCES/box.jl index 5ce2b0517..777d9e630 100644 --- a/validation/PISCES/box.jl +++ b/validation/PISCES/box.jl @@ -13,40 +13,68 @@ # ## Model setup # Load the packages and setup the initial and forcing conditions using OceanBioME, Oceananigans, Oceananigans.Units -using Oceananigans.Fields: FunctionField + +using Oceananigans.Fields: FunctionField, ConstantField + +using OceanBioME.Models.PISCESModel: SimpleIron, NitrateAmmonia const year = years = 365day -nothing #hide -grid = RectilinearGrid( topology = (Flat, Flat, Flat), size = (), z = 0) +grid = RectilinearGrid( topology = (Flat, Flat, Flat), size = (), z = -10) clock = Clock(time = zero(grid)) # This is forced by a prescribed time-dependent photosynthetically available radiation (PAR) -PAR_func(t) = 60 * (1 - cos((t + 15days) * 2π / year)) * (1 / (1 + 0.2 * exp(-((mod(t, year) - 200days) / 50days)^2))) + 2 -MLD(t) = - 100 -EU(t) = - 50 -PAR_func1(t) = PAR_func(t)/3 -PAR_func2(t) = PAR_func(t)/3 -PAR_func3(t)= PAR_func(t)/3 -#Each PAR component must sum to the initial PAR function at the surface, and we are only considering the 0 layer - -PAR₁ = FunctionField{Center, Center, Center}(PAR_func1, grid; clock) -PAR₂ = FunctionField{Center, Center, Center}(PAR_func2, grid; clock) -PAR₃ = FunctionField{Center, Center, Center}(PAR_func3, grid; clock) -zₘₓₗ = FunctionField{Center, Center, Center}(MLD, grid; clock) -zₑᵤ = FunctionField{Center, Center, Center}(EU, grid; clock) -nothing #hide +@inline surface_PAR(t) = 300 * (1 - cos((t + 15days) * 2π / year)) * (1 / (1 + 0.2 * exp(-((mod(t, year) - 200days) / 50days)^2))) + 10 +@inline temp(t) = 2.4 * (1 + cos(t * 2π / year + 50days)) + 8 + +@inline PAR_component(t) = surface_PAR(t) * exp(-10/2) / 3 + +PAR₁ = FunctionField{Nothing, Nothing, Center}(PAR_component, grid; clock) +PAR₂ = FunctionField{Nothing, Nothing, Center}(PAR_component, grid; clock) +PAR₃ = FunctionField{Nothing, Nothing, Center}(PAR_component, grid; clock) + +PAR = PAR₁ + PAR₂ + PAR₃ + +light_attenuation = PrescribedPhotosyntheticallyActiveRadiation((; PAR, PAR₁, PAR₂, PAR₃)) + +mixed_layer_depth = ConstantField(-100) +euphotic_depth = ConstantField(-100) +mean_mixed_layer_vertical_diffusivity = ConstantField(1e-2) +mean_mixed_layer_light = PAR +silicate_climatology = ConstantField(0) # turn off the contribution from enhanced requirments + +biogeochemistry = PISCES(; grid, + sinking_speeds = (POC = 0, GOC = 0), + light_attenuation, + mixed_layer_depth, + euphotic_depth, + silicate_climatology, + mean_mixed_layer_light, + mean_mixed_layer_vertical_diffusivity, + iron = SimpleIron(0), + nitrogen = NitrateAmmonia(maximum_fixation_rate = 0.0)) # Set up the model. Here, first specify the biogeochemical model, followed by initial conditions and the start and end times -model = BoxModel(; biogeochemistry = PISCES(; grid, light_attenuation_model = PrescribedPhotosyntheticallyActiveRadiation((; PAR, PAR₁, PAR₂, PAR₃)), - mixed_layer_depth = zₘₓₗ, euphotic_layer_depth = zₑᵤ), - clock) +model = BoxModel(; grid, biogeochemistry, clock, prescribed_tracers = (; T = temp)) + +set!(model, P = 6.95, D = 6.95, Z = 0.695, M = 0.695, + PChl = 1.671, DChl = 1.671, + PFe = 6.95/7, DFe = 6.95/7, + DSi = 1.162767, + NO₃ = 6.202, NH₄ = 0.25*6.202, + PO₄ = 0.8722, Fe = 1.256, Si = 7.313, + CaCO₃ = 100, + DIC = 2139.0, Alk = 2366.0, + O₂ = 237.0, S = 35, T = 10) -set!(model, P = 6.95, D = 6.95, Z = 0.695, M = 0.695, Pᶜʰˡ = 1.671, Dᶜʰˡ = 1.671, Pᶠᵉ =7e-6 * 1e9 / 1e6 * 6.95, Dᶠᵉ = 7e-6 * 1e9 / 1e6 * 6.95, Dˢⁱ = 1.162767, DOC = 0.0, POC = 0.0, GOC = 0.0, SFe = 7e-6 * 1e9 / 1e6 *1.256, BFe =7e-6 * 1e9 / 1e6 *1.256, NO₃ = 6.202, NH₄ = 0.25*6.202, PO₄ = 0.8722, Fe = 1.256, Si = 7.313, CaCO₃ = 0.29679635764590534, DIC = 2139.0, Alk = 2366.0, O₂ = 237.0, T = 14.0) #Using Copernicus Data (26.665, 14.) + +simulation = Simulation(model; Δt = 20minutes, stop_time = 4years) -simulation = Simulation(model; Δt = 90minutes, stop_time =5years) +simulation.output_writers[:fields] = JLD2OutputWriter(model, model.fields; filename = "box.jld2", schedule = TimeInterval(0.5day), overwrite_existing = true) + +PAR_field = Field(biogeochemistry.light_attenuation.fields[1]) +simulation.output_writers[:par] = JLD2OutputWriter(model, (; PAR = PAR_field); filename = "box_light.jld2", schedule = TimeInterval(0.5day), overwrite_existing = true) -simulation.output_writers[:fields] = JLD2OutputWriter(model, model.fields; filename = "box.jld2", schedule = TimeInterval(10day), overwrite_existing = true) prog(sim) = @info "$(prettytime(time(sim))) in $(prettytime(simulation.run_wall_time))" @@ -63,54 +91,34 @@ function non_zero_fields!(model) return nothing end -simulation.callbacks[:progress] = Callback(prog, IterationInterval(100)) -simulation.callbacks[:non_zero_fields] = Callback(non_zero_fields!, callsite = UpdateStateCallsite()) - +simulation.callbacks[:progress] = Callback(prog, TimeInterval(182days)) +#simulation.callbacks[:non_zero_fields] = Callback(non_zero_fields!, callsite = UpdateStateCallsite()) @info "Running the model..." run!(simulation) -# ## Load the output +# load and plot results +timeseries = FieldDataset("box.jld2") -times = FieldTimeSeries("box.jld2", "P").times +times = timeseries.fields["P"].times -timeseries = NamedTuple{keys(model.fields)}(FieldTimeSeries("box.jld2", "$field")[1, 1, 1, :] for field in keys(model.fields)) +PAR_timeseries = FieldTimeSeries("box_light.jld2", "PAR") -# ## And plot using CairoMakie fig = Figure(size = (2400, 3600), fontsize = 24) axs = [] -for (name, tracer) in pairs(timeseries) + +n_start = 731 + +for name in Oceananigans.Biogeochemistry.required_biogeochemical_tracers(biogeochemistry) idx = (length(axs)) push!(axs, Axis(fig[floor(Int, idx/4), Int(idx%4)], ylabel = "$name", xlabel = "years", xticks=(0:40))) - lines!(axs[end], times / year, tracer, linewidth = 3) + lines!(axs[end], times[n_start:end] / year, timeseries["$name"][n_start:end], linewidth = 3) end -#Plotting the function of PAR -push!(axs, Axis(fig[6,1], ylabel = "PAR", xlabel = "years", xticks=(0:40))) -lines!(axs[end], (0:10day:5years) / year, x -> PAR_func(x * year), linewidth = 3) - -#Below is merely a tester to check that values of things are conserved, these can be removed in the test files -fi = length(timeseries.P) - -Carbon_at_start = timeseries.P[1] + timeseries.D[1] + timeseries.Z[1] + timeseries.M[1] + timeseries.DOC[1] + timeseries.POC[1] + timeseries.GOC[1] + timeseries.DIC[1] + timeseries.CaCO₃[1] -Carbon_at_end = timeseries.P[fi] + timeseries.D[fi] + timeseries.Z[fi] + timeseries.M[fi] + timeseries.DOC[fi] + timeseries.POC[fi] + timeseries.GOC[fi] + timeseries.DIC[fi] + timeseries.CaCO₃[fi] -Iron_at_start = 10e-3*timeseries.Z[1] + 10e-3*timeseries.M[1] + timeseries.Pᶠᵉ[1] + timeseries.Dᶠᵉ[1] + timeseries.Fe[1] + timeseries.BFe[1] + timeseries.SFe[1] -Iron_at_end = 10e-3*timeseries.Z[fi] + 10e-3*timeseries.M[fi] + timeseries.Pᶠᵉ[fi] + timeseries.Dᶠᵉ[fi] + timeseries.Fe[fi] + timeseries.BFe[fi] + timeseries.SFe[fi] -Silicon_at_start = timeseries.Dˢⁱ[1] + timeseries.Si[1] + timeseries.PSi[1] -Silicon_at_End = timeseries.Dˢⁱ[fi] + timeseries.Si[fi] + timeseries.PSi[fi] -Nitrogen_at_start = 16/122*(timeseries.P[1] + timeseries.D[1] + timeseries.Z[1] + timeseries.M[1] + timeseries.DOC[1] + timeseries.POC[1] + timeseries.GOC[1]) + timeseries.NO₃[1] + timeseries.NH₄[1] -Nitrogen_at_end = 16/122*(timeseries.P[fi] + timeseries.D[fi] + timeseries.Z[fi] + timeseries.M[fi] + timeseries.DOC[fi] + timeseries.POC[fi] + timeseries.GOC[fi]) + timeseries.NO₃[fi] + timeseries.NH₄[fi] -Phosphates_at_start = 1/122*(timeseries.P[1] + timeseries.D[1] + timeseries.Z[1] + timeseries.M[1] + timeseries.DOC[1] + timeseries.POC[1] + timeseries.GOC[1]) + timeseries.PO₄[1] -Phosphates_at_end = 1/122*(timeseries.P[fi] + timeseries.D[fi] + timeseries.Z[fi] + timeseries.M[fi] + timeseries.DOC[fi] + timeseries.POC[fi] + timeseries.GOC[fi]) + timeseries.PO₄[fi] - -println("Carbon at start = ", Carbon_at_start, " Carbon at end = ", Carbon_at_end) -println("Iron at start = ", Iron_at_start, " Iron at end = ", Iron_at_end) -println("Silicon at start = ", Silicon_at_start, " Silicon at end = ", Silicon_at_End) -println("Phosphates at start = ", Phosphates_at_start, " Phosphates at end = ", Phosphates_at_end) -println("Nitrogen at start = ", Nitrogen_at_start, " Nitrogen at end = ", Nitrogen_at_end) - -#Display the figure +push!(axs, Axis(fig[6, 2], ylabel = "PAR", xlabel = "years", xticks=(0:40))) +lines!(axs[end], times[n_start:end]/year, PAR_timeseries[n_start:end], linewidth = 3) + fig \ No newline at end of file diff --git a/validation/PISCES/column.jl b/validation/PISCES/column.jl index 46b264edc..0de8efa29 100644 --- a/validation/PISCES/column.jl +++ b/validation/PISCES/column.jl @@ -18,13 +18,15 @@ using OceanBioME, Oceananigans, Printf using Oceananigans.Fields: FunctionField, ConstantField using Oceananigans.Units +using OceanBioME.Sediments: sinking_flux + const year = years = 365days nothing #hide # ## Surface PAR and turbulent vertical diffusivity based on idealised mixed layer depth # Setting up idealised functions for PAR and diffusivity (details here can be ignored but these are typical of the North Atlantic), temperaeture and euphotic layer -@inline PAR⁰(t) = 60 * (1 - cos((t + 15days) * 2π / year)) * (1 / (1 + 0.2 * exp(-((mod(t, year) - 200days) / 50days)^2))) + 2 +@inline PAR⁰(t) = 300 * (1 - cos((t + 15days) * 2π / year)) * (1 / (1 + 0.2 * exp(-((mod(t, year) - 200days) / 50days)^2))) + 10 @inline H(t, t₀, t₁) = ifelse(t₀ < t < t₁, 1.0, 0.0) @@ -56,42 +58,38 @@ biogeochemistry = PISCES(; grid, mixed_layer_depth = zₘₓₗ, mean_mixed_layer_vertical_diffusivity = ConstantField(1e-2), # this is by default computed now surface_photosynthetically_active_radiation = PAR⁰, - carbon_chemistry) + carbon_chemistry)#, + #sinking_speeds = (POC = 2/day, GOC = 50/day)) CO₂_flux = CarbonDioxideGasExchangeBoundaryCondition(; carbon_chemistry) O₂_flux = OxygenGasExchangeBoundaryCondition() -T = FunctionField{Center, Center, Center}(temp, grid; clock) -S = ConstantField(35) - @info "Setting up the model..." -model = NonhydrostaticModel(; grid, - clock, - closure = ScalarDiffusivity(VerticallyImplicitTimeDiscretization(), κ = κ_field), - biogeochemistry, - boundary_conditions = (DIC = FieldBoundaryConditions(top = CO₂_flux), O₂ = FieldBoundaryConditions(top = O₂_flux)), - auxiliary_fields = (; S)) +model = HydrostaticFreeSurfaceModel(; grid, + velocities = PrescribedVelocityFields(), + tracer_advection = TracerAdvection(nothing, nothing, WENOFifthOrder(grid)), + buoyancy = nothing, + clock, + closure = ScalarDiffusivity(VerticallyImplicitTimeDiscretization(), κ = κ_field), + biogeochemistry, + boundary_conditions = (DIC = FieldBoundaryConditions(top = CO₂_flux), O₂ = FieldBoundaryConditions(top = O₂_flux))) @info "Setting initial values..." + + set!(model, P = 6.95, D = 6.95, Z = 0.695, M = 0.695, - Pᶜʰˡ = 1.671, Dᶜʰˡ = 1.671, - Pᶠᵉ =7e-6 * 1e9 / 1e6 * 6.95, Dᶠᵉ = 7e-6 * 1e9 / 1e6 * 6.95, - Dˢⁱ = 1.162767, - DOC = 0.0, POC = 0.0, GOC = 0.0, - SFe = 7e-6 * 1e9 / 1e6 *1.256, BFe =7e-6 * 1e9 / 1e6 *1.256, + PChl = 0.35, DChl = 0.35, + PFe = 6.95*50e-3, DFe = 6.95*50e-3, + DSi = 0.159*6.95, NO₃ = 6.202, NH₄ = 0.25*6.202, - PO₄ = 0.8722, Fe = 1.256, Si = 7.313, + PO₄ = 0.8722, Fe = 0.2, Si = 2, CaCO₃ = 0.001, DIC = 2139.0, Alk = 2366.0, - O₂ = 237.0) #Using Copernicus Data (26.665, 14.), Calcite is not correct, but this is to see it on the graphs + O₂ = 237.0, S = 35, T = 10) -# ## Simulation -# Next we setup a simulation and add some callbacks that: -# - Show the progress of the simulation -# - Store the model and particles output - -simulation = Simulation(model, Δt = 50minutes, stop_time = 2years) +# maybe get to 1.5hours after initial stuff +simulation = Simulation(model, Δt = 1.5hours, stop_time = 10years) progress_message(sim) = @printf("Iteration: %04d, time: %s, Δt: %s, wall time: %s\n", iteration(sim), @@ -136,8 +134,11 @@ simulation.output_writers[:tracers] = JLD2OutputWriter(model, model.tracers, schedule = TimeInterval(1day), overwrite_existing = true) +PAR = Field(Oceananigans.Biogeochemistry.biogeochemical_auxiliary_fields(biogeochemistry.light_attenuation).PAR) + internal_fields = (; biogeochemistry.underlying_biogeochemistry.calcite_saturation, biogeochemistry.underlying_biogeochemistry.euphotic_depth, + PAR )#biogeochemistry.underlying_biogeochemistry.mean_mixed_layer_vertical_diffusivity) simulation.output_writers[:internals] = JLD2OutputWriter(model, internal_fields, @@ -164,51 +165,65 @@ times = tracers["P"].times air_sea_CO₂_flux = zeros(length(times)) carbon_export = zeros(length(times)) +S = ConstantField(35) + using Oceananigans.Biogeochemistry: biogeochemical_drift_velocity for (n, t) in enumerate(times) - clock.time = t - compute!(T) - - air_sea_CO₂_flux[n] = CO₂_flux.condition.func(1, 1, grid, clock, (; DIC = tracers["DIC"][n], Alk = tracers["Alk"][n], T, S)) - POC = interior(tracers["POC"][n], 1, 1, grid.Nz-20)[1] - wPOC = biogeochemical_drift_velocity(model.biogeochemistry, Val(:POC)).w[1, 1, grid.Nz-20] + k_export = floor(Int, grid.Nz + MLD(t)/minimum_zspacing(grid)) + + air_sea_CO₂_flux[n] = CO₂_flux.condition.func(1, 1, grid, clock, (; DIC = tracers["DIC"][n], Alk = tracers["Alk"][n], T = tracers["T"][n], S)) - GOC = interior(tracers["GOC"][n], 1, 1, grid.Nz-20)[1] - wGOC = biogeochemical_drift_velocity(model.biogeochemistry, Val(:GOC)).w[1, 1, grid.Nz-20] + POC_export = -sinking_flux(1, 1, k_export, grid, model.advection.POC.z, Val(:POC), biogeochemistry, (; POC = tracers["POC"][n])) + GOC_export = -sinking_flux(1, 1, k_export, grid, model.advection.GOC.z, Val(:GOC), biogeochemistry, (; GOC = tracers["GOC"][n])) - carbon_export[n] = (POC * wPOC + GOC * wGOC) + carbon_export[n] = POC_export + GOC_export end using CairoMakie fig = Figure(size = (4000, 2100), fontsize = 20); -axis_kwargs = (xlabel = "Time (days)", ylabel = "z (m)", limits = ((180, times[end] / days), (-400, 0))) +start_day = 366 +end_day = 3651 + +axis_kwargs = (xlabel = "Time (days)", ylabel = "z (m)", limits = ((start_day, times[end_day] / days), (-200, 0))) for (n, name) in enumerate(keys(model.tracers)) - i = floor(Int, (n-1)/4) + 1 - j = mod(2 * (n-1), 8) + 1 - ax = Axis(fig[i, j]; title = "$name", axis_kwargs...) - hm = heatmap!(ax, times[180:end]./days, z, interior(tracers["$name"], 1, 1, :, 180:731)') - Colorbar(fig[i, j+1], hm) - lines!(ax, times[180:end]./days, t->MLD(t*day), linewidth = 2, color = :black, linestyle = :dash) - lines!(ax, times[180:end]./days, interior(internal_fields["euphotic_depth"], 1, 1, 1, 180:731), linewidth = 2, color = :white, linestyle = :dash) + if !(name == :S) + i = floor(Int, (n-1)/4) + 1 + j = mod(2 * (n-1), 8) + 1 + ax = Axis(fig[i, j]; title = "$name", axis_kwargs...) + hm = heatmap!(ax, times[start_day:end]./days, z, interior(tracers["$name"], 1, 1, :, start_day:end_day)') + Colorbar(fig[i, j+1], hm) + lines!(ax, times[start_day:end_day]./days, t->MLD(t*day), linewidth = 2, color = :black, linestyle = :dash) + lines!(ax, times[start_day:end_day]./days, interior(internal_fields["euphotic_depth"], 1, 1, 1, start_day:end_day), linewidth = 2, color = :white, linestyle = :dash) + end end -ax = Axis(fig[7, 3]; title = "log10(Calcite saturation), looks temperature dominated", axis_kwargs...) -hm = heatmap!(ax, times[180:end]./days, z, log10.(interior(internal_fields["calcite_saturation"], 1, 1, :, 180:731)'), colorrange = (-173, -95)) +ax = Axis(fig[7, 3]; title = "log₁₀((Calcite saturation)", axis_kwargs...) +hm = heatmap!(ax, times[start_day:end_day]./days, z, log10.(interior(internal_fields["calcite_saturation"], 1, 1, :, start_day:end_day)'), colorrange = (-173, -95)) Colorbar(fig[7, 4], hm) +ax = Axis(fig[7, 5]; title = "log₁₀(PAR)", axis_kwargs...) +hm = heatmap!(ax, times[start_day:end_day]./days, z, log10.(interior(internal_fields["PAR"], 1, 1, :, start_day:end_day)')) +Colorbar(fig[7, 6], hm) + CO₂_molar_mass = (12 + 2 * 16) * 1e-3 # kg / mol -axDIC = Axis(fig[7, 5], xlabel = "Time (days)", ylabel = "Flux (kgCO₂/m²/year)", +axDIC = Axis(fig[7, 7], xlabel = "Time (days)", ylabel = "Flux (kgCO₂/m²/year)", title = "Air-sea CO₂ flux and Sinking", limits = ((0, times[end] / days), nothing)) -lines!(axDIC, times[180:731] / days, air_sea_CO₂_flux[180:731] / 1e3 * CO₂_molar_mass * year, linewidth = 3, label = "Air-sea flux") -lines!(axDIC, times[180:731] / days, carbon_export[180:731] / 1e3 * CO₂_molar_mass * year, linewidth = 3, label = "Sinking export") -Legend(fig[7, 6], axDIC, framevisible = false) +lines!(axDIC, times[start_day:end_day] / days, air_sea_CO₂_flux[start_day:end_day] / 1e3 * CO₂_molar_mass * year, linewidth = 3, label = "Air-sea flux") +lines!(axDIC, times[start_day:end_day] / days, carbon_export[start_day:end_day] / 1e3 * CO₂_molar_mass * year, linewidth = 3, label = "Sinking below mixed layer") +Legend(fig[7, 8], axDIC, framevisible = false) fig + + +# TODO: +# - aggregation +# - check flux feeding +# - Si looks like its erroniously growing