diff --git a/Manifest.toml b/Manifest.toml index 4a090ca54..04ea25831 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,38 +2,45 @@ julia_version = "1.10.2" manifest_format = "2.0" -project_hash = "37e11a3bdd396c973917441db34ded05b97faa85" +project_hash = "90c0672acfd9ac1461839f7013be62ced327f62a" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" version = "1.5.0" -weakdeps = ["ChainRulesCore", "Test"] [deps.AbstractFFTs.extensions] AbstractFFTsChainRulesCoreExt = "ChainRulesCore" AbstractFFTsTestExt = "Test" + [deps.AbstractFFTs.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + [[deps.Accessors]] -deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "MacroTools", "Markdown", "Test"] -git-tree-sha1 = "f61b15be1d76846c0ce31d3fcfac5380ae53db6a" +deps = ["CompositionsBase", "ConstructionBase", "InverseFunctions", "LinearAlgebra", "MacroTools", "Markdown"] +git-tree-sha1 = "b392ede862e506d451fc1616e79aa6f4c673dab8" uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" -version = "0.1.37" +version = "0.1.38" [deps.Accessors.extensions] AccessorsAxisKeysExt = "AxisKeys" + AccessorsDatesExt = "Dates" AccessorsIntervalSetsExt = "IntervalSets" AccessorsStaticArraysExt = "StaticArrays" AccessorsStructArraysExt = "StructArrays" + AccessorsTestExt = "Test" AccessorsUnitfulExt = "Unitful" [deps.Accessors.weakdeps] AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5" + Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" Requires = "ae029012-a4dd-5104-9daa-d747884805df" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [[deps.Adapt]] @@ -92,10 +99,10 @@ uuid = "179af706-886a-5703-950a-314cd64e0468" version = "0.1.3" [[deps.CUDA]] -deps = ["AbstractFFTs", "Adapt", "BFloat16s", "CEnum", "CUDA_Driver_jll", "CUDA_Runtime_Discovery", "CUDA_Runtime_jll", "Crayons", "DataFrames", "ExprTools", "GPUArrays", "GPUCompiler", "KernelAbstractions", "LLVM", "LLVMLoopInfo", "LazyArtifacts", "Libdl", "LinearAlgebra", "Logging", "NVTX", "Preferences", "PrettyTables", "Printf", "Random", "Random123", "RandomNumbers", "Reexport", "Requires", "SparseArrays", "StaticArrays", "Statistics"] -git-tree-sha1 = "fdd9dfb67dfefd548f51000cc400bb51003de247" +deps = ["AbstractFFTs", "Adapt", "BFloat16s", "CEnum", "CUDA_Driver_jll", "CUDA_Runtime_Discovery", "CUDA_Runtime_jll", "Crayons", "DataFrames", "ExprTools", "GPUArrays", "GPUCompiler", "KernelAbstractions", "LLVM", "LLVMLoopInfo", "LazyArtifacts", "Libdl", "LinearAlgebra", "Logging", "NVTX", "Preferences", "PrettyTables", "Printf", "Random", "Random123", "RandomNumbers", "Reexport", "Requires", "SparseArrays", "StaticArrays", "Statistics", "demumble_jll"] +git-tree-sha1 = "e0725a467822697171af4dae15cec10b4fc19053" uuid = "052768ef-5323-5732-b1bb-66c8b64840ba" -version = "5.4.3" +version = "5.5.2" [deps.CUDA.extensions] ChainRulesCoreExt = "ChainRulesCore" @@ -109,31 +116,21 @@ version = "5.4.3" [[deps.CUDA_Driver_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "325058b426c2b421e3d2df3d5fa646d72d2e3e7e" +git-tree-sha1 = "ccd1e54610c222fadfd4737dac66bff786f63656" uuid = "4ee394cb-3365-5eb0-8335-949819d2adfc" -version = "0.9.2+0" +version = "0.10.3+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"] -git-tree-sha1 = "afea94249b821dc754a8ca6695d3daed851e1f5a" +git-tree-sha1 = "e43727b237b2879a34391eeb81887699a26f8f2f" uuid = "76a88914-d11a-5bdc-97e0-2f5a05c973a2" -version = "0.14.1+0" - -[[deps.ChainRulesCore]] -deps = ["Compat", "LinearAlgebra"] -git-tree-sha1 = "71acdbf594aab5bbb2cec89b208c41b4c411e49f" -uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.24.0" -weakdeps = ["SparseArrays"] - - [deps.ChainRulesCore.extensions] - ChainRulesCoreSparseArraysExt = "SparseArrays" +version = "0.15.3+0" [[deps.ColorTypes]] deps = ["FixedPointNumbers", "Random"] @@ -183,17 +180,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 +200,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" @@ -213,10 +211,10 @@ uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" version = "1.16.0" [[deps.DataFrames]] -deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "REPL", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] -git-tree-sha1 = "04c738083f29f86e62c8afc341f0967d8717bdb8" +deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] +git-tree-sha1 = "fb61b4812c49343d7ef0b533ba982c46021938a6" uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -version = "1.6.1" +version = "1.7.0" [[deps.DataStructures]] deps = ["Compat", "InteractiveUtils", "OrderedCollections"] @@ -244,12 +242,15 @@ deps = ["LinearAlgebra", "Statistics", "StatsAPI"] git-tree-sha1 = "66c4c81f259586e8f002eacebc177e1fb06363b0" uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" version = "0.10.11" -weakdeps = ["ChainRulesCore", "SparseArrays"] [deps.Distances.extensions] DistancesChainRulesCoreExt = "ChainRulesCore" DistancesSparseArraysExt = "SparseArrays" + [deps.Distances.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + [[deps.Distributed]] deps = ["Random", "Serialization", "Sockets"] uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" @@ -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" @@ -283,9 +279,9 @@ version = "1.8.0" [[deps.FFTW_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] -git-tree-sha1 = "c6033cc3892d0ef5bb9cd29b7f2f0331ea5184ea" +git-tree-sha1 = "4d81ed14783ec49ce9f2e168208a12ce1815aa25" uuid = "f5851436-0d7a-5f13-b9de-f02708fd171a" -version = "3.3.10+0" +version = "3.3.10+1" [[deps.FileIO]] deps = ["Pkg", "Requires", "UUIDs"] @@ -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"] @@ -324,10 +320,10 @@ uuid = "46192b85-c4d5-4398-a991-12ede77f4527" version = "0.1.6" [[deps.GPUCompiler]] -deps = ["ExprTools", "InteractiveUtils", "LLVM", "Libdl", "Logging", "Preferences", "Scratch", "Serialization", "TOML", "TimerOutputs", "UUIDs"] -git-tree-sha1 = "ab29216184312f99ff957b32cd63c2fe9c928b91" +deps = ["ExprTools", "InteractiveUtils", "LLVM", "Libdl", "Logging", "PrecompileTools", "Preferences", "Scratch", "Serialization", "TOML", "TimerOutputs", "UUIDs"] +git-tree-sha1 = "1d6f290a5eb1201cd63574fbc4440c788d5cb38f" uuid = "61eb1bfa-7361-4325-ad38-22787b887f55" -version = "0.26.7" +version = "0.27.8" [[deps.GibbsSeaWater]] deps = ["GibbsSeaWater_jll", "Libdl", "Test"] @@ -360,9 +356,9 @@ version = "1.14.2+1" [[deps.Hwloc_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "5e19e1e4fa3e71b774ce746274364aef0234634e" +git-tree-sha1 = "dd3b49277ec2bb2c6b94eb1604d4d0616016f7a6" uuid = "e33a78d0-f292-5ffc-b300-72abe9b543c8" -version = "2.11.1+0" +version = "2.11.2+0" [[deps.IncompleteLU]] deps = ["LinearAlgebra", "SparseArrays"] @@ -384,19 +380,19 @@ 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"] uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" [[deps.InverseFunctions]] -git-tree-sha1 = "2787db24f4e03daf859c6509ff87764e4182f7d1" +git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb" uuid = "3587e190-3f89-42d0-90ee-14403ec27112" -version = "0.1.16" +version = "0.1.17" weakdeps = ["Dates", "Test"] [deps.InverseFunctions.extensions] @@ -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"] @@ -444,9 +435,9 @@ version = "0.2.1+0" [[deps.KernelAbstractions]] deps = ["Adapt", "Atomix", "InteractiveUtils", "MacroTools", "PrecompileTools", "Requires", "StaticArrays", "UUIDs", "UnsafeAtomics", "UnsafeAtomicsLLVM"] -git-tree-sha1 = "35ceea58aa34ad08b1ae00a52622c62d1cfb8ce2" +git-tree-sha1 = "5126765c5847f74758c411c994312052eb7117ef" uuid = "63c18a36-062a-441e-b654-da1e3ab1ce7c" -version = "0.9.24" +version = "0.9.27" [deps.KernelAbstractions.extensions] EnzymeExt = "EnzymeCore" @@ -460,9 +451,9 @@ version = "0.9.24" [[deps.LLVM]] deps = ["CEnum", "LLVMExtra_jll", "Libdl", "Preferences", "Printf", "Requires", "Unicode"] -git-tree-sha1 = "2470e69781ddd70b8878491233cd09bc1bd7fc96" +git-tree-sha1 = "4ad43cb0a4bb5e5b1506e1d1f48646d7e0c80363" uuid = "929cbde3-209d-540e-8aea-75f648917ca0" -version = "8.1.0" +version = "9.1.2" weakdeps = ["BFloat16s"] [deps.LLVM.extensions] @@ -470,9 +461,9 @@ weakdeps = ["BFloat16s"] [[deps.LLVMExtra_jll]] deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl", "TOML"] -git-tree-sha1 = "597d1c758c9ae5d985ba4202386a607c675ee700" +git-tree-sha1 = "05a8bd5a42309a9ec82f700876903abce1017dd3" uuid = "dad2f222-ce93-54a1-a47d-0025e8a3acab" -version = "0.0.31+0" +version = "0.0.34+0" [[deps.LLVMLoopInfo]] git-tree-sha1 = "2e5c102cfc41f48ae4740c7eca7743cc7e7b75ea" @@ -481,9 +472,9 @@ version = "1.0.0" [[deps.LLVMOpenMP_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "e16271d212accd09d52ee0ae98956b8a05c4b626" +git-tree-sha1 = "78211fb6cbc872f77cad3fc0b6cf647d923f4929" uuid = "1d63c593-3942-5779-bab2-d838dc0a180e" -version = "17.0.6+0" +version = "18.1.7+0" [[deps.LRUCache]] git-tree-sha1 = "b3cc6698599b10e652832c2f23db3cab99d51b59" @@ -540,30 +531,14 @@ 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" [[deps.Lz4_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "7f26c8fc5229e68484e0b3447312c98e16207d11" +git-tree-sha1 = "abf88ff67f4fd89839efcae2f4c39cbc4ecd0846" uuid = "5ced341a-0733-55b8-9ab6-a4889d929147" -version = "1.10.0+0" +version = "1.10.0+1" [[deps.MKL_jll]] deps = ["Artifacts", "IntelOpenMP_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "oneTBB_jll"] @@ -573,9 +548,9 @@ version = "2024.2.0+0" [[deps.MPI]] deps = ["Distributed", "DocStringExtensions", "Libdl", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "PkgVersion", "PrecompileTools", "Requires", "Serialization", "Sockets"] -git-tree-sha1 = "b4d8707e42b693720b54f0b3434abee6dd4d947a" +git-tree-sha1 = "892676019c58f34e38743bc989b0eca5bce5edc5" uuid = "da04e1cc-30fd-572f-bb4f-1f8673147195" -version = "0.20.16" +version = "0.20.22" [deps.MPI.extensions] AMDGPUExt = "AMDGPU" @@ -587,9 +562,9 @@ version = "0.20.16" [[deps.MPICH_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] -git-tree-sha1 = "19d4bd098928a3263693991500d05d74dbdc2004" +git-tree-sha1 = "7715e65c47ba3941c502bffb7f266a41a7f54423" uuid = "7cb0a576-ebde-5e09-9194-50597f1243b4" -version = "4.2.2+0" +version = "4.2.3+0" [[deps.MPIPreferences]] deps = ["Libdl", "Preferences"] @@ -599,9 +574,9 @@ version = "0.1.11" [[deps.MPItrampoline_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] -git-tree-sha1 = "8c35d5420193841b2f367e658540e8d9e0601ed0" +git-tree-sha1 = "fde81c9f9c94fe5fbeaed7b3f1330305cf9a327c" uuid = "f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748" -version = "5.4.0+0" +version = "5.5.0+0" [[deps.MacroTools]] deps = ["Markdown", "Random"] @@ -639,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"] @@ -673,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 = "12cdd648cc342e52686515811c69bc4bc452a94c" +git-tree-sha1 = "cac4f28778a22404724ba667368f071047b513c1" uuid = "9e8cae18-63c1-5223-a75c-80ca9d6e9a09" -version = "0.91.9" +version = "0.92.0" [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]] @@ -699,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" @@ -712,15 +683,9 @@ version = "5.0.5+0" [[deps.OpenSSL_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "a028ee3cb5641cccc4c24e90c36b0a4f7707bdf5" +git-tree-sha1 = "7493f61f55a6cce7325f197443aa80d32554ba10" 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+1" [[deps.OrderedCollections]] git-tree-sha1 = "dfdf5519f235516220579f949664f1bf44e741c5" @@ -764,20 +729,14 @@ version = "1.4.3" [[deps.PrettyTables]] deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "Reexport", "StringManipulation", "Tables"] -git-tree-sha1 = "66b20dd35966a748321d3b2537c4584cf40387c7" +git-tree-sha1 = "1101cd475833706e4d0e7b122218257178f48f34" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" -version = "2.3.2" +version = "2.4.0" [[deps.Printf]] 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" @@ -828,18 +787,20 @@ uuid = "ae029012-a4dd-5104-9daa-d747884805df" version = "1.3.0" [[deps.Roots]] -deps = ["Accessors", "ChainRulesCore", "CommonSolve", "Printf"] -git-tree-sha1 = "3484138c9fa4296a0cf46a74ca3f97b59d12b1d0" +deps = ["Accessors", "CommonSolve", "Printf"] +git-tree-sha1 = "3a7c7e5c3f015415637f5debdf8a674aa2c979c4" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -version = "2.1.6" +version = "2.2.1" [deps.Roots.extensions] + RootsChainRulesCoreExt = "ChainRulesCore" RootsForwardDiffExt = "ForwardDiff" RootsIntervalRootFindingExt = "IntervalRootFinding" RootsSymPyExt = "SymPy" RootsSymPyPythonCallExt = "SymPyPythonCall" [deps.Roots.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" @@ -866,9 +827,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"] @@ -893,27 +854,20 @@ 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" uuid = "90137ffa-7385-5640-81b9-e52037218182" version = "1.9.7" -weakdeps = ["ChainRulesCore", "Statistics"] [deps.StaticArrays.extensions] StaticArraysChainRulesCoreExt = "ChainRulesCore" StaticArraysStatisticsExt = "Statistics" + [deps.StaticArrays.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + [[deps.StaticArraysCore]] git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" @@ -932,9 +886,9 @@ version = "1.7.0" [[deps.StringManipulation]] deps = ["PrecompileTools"] -git-tree-sha1 = "a04cabe79c5f01f4d723cc6704070ada0b9d46d5" +git-tree-sha1 = "a6b1675a536c5ad1a60e5a5153e1fee12eb146e3" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" -version = "0.3.4" +version = "0.4.0" [[deps.StructArrays]] deps = ["ConstructionBase", "DataAPI", "Tables"] @@ -978,15 +932,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"] @@ -1017,9 +977,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"] @@ -1040,9 +1000,15 @@ version = "1.2.13+1" [[deps.Zstd_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "e678132f07ddb5bfa46857f0d7620fb9be675d3b" +git-tree-sha1 = "555d1076590a6cc2fdee2ef1469451f872d8b41b" uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" -version = "1.5.6+0" +version = "1.5.6+1" + +[[deps.demumble_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "6498e3581023f8e530f34760d18f75a69e3a4ea8" +uuid = "1e29f10c-031c-5a83-9565-69cddfc27673" +version = "1.3.0+0" [[deps.libaec_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] diff --git a/Project.toml b/Project.toml index f420f20f9..f969ab61e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "OceanBioME" uuid = "a49af516-9db8-4be4-be45-1dad61c5a376" authors = ["Jago Strong-Wright and contributors"] -version = "0.11.2" +version = "0.12.0" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" @@ -12,7 +12,7 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" Oceananigans = "9e8cae18-63c1-5223-a75c-80ca9d6e9a09" Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" SeawaterPolynomials = "d496a93d-167e-4197-9f49-d3af4ff8fe40" -StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" + [compat] Adapt = "3, 4" @@ -20,9 +20,9 @@ CUDA = "5" Documenter = "1" EnsembleKalmanProcesses = "1" GibbsSeaWater = "0.1" -JLD2 = "0.4" +JLD2 = "0.4, 0.5" KernelAbstractions = "0.9" -Oceananigans = "0.91" +Oceananigans = "0.91.9, 0.92" Roots = "2" SeawaterPolynomials = "0.3" StructArrays = "0.4, 0.5, 0.6" @@ -40,6 +40,7 @@ JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" NetCDF = "30363a11-5582-574a-97bb-aa9a979735b9" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] 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..d875119ff 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,48 @@ Modules = [OceanBioME.Models.NPZDModel] Modules = [OceanBioME.Models.LOBSTERModel] ``` +### Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.DissolvedOrganicMatter] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.ParticulateOrganicMatter] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Iron] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.InorganicCarbons] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Zooplankton] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Phytoplankton] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Phosphates] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Silicates] +``` + +```@autodocs +Modules = [OceanBioME.Models.PISCESModel.Nitrogen] +``` + ### 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..621a00e6f --- /dev/null +++ b/docs/src/model_components/biogeochemical/PISCES/PISCES.md @@ -0,0 +1,37 @@ +# [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) + +The default configuration of PISCES in OceanBioME is the operational/standard version with 24 tracers and can be set up by writing: + +```@example +using OceanBioME, Oceananigans + +grid = RectilinearGrid(size=(1, 1, 1), extent=(1, 1, 1)); + +biogeochemistry = PISCES(; grid) + +show(biogeochemistry) + +Oceananigans.Biogeochemistry.required_biogeochemical_tracers(biogeochemistry) + +(:P, :PChl, :PFe, :D, :DChl, :DFe, :DSi, :Z, :M, :DOC, :POC, :GOC, :SFe, :BFe, :PSi, :CaCO₃, :NO₃, :NH₄, :PO₄, :Fe, :Si, :DIC, :Alk, :O₂, :T, :S) +``` + +The parametrisations can easily be switched out when the `biogeochemistry` is constructed by setting the key word parameter, see the [API documentation](@ref library_api), although we currently do not have any of the other configurations implemented. Note that `PISCES-simple` is very similar to [`LOBSTER`](@ref LOBSTER) if that is what you are looking for. + +More documentation will follow but for now the equations can be found in [Aumont2015](@citet) read along side our notes [here](@ref PISCES_queries). + +## Model conservation +When the permanent scavenging of iron, nitrogen fixation, and particle sinking 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..3642bc776 --- /dev/null +++ b/docs/src/model_components/biogeochemical/PISCES/notable_differences.md @@ -0,0 +1,53 @@ +# [Notes](@id PISCES_queries) + +While most of the function formula can be found in [Aumont2015](@citet), we have compiled the following list of minor errors in the paper, as well as changes that are present in the [NEMO](https://www.nemo-ocean.eu/) implementation. + +## Preface +This implementation of PISCES varies from NEMO and CROC in a few regards: +- Our standard unit of concentration in mmol / m³ which is equivalent to μmol / L, so we have retained these units all the tracers except iron +- Iron is modelled in μmol / m³ which is equivalent to nmol / L +- In other implementations of PISCES nitrogen is tracked in carbon units (i.e. the concentration of nitrogen divided by the Redfield ratio), we instead opted to track in nitrogen units and so multiply most terms by the Redfield ratio (TODO: check that constants are in the correct units) +- [Aumont2015](@citet) refers to the concentrations in μmol / L and nmol / L the NEMO and CROC source code actually track everything in mol/L, therefore many units were converted, but some were missed (listed below) +- [Aumont2015](@citet) includes the "yearly maximum silicate", `Si′` but it appears that the NEMO source code actually uses the surface silicate in this roll, so we have renamed it to `silicate_climatology` +- Other implementations of PISCES compute the dark residence time (the time spent below the euphotic depth due to mixing within the mixed layer) assuming a constant diffusivity, we replace this with the actual diffusivity (or it can be set to a `ConstantField` to replicate other results) +- We have removed dust from PISCES since it can be implemented as a more generic (and easily more sophisticated) model elsewhere, and doesn't actually appear in PISCES except in the iron scavenging term which would need to be added as a forcing if iron scavenging from dust was desired in a model +- The bacterial remineralisation of DOC is split into the oxic and anoxic parts which are referred to as ``Remin`` and ``Denit``, but we have renamed these as `oxic_remineralisation` and `anoxic_remineralisation` for clarity +- We would also like to note for future developers that ``\theta^{Chl}`` is mg Chl / mg C so needs to be computed as ``IChl / 12I`` + +## Constant disparities +Constant units were sometimes incorrect in [Aumont2015](@citet), all units are now all noted in the code and may vary. +The values vary for: +- Aggregation factors (``a_1, a_2, ...``), found in `TwoCompartementCarbonIronParticles` and `DissolvedOrganicCarbon` `aggregation_parameters`: from the NEMO source code, all of these parameters need a factor of ``10^{-6}`` to convert them from units of 1 / (mol / L) to 1 / (μmol / L). Additionally, all the parameters require a factor of ``1 / 86400``, for the parameters not multiplied by shear this straight forwardly is because they have time units of 1 / day in the NEMO code, but for those multiplied by shear this is because they implicitly have a factor of seconds per day in them to result in an aggregation in mmol C / day +- In a similar vein, the flux feeding rate for zooplankton ``g_{FF}^M`` is in 1 / (m mol / L) where that `m` in `m`eters, so we need to multiply by a factor of ``10^{-6}`` to get 1 / (m μmol / L) +- The fraction of bacterially consumed iron going to small and big particles, ``\kappa^{BFe}_{Bact}`` and ``\kappa^{SFe}_{Bact}``, in equations 48 and 49 are not recorded but from the NEMO source code we can see that they are `0.04` and `0.12` respectively. Additionally, we need to multiply by a factor of `0.16` (``bacterial_iron_uptake_efficiency``) to the drawdown from the iron pool due to the instantaneous return (as per the NEMO version) +- ``\theta_{max}^{Fe, Bact}`` is not recorded so the value `0.06` μmol Fe / mmol C is taken from the NEMO source code +- ``\theta^{Fe, Z}`` and ``\theta^{Fe, M}`` are taken from the NEMO source code to be 0.01 and 0.015 μmol Fe / mmol C +- ``\theta_{max}^{Fe, P}`` is taken from the NEMO source code to be `0.06` μmol Fe / mmol C, we note that this isn't actually the maximum in the sense that the ratio could (probably) go above this value +- ``K^{B, 1}_{Fe}`` is not recorded so the value `0.3` μmol Fe / m³ is taken from the NEMO source code +- ``\eta^Z`` and ``\eta^M`` in equation 76 are incorrectly labelled as ``\nu^I`` in parameter table +- Iron ratios are often given as mol Fe / mol C, so we have converted to μmol Fe / mmol C + +## Equation disparities +- The calcite production limitation, ``L^{CaCO_3}_{lim}`` in equation 77, is not documented. From the NEMO source code it appears to take the form ``L^{CaCO_3}_{lim} = min(L_N^P, L_{PO_4}^P, L_{Fe})`` where ``L_{Fe} = Fe / (Fe + 0.05)``. Additionally, in the NEMO source code ``L_{PO_4}`` is ``PO_4 / (PO_4 + K^{P, min}_{NH_4})`` but that didn't make sense to us so we assumed it was ``L_{PO_4}^P`` +- The temperature factor in calcite production is supposed to bring the production to zero when the temperature goes below 0°C but in the documented form does not, it was changed to ``max(0, T / (T + 0.1))`` +- We think there is an additional factor of ``Diss_{Si}`` in the ``PSi`` equation (51) so have neglected it +- A factor of ``R_{NH_4}`` appears in the nitrate equation which is undefined, and we did not track down in the NEMO source code so have neglected +- The form of ``K^{Fe}_{eq}`` in equation 65 is not given, so we took the form ``\exp\left(16.27 - 1565.7 / max(T + 273.15, 5)\right)`` from the NEMO source code +- Equation 32 contains a typo, the second term should be ``(1 - \gamma ^M)(1 - e^M - \sigma^M)(\sum \textcolor{red}{g^M (I)} + g_{FF}^M(GOC))M`` +- Equation 37 is missing a factor of ``3\Delta O_2`` in the third term, and ``sh`` in the fifth term +- Equation 40 is missing a factor of ``sh`` in the third and fourth terms, and is missing a ``+`` in the fourth term which should read ``0.5m^D \frac{D}{D+K_m}D + sh \times w^D D^2`` +- Equation 48 is missing a factor of ``3\Delta O_2`` in the second term, and a factor of ``Z`` in the penultimate term +- Equation 49 is missing a factor of ``3\Delta O_2`` in the second term +- Equations 54 and 55 are missing factors of the Redfield ratio in all terms except nitrification, nitrogen fixation. Additionally, we think that the term ``R_{NH_4}\lambda_{NH_4}\Delta(O_2)NH_4`` is not meant to be present and can not work out its utility or parameter values so have neglected +- Equation 60 is missing a factor of ``e^Z`` in the first term and ``e^M``, but for clarity we have rewritten it as: +```math +\frac{\partial Fe}{\partial t} += \sum_J^{Z, M}\left[J\max\left(0, (1 - \sigma)\sum_I{g^J(I)\theta^{Fe, I}} - e^J\theta^{Fe, J}\sum_I{g^J(I)} \right)\right], +``` +which is the total iron grazed, minus the amount which is routed to particles, minus the amount stored in the zooplankton (and is identical with different simplification to the original) +- Equation 19 has a typo and ``L^{I^Fe}_{lim, 2}`` should read ``4 - 4.5 LFe / (LFe + 1)`` +- In equation 33, the `min` parts did not make sense (and we don't think are present in the NEMO source code), and so have been neglected +- The first term in equation 14 should read ``(1-\delta^I)12 (\theta_{min}^{Chl, I} + \rho(\theta_{max}^{Chl, I}-\theta_{min}^{Chl, I}))\mu^I I`` and ``\rho`` should be given by ``L_{day}\frac{\mu I}{f_1}L\frac{12I}{IChl}\frac{L_P}{\alpha PAR}``, maybe this is what it says, but it was not clear + +## Changes since [Aumont2015](@citet) in NEMO +- Diatom quadratic mortality has changed forms to ``w^D=w^P + 0.25 w^D_{max} \frac{1 - (L^D_{lim})^2}{0.25 + (L^D_{lim})^2}`` +- The P-I slope, ``\alpha``, can vary for adaptation to depth, but the default is no enhancement. This can be included in our version by setting `low_light_adaptation` to be non-zero in the growth rate parameterisations diff --git a/docs/src/model_components/carbon-chemistry.md b/docs/src/model_components/carbon-chemistry.md index c78ab4711..9feceb5d4 100644 --- a/docs/src/model_components/carbon-chemistry.md +++ b/docs/src/model_components/carbon-chemistry.md @@ -63,11 +63,11 @@ carbon_chemistry(; DIC, Alk, T, S, lon = -31.52, lat = 33.75) The default uses the polynomial approximation described in [roquet2015](@citet) as provided by [`SeawaterPolynomials.jl`](https://github.com/CliMA/SeawaterPolynomials.jl/). ### Computing the carbonate concentration -So that this model can be used in calcite dissolution models it can also return the carbonate saturation by calling the function `carbonate_saturation` +So that this model can be used in calcite dissolution models it can also return the carbonate saturation by calling the function `calcite_saturation` ```@example carbon-chem -using OceanBioME.Models.CarbonChemistryModel: carbonate_saturation +using OceanBioME.Models.CarbonChemistryModel: calcite_saturation -carbonate_saturation(carbon_chemistry; DIC, Alk, T, S) +calcite_saturation(carbon_chemistry; DIC, Alk, T, S) ``` This function takes all of the same arguments (e.g. `boron`) as `carbon_chemistry` above. diff --git a/src/BoxModel/boxmodel.jl b/src/BoxModel/boxmodel.jl index f04800dd0..4e740e384 100644 --- a/src/BoxModel/boxmodel.jl +++ b/src/BoxModel/boxmodel.jl @@ -13,14 +13,16 @@ using Oceananigans.Biogeochemistry: update_biogeochemical_state! using Oceananigans.Fields: CenterField +using Oceananigans.Grids: RectilinearGrid, Flat using Oceananigans.TimeSteppers: tick!, TimeStepper +using Oceananigans: UpdateStateCallsite, TendencyCallsite using OceanBioME: BoxModelGrid -using StructArrays, JLD2 +using 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! @@ -46,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...)`. @@ -64,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)]) @@ -106,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 c16e46d01..bb27dd774 100644 --- a/src/BoxModel/timesteppers.jl +++ b/src/BoxModel/timesteppers.jl @@ -1,8 +1,9 @@ using Oceananigans.Architectures: device -using Oceananigans.Biogeochemistry: update_tendencies!, biogeochemical_auxiliary_fields +using Oceananigans.Biogeochemistry: update_tendencies!, biogeochemical_auxiliary_fields, AbstractBiogeochemistry, AbstractContinuousFormBiogeochemistry using Oceananigans.Grids: nodes, Center using Oceananigans.TimeSteppers: rk3_substep_field!, store_field_tendencies!, RungeKutta3TimeStepper, QuasiAdamsBashforth2TimeStepper using Oceananigans.Utils: work_layout, launch! +using Oceananigans: TendencyCallsite import Oceananigans.TimeSteppers: rk3_substep!, store_tendencies!, compute_tendencies! @@ -40,7 +41,7 @@ function compute_tendencies!(model::BoxModel, callbacks) for tracer in required_biogeochemical_tracers(model.biogeochemistry) forcing = @inbounds model.forcing[tracer] - @inbounds Gⁿ[tracer][1, 1, 1] = tracer_tendency(Val(tracer), model.biogeochemistry, forcing, model.clock.time, model.field_values, model.grid) + @inbounds Gⁿ[tracer][1, 1, 1] = tracer_tendency(Val(tracer), model.biogeochemistry, forcing, model.clock, model.fields, model.field_values, model.grid) end for callback in callbacks @@ -56,10 +57,11 @@ end @inline boxmodel_coordinate(::Nothing, grid) = zero(grid) @inline boxmodel_coordinate(nodes, grid) = @inbounds nodes[1] -@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_name, biogeochemistry::AbstractContinuousFormBiogeochemistry, forcing, clock, model_fields, model_field_values, grid) = + biogeochemistry(val_name, boxmodel_xyz(nodes(grid, Center(), Center(), Center()), grid)..., clock.time, model_field_values...) + forcing(time, model_fields...) -@inline tracer_tendency(::Val{:T}, biogeochemistry, forcing, time, model_fields, grid) = 0 +@inline tracer_tendency(val_name, biogeochemistry::AbstractBiogeochemistry, forcing, clock, model_fields, model_field_values, grid) = + biogeochemistry(1, 1, 1, grid, val_name, clock, model_fields) + forcing(clock, model_fields) function rk3_substep!(model::BoxModel, Δt, γⁿ, ζⁿ) model_fields = prognostic_fields(model) diff --git a/src/Light/Light.jl b/src/Light/Light.jl index 8cb8a594b..04e982112 100644 --- a/src/Light/Light.jl +++ b/src/Light/Light.jl @@ -13,7 +13,7 @@ using KernelAbstractions, Oceananigans.Units using Oceananigans.Architectures: device, architecture, on_architecture using Oceananigans.Utils: launch! using Oceananigans: Center, Face, fields -using Oceananigans.Grids: node, znodes +using Oceananigans.Grids: node, znodes, znode using Oceananigans.Fields: CenterField, TracerFields, location using Oceananigans.BoundaryConditions: fill_halo_regions!, ValueBoundaryCondition, @@ -36,6 +36,8 @@ include("2band.jl") include("multi_band.jl") include("prescribed.jl") +include("compute_euphotic_depth.jl") + default_surface_PAR(x, y, t) = default_surface_PAR(t) default_surface_PAR(x_or_y, t) = default_surface_PAR(t) default_surface_PAR(t) = 100 * max(0, cos(t * π / 12hours)) diff --git a/src/Light/compute_euphotic_depth.jl b/src/Light/compute_euphotic_depth.jl new file mode 100644 index 000000000..8652d9938 --- /dev/null +++ b/src/Light/compute_euphotic_depth.jl @@ -0,0 +1,44 @@ +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] + PAR[i, j, grid.Nz + 1])/2 + + @inbounds euphotic_depth[i, j, 1] = -Inf + + for k in grid.Nz-1:-1:1 + PARₖ = @inbounds PAR[i, j, k] + + # BRANCHING! + if (PARₖ <= surface_PAR * cutoff) && isinf(euphotic_depth[i, j]) + # interpolate to find depth + 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()) + + @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) + grid = PAR.grid + arch = architecture(grid) + + launch!(arch, grid, :xy, _compute_euphotic_depth!, euphotic_depth, PAR, grid, cutoff) + + fill_halo_regions!(euphotic_depth) + + return nothing +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/Light/multi_band.jl b/src/Light/multi_band.jl index d3cbebcda..bb3f55fe2 100644 --- a/src/Light/multi_band.jl +++ b/src/Light/multi_band.jl @@ -71,7 +71,7 @@ function MultiBandPhotosyntheticallyActiveRadiation(; grid, base_water_attenuation_coefficient = MOREL_kʷ, base_chlorophyll_exponent = MOREL_e, base_chlorophyll_attenuation_coefficient = MOREL_χ, - field_names = [par_symbol(n) for n in 1:length(bands)], + field_names = ntuple(n->par_symbol(n), Val(length(bands))), surface_PAR = default_surface_PAR, surface_PAR_division = fill(1 / length(bands), length(bands))) Nbands = length(bands) @@ -91,11 +91,17 @@ function MultiBandPhotosyntheticallyActiveRadiation(; grid, sum(surface_PAR_division) == 1 || throw(ArgumentError("surface_PAR_division does not sum to 1")) - fields = [CenterField(grid; - boundary_conditions = - regularize_field_boundary_conditions( - FieldBoundaryConditions(top = ValueBoundaryCondition(ScaledSurfaceFunction(surface_PAR, surface_PAR_division[n]))), grid, name)) - for (n, name) in enumerate(field_names)] + n_fields = length(field_names) + + surface_boundary_conditions = + ntuple(n-> ValueBoundaryCondition(ScaledSurfaceFunction(surface_PAR, surface_PAR_division[n])), Val(n_fields)) + + field_boundary_conditions = + ntuple(n -> regularize_field_boundary_conditions(FieldBoundaryConditions(top = surface_boundary_conditions[n]), + grid, field_names[n]), + Val(n_fields)) + + fields = NamedTuple{field_names}(ntuple(n -> CenterField(grid; boundary_conditions = field_boundary_conditions[n]), Val(n_fields))) total_PAR = sum(fields) @@ -115,14 +121,10 @@ function numerical_mean(λ, C, idx1, idx2) return ∫Cdλ / ∫dλ end -function par_symbol(n) - subscripts = Symbol[] - for digit in reverse(digits(n)) - push!(subscripts, Symbol(Char('\xe2\x82\x80'+digit))) - end +@inline par_symbol(n) = Symbol(:PAR, Char('\xe2\x82\x80'+n)) #Symbol(:PAR, number_subscript(tuple(reverse(digits(n))...))...) - return Symbol(:PAR, subscripts...) -end +@inline number_subscript(digits::NTuple{N}) where N = + ntuple(n->Symbol(Char('\xe2\x82\x80'+digits[n])), Val(N)) @kernel function update_MultiBandPhotosyntheticallyActiveRadiation!(grid, field, kʷ, e, χ, _surface_PAR, surface_PAR_division, @@ -146,7 +148,6 @@ end end end - function update_biogeochemical_state!(model, PAR::MultiBandPhotosyntheticallyActiveRadiation) grid = model.grid @@ -168,9 +169,9 @@ function update_biogeochemical_state!(model, PAR::MultiBandPhotosyntheticallyAct k′) end - for field in PAR.fields - fill_halo_regions!(field, model.clock, fields(model)) - end + #for field in PAR.fields + # fill_halo_regions!(field, model.clock, fields(model)) + #end end summary(par::MultiBandPhotosyntheticallyActiveRadiation) = @@ -179,7 +180,19 @@ summary(par::MultiBandPhotosyntheticallyActiveRadiation) = show(io::IO, model::MultiBandPhotosyntheticallyActiveRadiation) = print(io, summary(model)) biogeochemical_auxiliary_fields(par::MultiBandPhotosyntheticallyActiveRadiation) = - merge((PAR = par.total, ), NamedTuple{par.field_names}(par.fields)) + merge((PAR = par.total, ), par.fields)#NamedTuple{field_names(par.field_names, par.fields)}(par.fields)) + +@inline field_names(field_names, fields) = field_names +@inline field_names(::Nothing, fields::NTuple{N}) where N = ntuple(n -> par_symbol(n), Val(N)) # avoid passing this into kernels -Adapt.adapt_structure(to, par::MultiBandPhotosyntheticallyActiveRadiation) = nothing +Adapt.adapt_structure(to, par::MultiBandPhotosyntheticallyActiveRadiation) = + MultiBandPhotosyntheticallyActiveRadiation(adapt(to, par.total), + adapt(to, par.fields), + nothing, + nothing, + nothing, + nothing, + nothing, + nothing) + diff --git a/src/Light/prescribed.jl b/src/Light/prescribed.jl index b3949977e..484e0b3fb 100644 --- a/src/Light/prescribed.jl +++ b/src/Light/prescribed.jl @@ -1,13 +1,15 @@ -using Oceananigans.Fields: compute!, AbstractField +using Oceananigans.Architectures: architecture, GPU +using Oceananigans.Fields: compute!, AbstractField, ConstantField function maybe_named_fields(field) isa(field, AbstractField) || @warn "fields: $field is not an `AbstractField" - return ((:PAR, ), (field, )) + return NamedTuple{(:PAR, )}((field, )) end -maybe_named_fields(fields::NamedTuple) = (keys(fields), values(fields)) +maybe_named_fields(fields::NamedTuple) = fields + """ PrescribedPhotosyntheticallyActiveRadiation(fields) @@ -20,26 +22,20 @@ fields which are user specified, e.g. they may be `FunctionField`s or fields which will be returned in `biogeochemical_auxiliary_fields`, if only one field is present the field will be named `PAR`. """ - -struct PrescribedPhotosyntheticallyActiveRadiation{F, FN} +struct PrescribedPhotosyntheticallyActiveRadiation{F} fields :: F - field_names :: FN - - PrescribedPhotosyntheticallyActiveRadiation(fields::F, names::FN) where {F, FN} = - new{F, FN}(fields, names) function PrescribedPhotosyntheticallyActiveRadiation(fields) - names, values = maybe_named_fields(fields) + fields = maybe_named_fields(fields) - F = typeof(values) - FN = typeof(names) - - return new{F, FN}(values, names) + F = typeof(fields) + + return new{F}(fields) end end function update_biogeochemical_state!(model, PAR::PrescribedPhotosyntheticallyActiveRadiation) - for field in PAR.fields + for field in values(PAR.fields) compute!(field) end @@ -49,9 +45,12 @@ end summary(::PrescribedPhotosyntheticallyActiveRadiation) = string("Prescribed PAR") show(io::IO, model::PrescribedPhotosyntheticallyActiveRadiation{F}) where {F} = print(io, summary(model), "\n", " Fields:", "\n", - " └── $(model.field_names)") + " └── $(keys(model.fields))") + +biogeochemical_auxiliary_fields(par::PrescribedPhotosyntheticallyActiveRadiation) = par.fields -biogeochemical_auxiliary_fields(par::PrescribedPhotosyntheticallyActiveRadiation) = NamedTuple{par.field_names}(par.fields) +@inline prescribed_field_names(field_names, fields) = field_names +@inline prescribed_field_names(::Nothing, fields::NTuple{N}) where N = tuple(:PAR, ntuple(n -> par_symbol(n), Val(N-1))...) -adapt_structure(to, par::PrescribedPhotosyntheticallyActiveRadiation) = PrescribedPhotosyntheticallyActiveRadiation(adapt(to, par.fields), - adapt(to, par.field_names)) +adapt_structure(to, par::PrescribedPhotosyntheticallyActiveRadiation) = + PrescribedPhotosyntheticallyActiveRadiation(adapt(to, par.fields)) diff --git a/src/Models/AdvectedPopulations/PISCES/PISCES.jl b/src/Models/AdvectedPopulations/PISCES/PISCES.jl new file mode 100644 index 000000000..254921787 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/PISCES.jl @@ -0,0 +1,406 @@ +""" +Pelagic Interactions Scheme for Carbon and Ecosystem Studies (PISCES) model. + +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, 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, DiscreteBiogeochemistry, ScaleNegativeTracers, CBMDayLength +using OceanBioME.BoxModels: BoxModel +using OceanBioME.Models.CarbonChemistryModel: CarbonChemistry + +using Oceananigans.Biogeochemistry: AbstractBiogeochemistry +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_auxiliary_fields, + update_biogeochemical_state! + +import OceanBioME: maximum_sinking_velocity + +import Base: show, summary + +struct PISCES{PP, ZP, DM, PM, NI, FE, SI, OX, PO, IC, FT, LA, DL, ML, EU, MS, VD, MP, CC, CS, SS} <: AbstractBiogeochemistry + phytoplankton :: PP + + zooplankton :: ZP + + dissolved_organic_matter :: DM + particulate_organic_matter :: PM + + nitrogen :: NI + iron :: FE + silicate :: SI + oxygen :: OX + phosphate :: PO + + inorganic_carbon :: IC + + 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 + + sinking_velocities :: SS +end + +@inline required_biogeochemical_tracers(bgc::PISCES) = + (required_biogeochemical_tracers(bgc.phytoplankton)..., + required_biogeochemical_tracers(bgc.zooplankton)..., + required_biogeochemical_tracers(bgc.dissolved_organic_matter)..., + required_biogeochemical_tracers(bgc.particulate_organic_matter)..., + required_biogeochemical_tracers(bgc.nitrogen)..., + required_biogeochemical_tracers(bgc.phosphate)..., + required_biogeochemical_tracers(bgc.iron)..., + required_biogeochemical_tracers(bgc.silicate)..., + required_biogeochemical_tracers(bgc.inorganic_carbon)..., + required_biogeochemical_tracers(bgc.oxygen)..., + :T, :S) + +@inline required_biogeochemical_auxiliary_fields(::PISCES) = + (:zₘₓₗ, :zₑᵤ, :Si′, :Ω, :κ, :mixed_layer_PAR, :wPOC, :wGOC, :PAR, :PAR₁, :PAR₂, :PAR₃) + +@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) + +(bgc::PISCES)(i, j, k, grid, val_name, clock, fields, auxiliary_fields) = zero(grid) + +(bgc::DiscreteBiogeochemistry{<:PISCES})(i, j, k, grid, val_name, clock, fields) = + bgc.underlying_biogeochemistry(i, j, k, grid, val_name, clock, fields, biogeochemical_auxiliary_fields(bgc)) + +include("common.jl") +include("generic_functions.jl") +include("mean_mixed_layer_properties.jl") +include("compute_calcite_saturation.jl") +include("update_state.jl") + +include("zooplankton/zooplankton.jl") + +using .Zooplankton + +include("phytoplankton/phytoplankton.jl") + +using .Phytoplankton + +include("dissolved_organic_matter/dissolved_organic_matter.jl") + +using .DissolvedOrganicMatter + +include("particulate_organic_matter/particulate_organic_matter.jl") + +using .ParticulateOrganicMatter + +include("nitrogen/nitrogen.jl") + +using .Nitrogen + +include("iron/iron.jl") + +using .Iron + +include("silicate.jl") + +using .Silicates + +include("oxygen.jl") + +using .OxygenModels + +include("phosphate.jl") + +using .Phosphates + +include("inorganic_carbon.jl") + +using .InorganicCarbons + +include("coupling_utils.jl") + +include("adapts.jl") + +include("show_methods.jl") + +""" + PISCES(; grid, + phytoplankton = MixedMondoNanoAndDiatoms(), + zooplankton = MicroAndMesoZooplankton(), + dissolved_organic_matter = DissolvedOrganicCarbon(), + particulate_organic_matter = TwoCompartementCarbonIronParticles(), + + nitrogen = NitrateAmmonia(), + iron = SimpleIron(), + silicate = Silicate(), + oxygen = Oxygen(), + phosphate = Phosphate(), + + inorganic_carbon = InorganicCarbon(), + + # 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. + + +Keyword Arguments +================= + +- `grid`: (required) the geometry to build the model on +- `phytoplankton`: phytoplankton evolution parameterisation, defaults to nanophyto and diatom size classes with `MixedMondo` growth +- `zooplankton`: zooplankton evolution parameterisation, defaults to two class `Z` and `M` +- `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`, `CaCO₃`) +- `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₄`) +- `inorganic_carbon`: parameterisation for the evolution of the inorganic 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 +- `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 + +All parameterisations default to the operaitonal version of PISCES as close as possible. + +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, + phytoplankton = MixedMondoNanoAndDiatoms(), + zooplankton = MicroAndMesoZooplankton(), + dissolved_organic_matter = DissolvedOrganicCarbon(), + particulate_organic_matter = TwoCompartementCarbonIronParticles(), + + nitrogen = NitrateAmmonia(), + iron = SimpleIron(), + silicate = Silicate(), + oxygen = Oxygen(), + phosphate = Phosphate(), + + inorganic_carbon = InorganicCarbon(), + + # 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 = CBMDayLength(), + + 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) + + @warn "This implementation of PISCES is in early development and has not yet been validated against the operational version" + + 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) + + 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 (latitude isa ModelLatitude) & (grid isa RectilinearGrid) + throw(ArgumentError("You must prescribe a latitude when using a `RectilinearGrid`")) + end + + # just incase we're in the default state with no closure model + # this highlights that the darkness term for phytoplankton growth is obviously wrong because not all phytoplankton + # cells spend an infinite amount of time in the dark if the diffusivity is zero, it should depend on where they are... + if !(mean_mixed_layer_vertical_diffusivity isa ConstantField) + set!(mean_mixed_layer_vertical_diffusivity, 1) + end + + underlying_biogeochemistry = PISCES(phytoplankton, zooplankton, + dissolved_organic_matter, particulate_organic_matter, + nitrogen, iron, silicate, oxygen, phosphate, + inorganic_carbon, + 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 + scalers = ScaleNegativeTracers(underlying_biogeochemistry, grid; invalid_fill_value) + if isnothing(modifiers) + modifiers = scalers + elseif modifiers isa Tuple + modifiers = (modifiers..., scalers...) + else + modifiers = (modifiers, scalers...) + end + end + + return Biogeochemistry(underlying_biogeochemistry; + light_attenuation, + sediment, + particles, + modifiers) +end + +end # module diff --git a/src/Models/AdvectedPopulations/PISCES/adapts.jl b/src/Models/AdvectedPopulations/PISCES/adapts.jl new file mode 100644 index 000000000..6ccd1fccc --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/adapts.jl @@ -0,0 +1,61 @@ +using Adapt + +import Adapt: adapt_structure + +Adapt.adapt_structure(to, bgc::PISCES) = + PISCES(adapt(to, bgc.phytoplankton), + adapt(to, bgc.zooplankton), + 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.inorganic_carbon), + 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.calcite_saturation), + adapt(to, bgc.sinking_velocities)) + +Adapt.adapt_structure(to, zoo::MicroAndMeso) = + MicroAndMeso(adapt(to, zoo.micro), + adapt(to, zoo.meso), + adapt(to, zoo.microzooplankton_bacteria_concentration), + adapt(to, zoo.mesozooplankton_bacteria_concentration), + adapt(to, zoo.maximum_bacteria_concentration), + adapt(to, zoo.bacteria_concentration_depth_exponent), + adapt(to, zoo.doc_half_saturation_for_bacterial_activity), + adapt(to, zoo.nitrate_half_saturation_for_bacterial_activity), + adapt(to, zoo.ammonia_half_saturation_for_bacterial_activity), + adapt(to, zoo.phosphate_half_saturation_for_bacterial_activity), + adapt(to, zoo.iron_half_saturation_for_bacterial_activity)) + +Adapt.adapt_structure(to, zoo::QualityDependantZooplankton) = + QualityDependantZooplankton(adapt(to, zoo.temperature_sensetivity), + adapt(to, zoo.maximum_grazing_rate), + adapt(to, zoo.food_preferences), # the only one that isn't already bits + adapt(to, zoo.food_threshold_concentration), + adapt(to, zoo.specific_food_thresehold_concentration), + adapt(to, zoo.grazing_half_saturation), + adapt(to, zoo.maximum_flux_feeding_rate), + adapt(to, zoo.iron_ratio), + adapt(to, zoo.minimum_growth_efficiency), + adapt(to, zoo.non_assililated_fraction), + adapt(to, zoo.mortality_half_saturation), + adapt(to, zoo.quadratic_mortality), + adapt(to, zoo.linear_mortality), + adapt(to, zoo.dissolved_excretion_fraction), + adapt(to, zoo.undissolved_calcite_fraction)) diff --git a/src/Models/AdvectedPopulations/PISCES/calcite.jl b/src/Models/AdvectedPopulations/PISCES/calcite.jl new file mode 100644 index 000000000..460584b32 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/calcite.jl @@ -0,0 +1,82 @@ +""" + Calcite + +Stores the parameter values for calcite (`CaCO₃`) evolution. + +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 + +""" +@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 + +@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 + +@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 + + linear_mortality, quadratic_mortality = mortality(bgc.nanophytoplankton, bgc, z, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′, zₘₓₗ) + + total_mortality = 0.5 * (linear_mortality + quadratic_mortality) + + 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 + _, _, LPO₄, LN = bgc.nanophytoplankton.nutrient_limitation(bgc, P, PChl, PFe, NO₃, NH₄, PO₄, Fe, Si, Si′) + + LFe = Fe / (Fe + 0.05) + + # from NEMO source code +/- presumably a typo replacing kPO4 with kNH4 which aren't even same units + L_CaCO₃ = min(LN, LPO₄, LFe) + + phytoplankton_concentration_factor = max(1, P / 2) + + low_light_factor = max(0, PAR - 1) / (4 + PAR) + high_light_factor = 30 / (30 + 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) + + 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/common.jl b/src/Models/AdvectedPopulations/PISCES/common.jl new file mode 100644 index 000000000..dc96a45fc --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/common.jl @@ -0,0 +1,71 @@ +using KernelAbstractions: @kernel, @index + +using Oceananigans.Fields: flatten_node +using Oceananigans.Grids: znode, zspacing, φnode + +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 + +""" + 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 (pl::PrescribedLatitude)(i, j, k, grid) = pl.latitude + +@inline (::ModelLatitude)(φ) = φ +@inline (::ModelLatitude)(i, j, k, grid) = φnode(i, j, k, grid, Center(), Center(), Center()) + +""" + 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 = 30/day # m/s + 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 +@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()) + + 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 diff --git a/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl b/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl new file mode 100644 index 000000000..112e97b77 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/compute_calcite_saturation.jl @@ -0,0 +1,35 @@ +using Oceananigans.Architectures: architecture +using Oceananigans.BoundaryConditions: fill_halo_regions! +using Oceananigans.BuoyancyModels: g_Earth +using Oceananigans.Models: fields +using Oceananigans.Utils: launch! + +using OceanBioME.Models: CarbonChemistryModel + +function compute_calcite_saturation!(carbon_chemistry, calcite_saturation, model) + grid = model.grid + + arch = architecture(grid) + + launch!(arch, grid, :xyz, _compute_calcite_saturation!, carbon_chemistry, calcite_saturation, grid, fields(model)) + + fill_halo_regions!(calcite_saturation) + + return nothing +end + +@kernel function _compute_calcite_saturation!(carbon_chemistry, calcite_saturation, grid, model_fields) + i, j, k = @index(Global, NTuple) + + T = @inbounds model_fields.T[i, j, k] + S = @inbounds model_fields.S[i, j, k] + DIC = @inbounds model_fields.DIC[i, j, k] + Alk = @inbounds model_fields.Alk[i, j, k] + silicate = @inbounds model_fields.Si[i, j, k] # might get rid of this since it doesn't do anything + + 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 + + @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..f9a4b7d56 --- /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.zooplankton.micro.iron_ratio, bgc.zooplankton.meso.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 new file mode 100644 index 000000000..7c3b13c14 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter.jl @@ -0,0 +1,137 @@ +""" + 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 + +@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 + +@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 + + Bact = bacteria_concentration(dom, z, Z, M, zₘₓₗ, zₑᵤ) + + 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 + +@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 + +@inline function denitrifcation(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 ΔO₂ * degredation +end + +@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 diff --git a/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_carbon.jl b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_carbon.jl new file mode 100644 index 000000000..551e2960d --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_carbon.jl @@ -0,0 +1,111 @@ +using Oceananigans.Grids: znode, Center + +""" + DissolvedOrganicCarbon + +Parameterisation of dissolved organic matter which depends on a bacterial +concentration. +""" +@kwdef struct DissolvedOrganicCarbon{FT, AP} + remineralisation_rate :: FT = 0.3/day # 1 / s + bacteria_concentration_depth_exponent :: FT = 0.684 # + reference_bacteria_concentration :: FT = 1.0 # mmol C / m³ + temperature_sensetivity :: FT = 1.066 # +# (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) +end + +required_biogeochemical_tracers(::DissolvedOrganicCarbon) = tuple(:DOC) + +@inline function (bgc::PISCES{<:Any, <:Any, <:DissolvedOrganicCarbon})(i, j, k, grid, ::Val{:DOC}, clock, fields, auxiliary_fields) + phytoplankton_exudate = dissolved_exudate(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic_exudate = upper_trophic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + grazing_waste = organic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + particulate_breakdown = degredation(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolved_breakdown = degredation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + aggregation_to_particles, = aggregation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (phytoplankton_exudate + upper_trophic_exudate + grazing_waste + particulate_breakdown + - dissolved_breakdown - aggregation_to_particles) +end + +@inline function degredation(dom::DissolvedOrganicCarbon, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + Bact_ref = dom.reference_bacteria_concentration + b = dom.temperature_sensetivity + λ = dom.remineralisation_rate + + T = @inbounds fields.T[i, j, k] + DOC = @inbounds fields.DOC[i, j, k] + + f = b^T + + Bact = bacteria_concentration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + LBact = bacteria_activity(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return λ * f * LBact * Bact / Bact_ref * DOC # differes from Aumont 2015 since the dimensions don't make sense +end + +@inline function aggregation(dom::DissolvedOrganicCarbon, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + a₁, a₂, a₃, a₄, a₅ = dom.aggregation_parameters + + backgroound_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + + DOC = @inbounds fields.DOC[i, j, k] + POC = @inbounds fields.POC[i, j, k] + GOC = @inbounds fields.GOC[i, j, k] + + 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 + +@inline function aggregation_of_colloidal_iron(dom::DissolvedOrganicCarbon, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + _, Φ₁, Φ₂, Φ₃ = aggregation(dom, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + Fe = @inbounds fields.Fe[i, j, k] + DOC = @inbounds fields.DOC[i, j, k] + + Fe′ = free_iron(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + 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 + +@inline function oxic_remineralisation(dom::DissolvedOrganicCarbon, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + O₂ = @inbounds fields.O₂[i, j, k] + + ΔO₂ = anoxia_factor(bgc, O₂) + + total_degredation = degredation(dom, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (1 - ΔO₂) * total_degredation +end + +@inline function anoxic_remineralisation(dom::DissolvedOrganicCarbon, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + O₂ = @inbounds fields.O₂[i, j, k] + + ΔO₂ = anoxia_factor(bgc, O₂) + + total_degredation = degredation(dom, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return ΔO₂ * total_degredation +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_matter.jl b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_matter.jl new file mode 100644 index 000000000..9273a479e --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/dissolved_organic_matter/dissolved_organic_matter.jl @@ -0,0 +1,18 @@ +module DissolvedOrganicMatter + +export DissolvedOrganicCarbon + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: + degredation, aggregation, PISCES, free_iron, anoxia_factor +using OceanBioME.Models.PISCESModel.Phytoplankton: dissolved_exudate +using OceanBioME.Models.PISCESModel.Zooplankton: + organic_excretion, upper_trophic_excretion, bacteria_concentration, bacteria_activity + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers +import OceanBioME.Models.PISCESModel: degredation, aggregation + +include("dissolved_organic_carbon.jl") + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/generic_functions.jl b/src/Models/AdvectedPopulations/PISCES/generic_functions.jl new file mode 100644 index 000000000..b573ef24f --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/generic_functions.jl @@ -0,0 +1,7 @@ +# function that need to be defined and accessed in several sub-modules + +function degredation end +function aggregation end +function mortality end +function free_iron end +function flux_rate end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/inorganic_carbon.jl b/src/Models/AdvectedPopulations/PISCES/inorganic_carbon.jl new file mode 100644 index 000000000..f783299b9 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/inorganic_carbon.jl @@ -0,0 +1,60 @@ +module InorganicCarbons + +export InorganicCarbon + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: degredation + +using OceanBioME.Models.PISCESModel.ParticulateOrganicMatter: + calcite_production, calcite_dissolution + +using OceanBioME.Models.PISCESModel.Phytoplankton: total_production + +using OceanBioME.Models.PISCESModel.Zooplankton: + inorganic_excretion, upper_trophic_respiration + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers + +""" + InorganicCarbon + +Default parameterisation for `DIC`` and `Alk`alinity evolution. +""" +struct InorganicCarbon end + +required_biogeochemical_tracers(::InorganicCarbon) = (:DIC, :Alk) + +const PISCESCarbon = PISCES{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:InorganicCarbon} + +@inline function (bgc::PISCESCarbon)(i, j, k, grid, ::Val{:DIC}, clock, fields, auxiliary_fields) + zooplankton_respiration = inorganic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic = upper_trophic_respiration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + remineralisation = degredation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + calcite_diss = calcite_dissolution(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + calcite_prod = calcite_production(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + consumption = total_production(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (zooplankton_respiration + upper_trophic + remineralisation + + calcite_diss - calcite_prod - consumption) +end + +@inline function (bgc::PISCESCarbon)(i, j, k, grid, val_name::Val{:Alk}, clock, fields, auxiliary_fields) + θ = bgc.nitrogen_redfield_ratio + + nitrate_production = bgc(i, j, k, grid, Val(:NO₃), clock, fields, auxiliary_fields) + ammonia_production = bgc(i, j, k, grid, Val(:NH₄), clock, fields, auxiliary_fields) + calcite_production = bgc(i, j, k, grid, Val(:CaCO₃), clock, fields, auxiliary_fields) + + # I think there are typos in Aumount 2015 but this is what it should be ( I think ???) + return ammonia_production - nitrate_production - 2 * calcite_production +end + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/iron/iron.jl b/src/Models/AdvectedPopulations/PISCES/iron/iron.jl new file mode 100644 index 000000000..d0edc7519 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/iron/iron.jl @@ -0,0 +1,39 @@ +module Iron + +export SimpleIron + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: + aggregation_of_colloidal_iron, degredation + +using OceanBioME.Models.PISCESModel.ParticulateOrganicMatter: + iron_scavenging, iron_scavenging_rate, bacterial_iron_uptake + +using OceanBioME.Models.PISCESModel.Phytoplankton: uptake + +using OceanBioME.Models.PISCESModel.Zooplankton: + non_assimilated_iron, upper_trophic_dissolved_iron + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers +import OceanBioME.Models.PISCESModel: free_iron + +include("simple_iron.jl") + +@inline function free_iron(::SimpleIron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + DOC = @inbounds fields.DOC[i, j, k] + Fe = @inbounds fields.Fe[i, j, k] + + T = @inbounds fields.T[i, j, k] + + # 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 + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/iron/simple_iron.jl b/src/Models/AdvectedPopulations/PISCES/iron/simple_iron.jl new file mode 100644 index 000000000..44cd9ac72 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/iron/simple_iron.jl @@ -0,0 +1,63 @@ +""" + 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.0 # unitless + maximum_ligand_concentration :: FT = 0.6 # μmol Fe / m³ + dissolved_ligand_ratio :: FT = 0.09 # μmol Fe / mmol C +end + +required_biogeochemical_tracers(::SimpleIron) = tuple(:Fe) + +const SimpleIronPISCES = PISCES{<:Any, <:Any, <:Any, <:Any, <:Any, <:SimpleIron} + +@inline function (bgc::SimpleIronPISCES)(i, j, k, grid, val_name::Val{:Fe}, clock, fields, auxiliary_fields) + λ̄ = bgc.iron.excess_scavenging_enhancement + + Fe = @inbounds fields.Fe[i, j, k] + + λFe = iron_scavenging_rate(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + Fe′ = free_iron(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + total_ligand_concentration = ligand_concentration(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # terminal process which removes iron from the ocean + ligand_aggregation = λ̄ * λFe * max(0, Fe - total_ligand_concentration) * Fe′ + + colloidal_aggregation, = aggregation_of_colloidal_iron(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # scavenging and bacterial uptake + scavenging = iron_scavenging(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + BactFe = bacterial_iron_uptake(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # particle breakdown + small_particles = degredation(bgc.particulate_organic_matter, Val(:SFe), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # consumption + consumption = uptake(bgc.phytoplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # waste + grazing_waste = non_assimilated_iron(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic_waste = upper_trophic_dissolved_iron(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (small_particles + grazing_waste + upper_trophic_waste + - consumption - ligand_aggregation - colloidal_aggregation - scavenging - BactFe) +end + +@inline function ligand_concentration(iron::SimpleIron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + Lₜᵐᵃˣ = iron.maximum_ligand_concentration + + DOC = @inbounds fields.DOC[i, j, k] + + Lₜ = iron.dissolved_ligand_ratio * DOC - Lₜᵐᵃˣ + + return max(Lₜᵐᵃˣ, Lₜ) +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl b/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl new file mode 100644 index 000000000..cd034666a --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/mean_mixed_layer_properties.jl @@ -0,0 +1,115 @@ +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!(κ, mixed_layer_depth, model) = + 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, grid) = nothing + +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) + + compute_mixed_layer_mean!(mean_mixed_layer_vertical_diffusivity, mixed_layer_depth, κ, grid) + + return nothing +end + +##### +##### Mean mixed layer light +##### + +compute_mean_mixed_layer_light!(mean_PAR, mixed_layer_depth, PAR, model) = + compute_mixed_layer_mean!(mean_PAR, mixed_layer_depth, PAR, model.grid) + +##### +##### 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 + +phytoplankton_diffusivity(closure, diffusivity_fields) = + phytoplankton_diffusivity(formulation(closure), closure, diffusivity_fields) + +phytoplankton_diffusivity(closure::Tuple, diffusivity_fields) = + sum(map(n -> phytoplankton_diffusivity(closure[n], diffusivity_fields[n]), 1:length(closure))) + +phytoplankton_diffusivity(formulation, closure, diffusivit_fields) = ZeroField() + +const NotHorizontalFormulation = Union{VerticalFormulation, ThreeDimensionalFormulation} + +phytoplankton_diffusivity(::NotHorizontalFormulation, closure, diffusivity_fields) = + throw(ErrorException("Mean mixed layer vertical diffusivity can not be calculated for $(closure)")) + +phytoplankton_diffusivity(::NotHorizontalFormulation, + closure::Union{ScalarDiffusivity, ScalarBiharmonicDiffusivity}, + diffusivity_fields) = + phytoplankton_diffusivity(closure.κ) + +phytoplankton_diffusivity(diffusivity_field) = diffusivity_field +phytoplankton_diffusivity(diffusivity_field::Number) = ConstantField(diffusivity_field) +phytoplankton_diffusivity(diffusivity_fields::NamedTuple) = phytoplankton_diffusivity(diffusivity_fields.P) +phytoplankton_diffusivity(::Function) = + throw(ErrorException("Can not compute mean mixed layer vertical diffusivity for `Function` type diffusivity, changing to a `FunctionField` would work")) diff --git a/src/Models/AdvectedPopulations/PISCES/nitrate_ammonia.jl b/src/Models/AdvectedPopulations/PISCES/nitrate_ammonia.jl new file mode 100644 index 000000000..5d5194040 --- /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/nitrogen/nitrate_ammonia.jl b/src/Models/AdvectedPopulations/PISCES/nitrogen/nitrate_ammonia.jl new file mode 100644 index 000000000..cf4029382 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/nitrogen/nitrate_ammonia.jl @@ -0,0 +1,89 @@ +""" + 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_nitrification_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 + +required_biogeochemical_tracers(::NitrateAmmonia) = (:NO₃, :NH₄) + +const NitrateAmnmoniaPISCES = PISCES{<:Any, <:Any, <:Any, <:Any, <:NitrateAmmonia} + +@inline function (bgc::NitrateAmnmoniaPISCES)(i, j, k, grid, val_name::Val{:NO₃}, clock, fields, auxiliary_fields) + θ = bgc.nitrogen_redfield_ratio + + nitrif = nitrification(bgc.nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + remineralisation = oxic_remineralisation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + consumption = uptake(bgc.phytoplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return nitrif + θ * (remineralisation - consumption) +end + +@inline function (bgc::NitrateAmnmoniaPISCES)(i, j, k, grid, val_name::Val{:NH₄}, clock, fields, auxiliary_fields) + θ = bgc.nitrogen_redfield_ratio + + nitrif = nitrification(bgc.nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + remineralisation = anoxic_remineralisation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + consumption = uptake(bgc.phytoplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + grazing_waste = inorganic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic_waste = upper_trophic_respiration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + fixation = nitrogen_fixation(bgc.nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return fixation + θ * (remineralisation + grazing_waste + upper_trophic_waste - consumption) - nitrif +end + +@inline function nitrification(nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + λ = nitrogen.maximum_nitrification_rate + + O₂ = @inbounds fields.O₂[i, j, k] + NH₄ = @inbounds fields.NH₄[i, j, k] + + PAR = @inbounds auxiliary_fields.mixed_layer_PAR[i, j, k] + + ΔO₂ = anoxia_factor(bgc, O₂) + + return λ * NH₄ / (1 + PAR) * (1 - ΔO₂) +end + +@inline function nitrogen_fixation(nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + 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 + + PAR = @inbounds auxiliary_fields.PAR[i, j, k] + + Fe = @inbounds fields.Fe[i, j, k] + PO₄ = @inbounds fields.PO₄[i, j, k] + + availability_limitation = nitrogen_availability_limitation(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + fixation_limit = ifelse(availability_limitation >= 0.8, 0.01, 1 - availability_limitation) + + μ = base_production_rate(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + 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 \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/nitrogen/nitrogen.jl b/src/Models/AdvectedPopulations/PISCES/nitrogen/nitrogen.jl new file mode 100644 index 000000000..84ff28ddd --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/nitrogen/nitrogen.jl @@ -0,0 +1,16 @@ +module Nitrogen + +export NitrateAmmonia + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: anoxia_factor, PISCES +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: oxic_remineralisation, anoxic_remineralisation +using OceanBioME.Models.PISCESModel.Phytoplankton: uptake, nitrogen_availability_limitation, base_production_rate +using OceanBioME.Models.PISCESModel.Zooplankton: upper_trophic_respiration, inorganic_excretion + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers + +include("nitrate_ammonia.jl") + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/oxygen.jl b/src/Models/AdvectedPopulations/PISCES/oxygen.jl new file mode 100644 index 000000000..389951a50 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/oxygen.jl @@ -0,0 +1,53 @@ +module OxygenModels + +export Oxygen + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: + oxic_remineralisation, anoxic_remineralisation + +using OceanBioME.Models.PISCESModel.Nitrogen: nitrification, nitrogen_fixation + +using OceanBioME.Models.PISCESModel.Phytoplankton: uptake + +using OceanBioME.Models.PISCESModel.Zooplankton: + inorganic_excretion, upper_trophic_respiration + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers + +@kwdef struct Oxygen{FT} + ratio_for_respiration :: FT = 133/122 # mol O₂ / mol C + ratio_for_nitrification :: FT = 32/122 # mol O₂ / mol C +end + +required_biogeochemical_tracers(::Oxygen) = tuple(:O₂) + +const PISCESOxygen = PISCES{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Oxygen} + +@inline function (bgc::PISCESOxygen)(i, j, k, grid, ::Val{:O₂}, clock, fields, auxiliary_fields) + θ_resp = bgc.oxygen.ratio_for_respiration + θ_nitrif = bgc.oxygen.ratio_for_nitrification + + zooplankton = θ_resp * inorganic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic = θ_resp * upper_trophic_respiration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + remineralisation = ((θ_resp + θ_nitrif) * oxic_remineralisation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + θ_resp * anoxic_remineralisation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + + ammonia_photosynthesis = θ_resp * uptake(bgc.phytoplankton, Val(:NH₄), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + nitrate_photosynthesis = (θ_resp + θ_nitrif) * uptake(bgc.phytoplankton, Val(:NO₃), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # I think (?) that we need the redfield raito here since θ_nitrif is oxygen per carbon + nitrif = θ_nitrif * nitrification(bgc.nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) / bgc.nitrogen_redfield_ratio + + fixation = θ_nitrif * nitrogen_fixation(bgc.nitrogen, i, j, k, grid, bgc, clock, fields, auxiliary_fields) / bgc.nitrogen_redfield_ratio + + return (ammonia_photosynthesis + nitrate_photosynthesis + fixation + - remineralisation - zooplankton - upper_trophic - nitrif) +end + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/calcite.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/calcite.jl new file mode 100644 index 000000000..1cd6e8857 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/calcite.jl @@ -0,0 +1,19 @@ +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:CaCO₃}, clock, fields, auxiliary_fields) + phytoplankton_production = calcite_production(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolution = calcite_dissolution(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return phytoplankton_production - dissolution +end + +@inline function calcite_dissolution(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + λ = poc.base_calcite_dissolution_rate + nca = poc.calcite_dissolution_exponent + + Ω = @inbounds auxiliary_fields.Ω[i, j, k] + CaCO₃ = @inbounds fields.CaCO₃[i, j, k] + + ΔCaCO₃ = max(0, 1 - Ω) + + return λ * ΔCaCO₃ ^ nca * CaCO₃ +end diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/carbon.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/carbon.jl new file mode 100644 index 000000000..fec438466 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/carbon.jl @@ -0,0 +1,59 @@ +# these are just completly different to eachother so not going to try and define a generic function + +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:POC}, clock, fields, auxiliary_fields) + # gains + grazing_waste = small_non_assimilated_waste(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + phytoplankton_mortality = small_mortality(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + zooplankton_mortality = small_mortality(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + _, Φ₁, _, Φ₃ = aggregation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolved_aggregation = Φ₁ + Φ₃ + + large_breakdown = degredation(bgc.particulate_organic_matter, Val(:GOC), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # losses + grazing = total_grazing(bgc.zooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + aggregation_to_large = aggregation(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + small_breakdown = degredation(bgc.particulate_organic_matter, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (grazing_waste + phytoplankton_mortality + zooplankton_mortality + dissolved_aggregation + large_breakdown + - grazing - aggregation_to_large - small_breakdown) +end + +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:GOC}, clock, fields, auxiliary_fields) + # gains + grazing_waste = large_non_assimilated_waste(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + phytoplankton_mortality = large_mortality(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + zooplankton_mortality = large_mortality(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + aggregation_to_large = aggregation(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + upper_trophic_feces = upper_trophic_fecal_production(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + _, _, dissolved_aggregation = aggregation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # losses + grazing = total_grazing(bgc.zooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + large_breakdown = degredation(bgc.particulate_organic_matter, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (grazing_waste + phytoplankton_mortality + zooplankton_mortality + upper_trophic_feces + + aggregation_to_large + dissolved_aggregation + - grazing - large_breakdown) +end + +@inline degredation(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = # for going to DOC + degredation(poc::TwoCompartementCarbonIronParticles, Val(:POC), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline degredation(poc::TwoCompartementCarbonIronParticles, ::Val{:POC}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds specific_degredation_rate(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * fields.POC[i, j, k] + +@inline degredation(poc::TwoCompartementCarbonIronParticles, ::Val{:GOC}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds specific_degredation_rate(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * fields.GOC[i, j, k] \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/iron.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/iron.jl new file mode 100644 index 000000000..502008fce --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/iron.jl @@ -0,0 +1,137 @@ + +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:SFe}, clock, fields, auxiliary_fields) + POC = @inbounds fields.POC[i, j, k] + SFe = @inbounds fields.SFe[i, j, k] + + θ = SFe / (POC + eps(0.0)) + + # gains + grazing_waste = + small_non_assimilated_iron_waste(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + phytoplankton_mortality = + small_mortality_iron(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + zooplankton_mortality = + small_mortality_iron(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + large_breakdown = + degredation(bgc.particulate_organic_matter, Val(:BFe), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + λFe = iron_scavenging_rate(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + Fe′ = free_iron(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + scavenging = λFe * POC * Fe′ + + κ = bgc.particulate_organic_matter.small_fraction_of_bacterially_consumed_iron + + BactFe = bacterial_iron_uptake(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + bacterial_assimilation = κ * BactFe + + _, colloidal_aggregation = aggregation_of_colloidal_iron(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # losses + grazing = total_grazing(bgc.zooplankton, Val(:POC), i, j, k, grid, bgc, clock, fields, auxiliary_fields) * θ + + aggregation_to_large = aggregation(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * θ + + small_breakdown = degredation(bgc.particulate_organic_matter, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (grazing_waste + phytoplankton_mortality + zooplankton_mortality + + large_breakdown + scavenging + bacterial_assimilation + colloidal_aggregation + - grazing - aggregation_to_large - small_breakdown) +end + +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:BFe}, clock, fields, auxiliary_fields) + POC = @inbounds fields.POC[i, j, k] + SFe = @inbounds fields.SFe[i, j, k] + GOC = @inbounds fields.GOC[i, j, k] + BFe = @inbounds fields.BFe[i, j, k] + + θS = SFe / (POC + eps(0.0)) + θB = BFe / (GOC + eps(0.0)) + + # gains + grazing_waste = large_non_assimilated_iron_waste(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + phytoplankton_mortality = large_mortality_iron(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + zooplankton_mortality = large_mortality_iron(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + aggregation_to_large = aggregation(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * θS + + upper_trophic_feces = upper_trophic_fecal_iron_production(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + λFe = iron_scavenging_rate(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + Fe′ = free_iron(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + scavenging = λFe * GOC * Fe′ + + κ = bgc.particulate_organic_matter.large_fraction_of_bacterially_consumed_iron + + BactFe = bacterial_iron_uptake(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + bacterial_assimilation = κ * BactFe + + _, _, colloidal_aggregation = aggregation_of_colloidal_iron(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # losses + grazing = total_grazing(bgc.zooplankton, Val(:GOC), i, j, k, grid, bgc, clock, fields, auxiliary_fields) * θB + + large_breakdown = degredation(bgc.particulate_organic_matter, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (grazing_waste + phytoplankton_mortality + zooplankton_mortality + upper_trophic_feces + + scavenging + bacterial_assimilation + colloidal_aggregation + aggregation_to_large + - grazing - large_breakdown) +end + +@inline degredation(poc::TwoCompartementCarbonIronParticles, ::Val{:SFe}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds specific_degredation_rate(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * fields.SFe[i, j, k] + +@inline degredation(poc::TwoCompartementCarbonIronParticles, ::Val{:BFe}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds specific_degredation_rate(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * fields.BFe[i, j, k] + +@inline function iron_scavenging_rate(pom::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + λ₀ = pom.minimum_iron_scavenging_rate + λ₁ = pom.load_specific_iron_scavenging_rate + + POC = @inbounds fields.POC[i, j, k] + GOC = @inbounds fields.GOC[i, j, k] + CaCO₃ = @inbounds fields.CaCO₃[i, j, k] + PSi = @inbounds fields.PSi[i, j, k] + + return λ₀ + λ₁ * (POC + GOC + CaCO₃ + PSi) +end + +@inline function bacterial_iron_uptake(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + μ₀ = poc.maximum_bacterial_growth_rate + b = poc.temperature_sensetivity + θ = poc.maximum_iron_ratio_in_bacteria + K = poc.iron_half_saturation_for_bacteria + κ = poc.bacterial_iron_uptake_efficiency + + T = @inbounds fields.T[i, j, k] + Fe = @inbounds fields.Fe[i, j, k] + + μ = μ₀ * b^T + + Bact = bacteria_concentration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + LBact = bacteria_activity(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return μ * LBact * θ * Fe / (Fe + K) * Bact * κ +end + +@inline function iron_scavenging(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + POC = @inbounds fields.POC[i, j, k] + GOC = @inbounds fields.GOC[i, j, k] + + λFe = iron_scavenging_rate(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + Fe′ = free_iron(bgc.iron, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return λFe * (POC + GOC) * Fe′ +end diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/micro_meso_zoo_coupling.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/micro_meso_zoo_coupling.jl new file mode 100644 index 000000000..dc1cb979f --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/micro_meso_zoo_coupling.jl @@ -0,0 +1,32 @@ +using OceanBioME.Models.PISCESModel.Zooplankton: non_assimilated_waste + +@inline small_non_assimilated_waste(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + non_assimilated_waste(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline large_non_assimilated_waste(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + non_assimilated_waste(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline small_non_assimilated_iron_waste(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + non_assimilated_iron_waste(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline large_non_assimilated_iron_waste(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + non_assimilated_iron_waste(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline small_mortality(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + mortality(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline large_mortality(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + linear_mortality(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline small_mortality_iron(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + mortality(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) * zoo.micro.iron_ratio + +@inline large_mortality_iron(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + linear_mortality(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) * zoo.meso.iron_ratio + +@inline total_grazing(zoo::MicroAndMeso, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (grazing(zoo, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + flux_feeding(zoo, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline total_grazing(zoo::MicroAndMeso, val_prey_name::LARGE_PARTICLE_COMPONENTS, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + flux_feeding(zoo, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/nano_diatom_coupling.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/nano_diatom_coupling.jl new file mode 100644 index 000000000..9bec8c5d2 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/nano_diatom_coupling.jl @@ -0,0 +1,124 @@ +@inline function small_mortality(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + P_linear_mortality, P_quadratic_mortality = mortality(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + R = rain_ratio(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + D_linear_mortality, = mortality(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (1 - R / 2) * (P_linear_mortality + P_quadratic_mortality) + D_linear_mortality / 2 +end + +@inline function large_mortality(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + P_linear_mortality, P_quadratic_mortality = mortality(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + R = rain_ratio(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + D_linear_mortality, D_quadratic_mortality = mortality(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return R / 2 * (P_linear_mortality + P_quadratic_mortality) + D_linear_mortality / 2 + D_quadratic_mortality +end + +@inline function small_mortality_iron(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + P_linear_mortality, P_quadratic_mortality = mortality(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + R = rain_ratio(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + D_linear_mortality, = mortality(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + P = @inbounds fields.P[i, j, k] + PFe = @inbounds fields.PFe[i, j, k] + D = @inbounds fields.D[i, j, k] + DFe = @inbounds fields.DFe[i, j, k] + + θP = PFe / (P + eps(0.0)) + θD = DFe / (D + eps(0.0)) + + return (1 - R / 2) * (P_linear_mortality + P_quadratic_mortality) * θP + D_linear_mortality * θD / 2 +end + +@inline function large_mortality_iron(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + P_linear_mortality, P_quadratic_mortality = mortality(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + R = rain_ratio(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + D_linear_mortality, D_quadratic_mortality = mortality(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + P = @inbounds fields.P[i, j, k] + PFe = @inbounds fields.PFe[i, j, k] + D = @inbounds fields.D[i, j, k] + DFe = @inbounds fields.DFe[i, j, k] + + θP = PFe / (P + eps(0.0)) + θD = DFe / (D + eps(0.0)) + + return R / 2 * (P_linear_mortality + P_quadratic_mortality) * θP + (D_linear_mortality / 2 + D_quadratic_mortality) * θD +end + +@inline function coccolithophore_nutrient_limitation(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + _, _, L_PO₄, LN = phyto.nano.nutrient_limitation(Val(:P), i, j, k, grid, bgc, phyto.nano, clock, fields, auxiliary_fields) + + Fe = @inbounds fields.Fe[i, j, k] + + L_Fe = Fe / (Fe + 0.05) + + # from NEMO we should replace LPO₄ with : zlim2 = trb(ji,jj,jk,jppo4) / ( trb(ji,jj,jk,jppo4) + concnnh4 ) + # but that has to be a typo + + return min(LN, L_Fe, L_PO₄) +end + +@inline coccolithophore_phytoplankton_factor(::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds max(one(grid), fields.P[i, j, k] / 2) + +@inline function particulate_silicate_production(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + D = @inbounds fields.D[i, j, k] + DSi = @inbounds fields.DSi[i, j, k] + + θ = DSi / (D + eps(0.0)) + + D_linear_mortality, D_quadratic_mortality = mortality(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + total_grazing = grazing(bgc.zooplankton, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (total_grazing + D_linear_mortality + D_quadratic_mortality) * θ +end + +@inline function calcite_production(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + R = rain_ratio(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + linear_mortality, quadratic_mortality = mortality(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + total_grazing_loss = calcite_loss(bgc.zooplankton, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return R * (total_grazing_loss + (linear_mortality + quadratic_mortality) / 2) +end + +@inline function rain_ratio(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + r = phyto.base_rain_ratio + + T = @inbounds fields.T[i, j, k] + PAR = @inbounds auxiliary_fields.PAR[i, j, k] + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + + L_CaCO₃ = coccolithophore_nutrient_limitation(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + phytoplankton_concentration_factor = + coccolithophore_phytoplankton_factor(phyto, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + low_light_factor = max(0, PAR - 1) / (4 + PAR) + high_light_factor = 30 / (30 + PAR) + + # modified from origional as it goes negative and does not achieve goal otherwise + low_temperature_factor = max(0, T / (T + 0.1)) + high_temperature_factor = 1 + exp(-(T - 10)^2 / 25) + + 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 \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/particulate_organic_matter.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/particulate_organic_matter.jl new file mode 100644 index 000000000..f517cb0a2 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/particulate_organic_matter.jl @@ -0,0 +1,22 @@ +module ParticulateOrganicMatter + +export TwoCompartementCarbonIronParticles + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: + degredation, aggregation, free_iron, PISCES, anoxia_factor, mortality +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: aggregation_of_colloidal_iron +using OceanBioME.Models.PISCESModel.Phytoplankton: dissolved_exudate, NanoAndDiatoms +using OceanBioME.Models.PISCESModel.Zooplankton: + organic_excretion, upper_trophic_excretion, grazing, MicroAndMeso, upper_trophic_fecal_production, + upper_trophic_fecal_iron_production, calcite_loss, flux_feeding, linear_mortality, + non_assimilated_iron_waste, bacteria_concentration, bacteria_activity + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers, biogeochemical_drift_velocity +import OceanBioME.Models.PISCESModel: degredation, aggregation, flux_rate +import OceanBioME.Models.PISCESModel.Zooplankton: edible_flux_rate, edible_iron_flux_rate + +include("two_size_class.jl") + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/silicate.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/silicate.jl new file mode 100644 index 000000000..66c260c7b --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/silicate.jl @@ -0,0 +1,48 @@ +@inline function (bgc::TwoCompartementPOCPISCES)(i, j, k, grid, val_name::Val{:PSi}, clock, fields, auxiliary_fields) + # this generalisation still assumes that we're only getting PSi from phytoplankton being grazed, will need changes if zooplankton get Si compartment + + phytoplankton_production = particulate_silicate_production(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolution = particulate_silicate_dissolution(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return phytoplankton_production - dissolution +end + +@inline function particulate_silicate_dissolution(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + PSi = @inbounds fields.PSi[i, j, k] + Si = @inbounds fields.Si[i, j, k] + + T = @inbounds fields.T[i, j, k] + + λₗ = poc.fast_dissolution_rate_of_silicate + λᵣ = poc.slow_dissolution_rate_of_silicate + + χ = particulate_silicate_liable_fraction(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + λ₀ = χ * λₗ + (1 - χ) * λᵣ + + equilibrium_silicate = 10^(6.44 - 968 / (T + 273.15)) + silicate_saturation = (equilibrium_silicate - Si) / equilibrium_silicate + + λ = λ₀ * (0.225 * (1 + T/15) * silicate_saturation + 0.775 * ((1 + T/400)^4 * silicate_saturation)^9) + + return λ * PSi # assuming the Diss_Si is typo in Aumont 2015, consistent with Aumont 2005 +end + +@inline function particulate_silicate_liable_fraction(poc, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + χ₀ = poc.base_liable_silicate_fraction + λₗ = poc.fast_dissolution_rate_of_silicate + λᵣ = poc.slow_dissolution_rate_of_silicate + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + zₑᵤ = @inbounds auxiliary_fields.zₑᵤ[i, j, k] + + # this isn't actually correct since wGOC isn't constant but nm + wGOC = ℑzᵃᵃᶜ(i, j, k, grid, auxiliary_fields.wGOC) + + zₘ = min(zₘₓₗ, zₑᵤ) + + return χ₀ * ifelse(z >= zₘ, 1, exp((λₗ - λᵣ) * (zₘ - z) / wGOC)) +end diff --git a/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/two_size_class.jl b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/two_size_class.jl new file mode 100644 index 000000000..23ef232e1 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/particulate_organic_matter/two_size_class.jl @@ -0,0 +1,99 @@ +using Oceananigans.Fields: ZeroField +using Oceananigans.Grids: znode, Center +using Oceananigans.Operators: ℑzᵃᵃᶜ + +""" + TwoCompartementCarbonIronParticles + +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 TwoCompartementCarbonIronParticles{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 + bacterial_iron_uptake_efficiency :: FT = 0.16 # + small_fraction_of_bacterially_consumed_iron :: FT = 0.12 / bacterial_iron_uptake_efficiency + large_fraction_of_bacterially_consumed_iron :: FT = 0.04 / bacterial_iron_uptake_efficiency + 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 + + # calcite + base_calcite_dissolution_rate :: FT = 0.197 / day # 1 / s + calcite_dissolution_exponent :: FT = 1.0 # + + # iron in particles + 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 + +const TwoCompartementPOCPISCES = PISCES{<:Any, <:Any, <:Any, <:TwoCompartementCarbonIronParticles} + +required_biogeochemical_tracers(::TwoCompartementCarbonIronParticles) = (:POC, :GOC, :SFe, :BFe, :PSi, :CaCO₃) + +@inline edible_flux_rate(poc, i, j, k, grid, fields, auxiliary_fields) = + flux_rate(Val(:POC), i, j, k, grid, fields, auxiliary_fields) + flux_rate(Val(:GOC), i, j, k, grid, fields, auxiliary_fields) +@inline edible_iron_flux_rate(poc, i, j, k, grid, fields, auxiliary_fields) = + flux_rate(Val(:SFe), i, j, k, grid, fields, auxiliary_fields) + flux_rate(Val(:BFe), i, j, k, grid, fields, auxiliary_fields) + +@inline flux_rate(::Val{:POC}, i, j, k, grid, fields, auxiliary_fields) = @inbounds fields.POC[i, j, k] * ℑzᵃᵃᶜ(i, j, k, grid, auxiliary_fields.wPOC) +@inline flux_rate(::Val{:GOC}, i, j, k, grid, fields, auxiliary_fields) = @inbounds fields.GOC[i, j, k] * ℑzᵃᵃᶜ(i, j, k, grid, auxiliary_fields.wGOC) +@inline flux_rate(::Val{:SFe}, i, j, k, grid, fields, auxiliary_fields) = @inbounds fields.SFe[i, j, k] * ℑzᵃᵃᶜ(i, j, k, grid, auxiliary_fields.wPOC) +@inline flux_rate(::Val{:BFe}, i, j, k, grid, fields, auxiliary_fields) = @inbounds fields.BFe[i, j, k] * ℑzᵃᵃᶜ(i, j, k, grid, auxiliary_fields.wGOC) + +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::TwoCompartementPOCPISCES, ::SMALL_PARTICLE_COMPONENTS) = + (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.POC) + +biogeochemical_drift_velocity(bgc::TwoCompartementPOCPISCES, ::LARGE_PARTICLE_COMPONENTS) = + (u = ZeroField(), v = ZeroField(), w = bgc.sinking_velocities.GOC) + +@inline function aggregation(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + a₁, a₂, a₃, a₄ = poc.aggregation_parameters + + backgroound_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + + POC = @inbounds fields.POC[i, j, k] + GOC = @inbounds fields.GOC[i, j, k] + + shear = ifelse(z < zₘₓₗ, backgroound_shear, mixed_layer_shear) + + return shear * (a₁ * POC^2 + a₂ * POC * GOC) + a₃ * POC * GOC + a₄ * POC^2 +end + +@inline function specific_degredation_rate(poc::TwoCompartementCarbonIronParticles, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + λ₀ = poc.base_breakdown_rate + b = poc.temperature_sensetivity + + O₂ = @inbounds fields.O₂[i, j, k] + T = @inbounds fields.T[i, j, k] + + ΔO₂ = anoxia_factor(bgc, O₂) + + return λ₀ * b^T * (1 - 0.45 * ΔO₂) +end + +include("carbon.jl") +include("iron.jl") +include("silicate.jl") +include("calcite.jl") +include("nano_diatom_coupling.jl") +include("micro_meso_zoo_coupling.jl") \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/phosphate.jl b/src/Models/AdvectedPopulations/PISCES/phosphate.jl new file mode 100644 index 000000000..3a0221214 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phosphate.jl @@ -0,0 +1,35 @@ +module Phosphates + +export Phosphate + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.DissolvedOrganicMatter: degredation + +using OceanBioME.Models.PISCESModel.Phytoplankton: total_production + +using OceanBioME.Models.PISCESModel.Zooplankton: inorganic_excretion, upper_trophic_respiration + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers + +struct Phosphate end + +required_biogeochemical_tracers(::Phosphate) = tuple(:PO₄) + +const PISCESPhosphate = PISCES{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Phosphate} + +@inline function (bgc::PISCESPhosphate)(i, j, k, grid, val_name::Val{:PO₄}, clock, fields, auxiliary_fields) + θ = bgc.phosphate_redfield_ratio + + phytoplankton_uptake = total_production(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + grazing_waste = inorganic_excretion(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + respiration_product = upper_trophic_respiration(bgc.zooplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + remineralisation = degredation(bgc.dissolved_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return θ * (grazing_waste + respiration_product + remineralisation - phytoplankton_uptake) +end + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton/growth_rate.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/growth_rate.jl new file mode 100644 index 000000000..c4c89801a --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/growth_rate.jl @@ -0,0 +1,165 @@ +abstract type BaseProduction end + +@inline function (μ::BaseProduction)(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields, 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₁ = @inbounds auxiliary_fields.PAR₁[i, j, k] + PAR₂ = @inbounds auxiliary_fields.PAR₂[i, j, k] + PAR₃ = @inbounds auxiliary_fields.PAR₃[i, j, k] + + zₑᵤ = @inbounds auxiliary_fields.zₑᵤ[i, j, k] + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + κ = @inbounds auxiliary_fields.κ[i, j, k] + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + + PAR = β₁ * PAR₁ + β₂ * PAR₂ + β₃ * PAR₃ + + φ = bgc.latitude(i, j, k, grid) + day_length = bgc.day_length(φ, clock.time) + + 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)) + + fₗ = light_limitation(μ, I, IChl, T, PAR, day_length, L, α) + + return μᵢ * f₁ * f₂ * fₗ * 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, val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + α₀ = growth_rate.initial_slope_of_PI_curve + β = growth_rate.low_light_adaptation + β₁ = phyto.blue_light_absorption + β₂ = phyto.green_light_absorption + β₃ = phyto.red_light_absorption + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + PAR₁ = @inbounds auxiliary_fields.PAR₁[i, j, k] + PAR₂ = @inbounds auxiliary_fields.PAR₂[i, j, k] + PAR₃ = @inbounds auxiliary_fields.PAR₃[i, j, k] + + PAR = β₁ * PAR₁ + β₂ * PAR₂ + β₃ * PAR₃ + + φ = bgc.latitude(i, j, k, grid) + + day_length = bgc.day_length(clock.time, φ) + + f₁ = 1.5 * day_length / (day_length + 0.5day) + + L, = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + μ = growth_rate(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields, L) + + μ̌ = μ / f₁ * day_length + + α = α₀ * (1 + β * exp(-PAR)) + + 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/phytoplankton/mixed_mondo.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/mixed_mondo.jl new file mode 100644 index 000000000..56807ea6c --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/mixed_mondo.jl @@ -0,0 +1,234 @@ +using Oceananigans.Grids: znode, Center + +""" + MixedMondo + +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 MixedMondo{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 + + half_saturation_for_iron_uptake :: FT # μmol Fe / m³ + + threshold_for_size_dependency :: FT = 1.0 # mmol C / m³ + size_ratio :: FT = 3.0 # +end + +required_biogeochemical_tracers(phyto::MixedMondo, base) = + (base, Symbol(base, :Chl), Symbol(base, :Fe), + ifelse(phyto.nutrient_limitation.silicate_limited, (Symbol(base, :Si), ), ())...) + +##### +##### Production/mortality functions +##### + +@inline function carbon_growth(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + # production + δ = phyto.exudated_fracton + + μI = total_production(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (1 - δ) * μI +end + +@inline function chlorophyll_growth(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + # production + δ = phyto.exudated_fracton + + θ₀ = phyto.minimum_chlorophyll_ratio + θ₁ = phyto.maximum_chlorophyll_ratio + + μ, ρ = production_and_energy_assimilation_absorption_ratio(phyto.growth_rate, val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + return (1 - δ) * 12 * (θ₀ + (θ₁ - θ₀) * ρ) * μ * I +end + +# production (we already account for the (1 - δ) term because it just goes straight back into Fe) +@inline iron_growth(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + iron_uptake(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline silicate_growth(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + silicate_uptake(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +##### +##### Underlying parameterisations +##### + +@inline function mortality(phyto::MixedMondo, bgc, z, I, zₘₓₗ, L) + K = phyto.mortality_half_saturation + m = phyto.linear_mortality_rate + + background_shear = bgc.background_shear + mixed_layer_shear = bgc.mixed_layer_shear + + linear_mortality = m * I / (I + K) * I + + w₀ = phyto.base_quadratic_mortality + w₁ = phyto.maximum_quadratic_mortality + + w = w₀ + w₁ * 0.25 * (1 - L^2) / (0.25 + L^2) + + shear = ifelse(z < zₘₓₗ, background_shear, mixed_layer_shear) + + quadratic_mortality = shear * w * I^2 + + return linear_mortality, quadratic_mortality +end + +@inline function mortality(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + I, = phytoplankton_concentrations(val_name, i, j, k, fields) + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + L, = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + return mortality(phyto, bgc, z, I, zₘₓₗ, L) +end + +@inline function total_production(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + I, = phytoplankton_concentrations(val_name, i, j, k, fields) + + L, = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + return phyto.growth_rate(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields, L) * I +end + +@inline function iron_uptake(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + δ = phyto.exudated_fracton + θFeₘ = phyto.maximum_iron_ratio + + T = @inbounds fields.T[i, j, k] + Fe = @inbounds fields.Fe[i, j, k] + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + θFe = IFe / (I + eps(0.0)) # μmol Fe / mmol C + + L, LFe = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + μᵢ = base_production_rate(phyto.growth_rate, T) + + L₁ = iron_uptake_limitation(phyto, I, Fe) # assuming bFe = Fe + + L₂ = 4 - 4.5 * LFe / (LFe + 1) # typo in Aumount 2015 + + return (1 - δ) * θFeₘ * L₁ * L₂ * max(0, (1 - θFe / θFeₘ) / (1.05 - θFe / θFeₘ)) * μᵢ * I +end + +@inline function iron_uptake_limitation(phyto, I, Fe) + k = phyto.half_saturation_for_iron_uptake + + K = k * size_factor(phyto, I) + + return Fe / (Fe + K + eps(0.0)) +end + +@inline function size_factor(phyto, I) + Iₘ = phyto.threshold_for_size_dependency + S = phyto.size_ratio + + I₁ = min(I, Iₘ) + I₂ = max(0, I - Iₘ) + + return (I₁ + S * I₂) / (I₁ + I₂ + eps(0.0)) +end + +@inline function silicate_uptake(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + δ = phyto.exudated_fracton + K₁ = phyto.silicate_half_saturation + K₂ = phyto.enhanced_silicate_half_saturation + θ₀ = phyto.optimal_silicate_ratio + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + Si = @inbounds fields.Si[i, j, k] + + L, LFe, LPO₄, LN = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + μ = phyto.growth_rate(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields, L) + + μᵢ = base_production_rate(phyto.growth_rate, T) + + L₁ = Si / (Si + K₁ + eps(0.0)) + + # enhanced silication in southern ocean + φ = bgc.latitude(i, j, k, grid) + + L₂ = ifelse(φ < 0, Si^3 / (Si^3 + K₂^3), 0) + + F₁ = min(μ / (μᵢ * L + eps(0.0)), LFe, LPO₄, LN) + + F₂ = min(1, 2.2 * max(0, L₁ - 0.5)) + + θ₁ = θ₀ * L₁ * min(5.4, (4.4 * exp(-4.23 * F₁) * F₂ + 1) * (1 + 2 * L₂)) + + return (1 - δ) * θ₁ * μ * I +end + +@inline function uptake(phyto::MixedMondo, val_name, ::Val{:NO₃}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + _, _, _, LN, LNO₃ = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + μI = total_production(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return μI * LNO₃ / (LN + eps(0.0)) +end + +@inline function uptake(phyto::MixedMondo, val_name, ::Val{:NH₄}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + _, _, _, LN, _, LNH₄ = phyto.nutrient_limitation(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + + μI = total_production(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return μI * LNH₄ / (LN + eps(0.0)) +end + +@inline uptake(phyto::MixedMondo, val_name, ::Val{:Fe}, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + iron_uptake(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline function dissolved_exudate(phyto::MixedMondo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + δ = phyto.exudated_fracton + + μI = total_production(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return δ * μI +end diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton/mixed_mondo_nano_diatoms.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/mixed_mondo_nano_diatoms.jl new file mode 100644 index 000000000..833160801 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/mixed_mondo_nano_diatoms.jl @@ -0,0 +1,111 @@ +function MixedMondoNanoAndDiatoms(; nano = MixedMondo(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, + 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, + half_saturation_for_iron_uptake = 1.0), + diatoms = MixedMondo(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, + 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, + half_saturation_for_iron_uptake = 3.0)) + + return NanoAndDiatoms(; nano, diatoms) +end + +const NANO_PHYTO = Union{Val{:P}, Val{:PChl}, Val{:PFe}} +const DIATOMS = Union{Val{:D}, Val{:DChl}, Val{:DFe}, Val{:DSi}} + +@inline phytoplankton_concentrations(::NANO_PHYTO, i, j, k, fields) = @inbounds fields.P[i, j, k], fields.PChl[i, j, k], fields.PFe[i, j, k] +@inline phytoplankton_concentrations(::DIATOMS, i, j, k, fields) = @inbounds fields.D[i, j, k], fields.DChl[i, j, k], fields.DFe[i, j, k] + +@inline carbon_name(::NANO_PHYTO) = Val(:P) +@inline carbon_name(::DIATOMS) = Val(:D) + +@inline parameterisation(::NANO_PHYTO, phyto::NanoAndDiatoms) = phyto.nano +@inline parameterisation(::DIATOMS, phyto::NanoAndDiatoms) = phyto.diatoms + +# I think these could be abstracted more so that we have a few simple functions in nano_and_diatoms +# and most only exist in `mixed_mondo.jl` +# also maybe should be dispatched on PISCES{NanoAndDiatoms{MixedMondo, MixedMondo}} +@inline function (bgc::PISCES{<:NanoAndDiatoms})(i, j, k, grid, val_name::Union{Val{:P}, Val{:D}}, clock, fields, auxiliary_fields) + phyto = parameterisation(val_name, bgc.phytoplankton) + + growth = carbon_growth(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + linear_mortality, quadratic_mortality = mortality(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + death = (linear_mortality + quadratic_mortality) + + getting_grazed = grazing(bgc.zooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return growth - death - getting_grazed +end + +@inline function (bgc::PISCES{<:NanoAndDiatoms})(i, j, k, grid, val_name::Union{Val{:PChl}, Val{:DChl}}, clock, fields, auxiliary_fields) + phyto = parameterisation(val_name, bgc.phytoplankton) + + growth = chlorophyll_growth(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + θChl = IChl / (12 * I + eps(0.0)) + + linear_mortality, quadratic_mortality = mortality(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + death = (linear_mortality + quadratic_mortality) + + getting_grazed = grazing(bgc.zooplankton, carbon_name(val_name), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return growth - (death + getting_grazed) * θChl * 12 +end + +@inline function (bgc::PISCES{<:NanoAndDiatoms})(i, j, k, grid, val_name::Union{Val{:PFe}, Val{:DFe}}, clock, fields, auxiliary_fields) + phyto = parameterisation(val_name, bgc.phytoplankton) + + growth = iron_growth(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + θFe = IFe / (I + eps(0.0)) + + linear_mortality, quadratic_mortality = mortality(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + death = (linear_mortality + quadratic_mortality) + + getting_grazed = grazing(bgc.zooplankton, carbon_name(val_name), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return growth - (death + getting_grazed) * θFe +end + +@inline function (bgc::PISCES{<:NanoAndDiatoms})(i, j, k, grid, val_name::Val{:DSi}, clock, fields, auxiliary_fields) + phyto = parameterisation(val_name, bgc.phytoplankton) + + growth = silicate_growth(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + D = @inbounds fields.D[i, j, k] + DSi = @inbounds fields.DSi[i, j, k] + + θSi = DSi / (D + eps(0.0)) + + linear_mortality, quadratic_mortality = mortality(phyto, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + death = (linear_mortality + quadratic_mortality) + + getting_grazed = grazing(bgc.zooplankton, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return growth - (death + getting_grazed) * θSi +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton/nano_and_diatoms.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/nano_and_diatoms.jl new file mode 100644 index 000000000..a0dacb2ee --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/nano_and_diatoms.jl @@ -0,0 +1,32 @@ +@kwdef struct NanoAndDiatoms{N, D, FT} + nano :: N + diatoms :: D + base_rain_ratio :: FT = 0.3 +end + +required_biogeochemical_tracers(phyto::NanoAndDiatoms) = (required_biogeochemical_tracers(phyto.nano, :P)..., + required_biogeochemical_tracers(phyto.diatoms, :D)...) + +@inline dissolved_exudate(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (dissolved_exudate(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolved_exudate(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline uptake(phyto::NanoAndDiatoms, val_uptake_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (uptake(phyto.nano, Val(:P), val_uptake_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + uptake(phyto.diatoms, Val(:D), val_uptake_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline function nitrogen_availability_limitation(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + _, _, _, LN = phyto.nano.nutrient_limitation(Val(:P), i, j, k, grid, bgc, phyto.nano, clock, fields, auxiliary_fields) + + return LN +end + +@inline base_production_rate(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + @inbounds base_production_rate(phyto.nano.growth_rate, fields.T[i, j, k]) + +@inline silicate_uptake(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + silicate_uptake(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline total_production(phyto::NanoAndDiatoms, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (total_production(phyto.nano, Val(:P), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + total_production(phyto.diatoms, Val(:D), i, j, k, grid, bgc, clock, fields, auxiliary_fields)) \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton/nutrient_limitation.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/nutrient_limitation.jl new file mode 100644 index 000000000..92bc393ab --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/nutrient_limitation.jl @@ -0,0 +1,73 @@ +""" + 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³ + 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³ +end + +@inline function (L::NitrogenIronPhosphateSilicateLimitation)(val_name, i, j, k, grid, bgc, phyto, clock, fields, auxiliary_fields) + 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 + + I, IChl, IFe = phytoplankton_concentrations(val_name, i, j, k, fields) + + NO₃ = @inbounds fields.NO₃[i, j, k] + NH₄ = @inbounds fields.NH₄[i, j, k] + PO₄ = @inbounds fields.PO₄[i, j, k] + Si = @inbounds fields.Si[i, j, k] + + Si′ = @inbounds bgc.silicate_climatology[i, j, k] + + # 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(phyto, I) + + Kₙₒ = kₙₒ * K̄ + 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₃) # 1 / 1 to 1/10^3 / 1 + + 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)) diff --git a/src/Models/AdvectedPopulations/PISCES/phytoplankton/phytoplankton.jl b/src/Models/AdvectedPopulations/PISCES/phytoplankton/phytoplankton.jl new file mode 100644 index 000000000..98278e450 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/phytoplankton/phytoplankton.jl @@ -0,0 +1,20 @@ +module Phytoplankton + +export NanoAndDiatoms, MixedMondoPhytoplankton, MixedMondoNanoAndDiatoms + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.Zooplankton: grazing + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers +import OceanBioME.Models.PISCESModel: mortality + +include("nano_and_diatoms.jl") +include("mixed_mondo.jl") +include("growth_rate.jl") +include("nutrient_limitation.jl") +include("mixed_mondo_nano_diatoms.jl") + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/show_methods.jl b/src/Models/AdvectedPopulations/PISCES/show_methods.jl new file mode 100644 index 000000000..88b2b3f14 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/show_methods.jl @@ -0,0 +1,90 @@ +using OceanBioME.Models.PISCESModel.Phytoplankton: MixedMondo +using OceanBioME.Models.PISCESModel.Zooplankton: QualityDependantZooplankton + +summary(bgc::PISCES) = string("PISCES biogeochemical model ($(length(required_biogeochemical_tracers(bgc))-2) tracers)") # hack to exclude temp and salinity + +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 Phytoplankton: $(summary(bgc.phytoplankton))" + + output *= "\n Zooplankton: $(summary(bgc.zooplankton))" + + 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 Inorganic carbon: $(summary(bgc.inorganic_carbon))" + + 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 + +summary(phyto::NanoAndDiatoms{<:Any, <:Any, FT}) where FT = + string("NanoAndDiatoms{$(summary(phyto.nano)), $(summary(phyto.diatoms)), $FT} - $(required_biogeochemical_tracers(phyto))") + +summary(phyto::NanoAndDiatoms{<:MixedMondo, <:MixedMondo, FT}) where FT = + string("MixedMondo-NanoAndDiatoms{$FT} - $(required_biogeochemical_tracers(phyto))") + +summary(::MixedMondo) = string("MixedMondo") + +summary(zoo::MicroAndMeso{<:QualityDependantZooplankton, <:QualityDependantZooplankton, FT}) where FT = + string("QualityDependantZooplankton-MicroAndMeso{$FT} - $(required_biogeochemical_tracers(zoo))") + +summary(zoo::MicroAndMeso{<:Any, <:Any, FT}) where FT = + string("MicroAndMeso {$(summary(zoo.micro)), $(summary(zoo.meso)), $FT} - $(required_biogeochemical_tracers(zoo))") + +summary(::QualityDependantZooplankton) = string("QualityDependantZooplankton") + +summary(::DissolvedOrganicCarbon{FT}) where FT = string("DissolvedOrganicCarbon{$FT} - DOC") + +summary(pom::TwoCompartementCarbonIronParticles{FT}) where FT = + string("TwoCompartementCarbonIronParticles{$FT} - $(required_biogeochemical_tracers(pom))") + +summary(::NitrateAmmonia{FT}) where FT = string("NitrateAmmonia{$FT} - (NO₃, NH₄)") +summary(::SimpleIron{FT}) where FT = string("SimpleIron{$FT} - Fe") +summary(::Oxygen{FT}) where FT = string("Oxygen{$FT} - O₂ (mmol O₂ / m³)") +summary(::Silicate) = string("Silicate - Si") +summary(::Phosphate) = string("Phosphate - PO₄") +summary(::InorganicCarbon) = string("InorganicCarbon - (DIC, Alk)") + +summary(::ModelLatitude) = string("ModelLatitude") +summary(lat::PrescribedLatitude{FT}) where FT = string("PrescribedLatitude{FT} $(lat.latitude)°") + +# TODO: add show methods \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/silicate.jl b/src/Models/AdvectedPopulations/PISCES/silicate.jl new file mode 100644 index 000000000..2e5382298 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/silicate.jl @@ -0,0 +1,28 @@ +module Silicates + +export Silicate + +using OceanBioME.Models.PISCESModel: PISCES + +using OceanBioME.Models.PISCESModel.ParticulateOrganicMatter: + particulate_silicate_dissolution + +using OceanBioME.Models.PISCESModel.Phytoplankton: silicate_uptake + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers + +struct Silicate end + +required_biogeochemical_tracers(::Silicate) = tuple(:Si) + +const PISCESSilicate = PISCES{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Silicate} + +@inline function (bgc::PISCESSilicate)(i, j, k, grid, ::Val{:Si}, clock, fields, auxiliary_fields) + consumption = silicate_uptake(bgc.phytoplankton, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + dissolution = particulate_silicate_dissolution(bgc.particulate_organic_matter, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return dissolution - consumption +end + +end # module \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/update_state.jl b/src/Models/AdvectedPopulations/PISCES/update_state.jl new file mode 100644 index 000000000..f01a44f92 --- /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/defaults.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/defaults.jl new file mode 100644 index 000000000..64b7698c9 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/defaults.jl @@ -0,0 +1,61 @@ +# this file sets up the default configuration of Z and M which graze on P, D, (Z, ) and POC +function MicroAndMesoZooplankton(; + micro = QualityDependantZooplankton(maximum_grazing_rate = 3/day, + food_preferences = (P = 1.0, D = 0.5, POC = 0.1, Z = 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), + meso = QualityDependantZooplankton(maximum_grazing_rate = 0.75/day, + food_preferences = (P = 0.3, D = 1.0, POC = 0.3, Z = 1.0), + quadratic_mortality = 0.03/day, + linear_mortality = 0.005/day, + minimum_growth_efficiency = 0.35, + maximum_flux_feeding_rate = 2e3 / 1e6, + undissolved_calcite_fraction = 0.75, + iron_ratio = 0.015)) + + return MicroAndMeso(; micro, meso) +end + +@inline concentration(::Val{:P}, i, j, k, fields) = @inbounds fields.P[i, j, k] +@inline concentration(::Val{:D}, i, j, k, fields) = @inbounds fields.D[i, j, k] +@inline concentration(::Val{:Z}, i, j, k, fields) = @inbounds fields.Z[i, j, k] +@inline concentration(::Val{:POC}, i, j, k, fields) = @inbounds fields.POC[i, j, k] + +@inline iron_ratio(::Val{:P}, i, j, k, bgc, fields) = @inbounds fields.PFe[i, j, k] / (fields.P[i, j, k] + eps(0.0)) +@inline iron_ratio(::Val{:D}, i, j, k, bgc, fields) = @inbounds fields.DFe[i, j, k] / (fields.D[i, j, k] + eps(0.0)) +@inline iron_ratio(::Val{:Z}, i, j, k, bgc, fields) = @inbounds bgc.zooplankton.micro.iron_ratio +@inline iron_ratio(::Val{:POC}, i, j, k, bgc, fields) = @inbounds fields.SFe[i, j, k] / (fields.POC[i, j, k] + eps(0.0)) + +@inline grazing_preference(val_prey_name, preferences) = 0 +@inline grazing_preference(::Val{:P}, preferences) = preferences.P +@inline grazing_preference(::Val{:D}, preferences) = preferences.D +@inline grazing_preference(::Val{:Z}, preferences) = preferences.Z +@inline grazing_preference(::Val{:POC}, preferences) = preferences.POC + +# TODO: this should dispatch on PISCES{<:NanoAndDiatoms, <:MicroAndMeso, <:Any, <:Two...} but the phyto and POC +# classes are not yet defined +@inline prey_names(::PISCES{<:Any, <:MicroAndMeso}, ::Val{:Z}) = (:P, :D, :POC) +@inline prey_names(::PISCES{<:Any, <:MicroAndMeso}, ::Val{:M}) = (:P, :D, :Z, :POC) + +# TODO: move these somewhere else so they can be dispatched on ::PISCES{<:NanoAndDiatoms, <:MicroAndMeso, <:Any, <:TwoCompartementCarbonIronParticles} +@inline function extract_food_availability(::PISCES, i, j, k, fields, ::NTuple{N}) where N + P = @inbounds fields.P[i, j, k] + D = @inbounds fields.D[i, j, k] + POC = @inbounds fields.POC[i, j, k] + Z = @inbounds fields.Z[i, j, k] + + return (; P, D, POC, Z) +end + +@inline function extract_iron_availability(bgc::PISCES, i, j, k, fields, ::NTuple{N}) where N + P = @inbounds fields.PFe[i, j, k] / (fields.P[i, j, k] + eps(0.0)) + D = @inbounds fields.DFe[i, j, k] / (fields.D[i, j, k] + eps(0.0)) + POC = @inbounds fields.SFe[i, j, k] / (fields.POC[i, j, k] + eps(0.0)) + Z = bgc.zooplankton.micro.iron_ratio + + return (; P, D, POC, Z) +end \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/food_quality_dependant.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/food_quality_dependant.jl new file mode 100644 index 000000000..2b705ea9c --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/food_quality_dependant.jl @@ -0,0 +1,204 @@ +""" + QualityDependantZooplankton + +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 QualityDependantZooplankton{FT, FP} + temperature_sensetivity :: FT = 1.079 # + maximum_grazing_rate :: FT # 1 / s + + food_preferences :: FP + + 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 + + # this should be called inorganic excretion factor + dissolved_excretion_fraction :: FT = 0.6 # + undissolved_calcite_fraction :: FT # +end + +required_biogeochemical_tracers(::QualityDependantZooplankton, name_base) = tuple(name_base) + +@inline function growth_death(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + gI, e = grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + gfI = flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + mI = mortality(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return e * (gI + gfI) - mI +end + +# fallback +@inline extract_food_availability(bgc, i, j, k, fields, names::NTuple{N}) where N = + ntuple(n -> concentration(Val(names[n]), i, j, k, fields), Val(N)) + +@inline extract_iron_availability(bgc, i, j, k, fields, names::NTuple{N}) where N = + ntuple(n -> iron_ratio(Val(names[n]), i, j, k, bgc, fields), Val(N)) + +@inline function grazing(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + # food quantity + g₀ = zoo.maximum_grazing_rate + b = zoo.temperature_sensetivity + p = zoo.food_preferences + food = prey_names(bgc, val_name) + J = zoo.specific_food_thresehold_concentration + K = zoo.grazing_half_saturation + food_threshold_concentration = zoo.food_threshold_concentration + + N = length(food) + + I = zooplankton_concentration(val_name, i, j, k, fields) + T = @inbounds fields.T[i, j, k] + + base_grazing_rate = g₀ * b ^ T + + food_availability = extract_food_availability(bgc, i, j, k, fields, food) + + total_food = sum(ntuple(n->food_availability[n] * p[n], Val(N))) + + available_total_food = sum(ntuple(n->max(zero(grid), (food_availability[n] - J)) * p[n], Val(N))) + + concentration_limited_grazing = max(0, available_total_food - min(available_total_food / 2, food_threshold_concentration)) + + total_specific_grazing = base_grazing_rate * concentration_limited_grazing / (K + total_food) + + # food quality + θFe = zoo.iron_ratio + e₀ = zoo.minimum_growth_efficiency + σ = zoo.non_assililated_fraction + + iron_availabillity = extract_iron_availability(bgc, i, j, k, fields, food) + + total_iron = sum(ntuple(n->iron_availabillity[n] * p[n], Val(N))) + + iron_grazing_ratio = total_iron / (θFe * total_specific_grazing + eps(0.0)) + + food_quality = min(1, iron_grazing_ratio) + + growth_efficiency = food_quality * min(e₀, (1 - σ) * iron_grazing_ratio) + + return total_specific_grazing * I, growth_efficiency +end + +@inline function flux_feeding(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + g₀ = zoo.maximum_flux_feeding_rate + b = zoo.temperature_sensetivity + + I = zooplankton_concentration(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + + sinking_flux = edible_flux_rate(bgc.particulate_organic_matter, i, j, k, grid, fields, auxiliary_fields) + + base_flux_feeding_rate = g₀ * b ^ T + + total_specific_flux_feeding = base_flux_feeding_rate * sinking_flux + + return total_specific_flux_feeding * I +end + +@inline function mortality(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + b = zoo.temperature_sensetivity + m₀ = zoo.quadratic_mortality + Kₘ = zoo.mortality_half_saturation + r = zoo.linear_mortality + + I = zooplankton_concentration(val_name, i, j, k, fields) + T = @inbounds fields.T[i, j, k] + + O₂ = @inbounds fields.O₂[i, j, k] + + temperature_factor = b^T + + concentration_factor = I / (I + Kₘ) + + return temperature_factor * I * (m₀ * I + r * (concentration_factor + 3 * anoxia_factor(bgc, O₂))) +end + +@inline function linear_mortality(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + b = zoo.temperature_sensetivity + Kₘ = zoo.mortality_half_saturation + r = zoo.linear_mortality + + T = @inbounds fields.T[i, j, k] + O₂ = @inbounds fields.O₂[i, j, k] + I = zooplankton_concentration(val_name, i, j, k, fields) + + temperature_factor = b^T + + concentration_factor = I / (I + Kₘ) + + return temperature_factor * r * (concentration_factor + 3 * anoxia_factor(bgc, O₂)) * I +end + +##### +##### Effect on other compartements +##### + +@inline function grazing(zoo::QualityDependantZooplankton, val_name, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + g₀ = zoo.maximum_grazing_rate + b = zoo.temperature_sensetivity + p = zoo.food_preferences + food = prey_names(bgc, val_name) + J = zoo.specific_food_thresehold_concentration + K = zoo.grazing_half_saturation + food_threshold_concentration = zoo.food_threshold_concentration + + N = length(food) + + I = zooplankton_concentration(val_name, i, j, k, fields) + T = @inbounds fields.T[i, j, k] + + base_grazing_rate = g₀ * b ^ T + + food_availability = extract_food_availability(bgc, i, j, k, fields, food) + + total_food = sum(ntuple(n->food_availability[n] * p[n], Val(N))) + + available_total_food = sum(ntuple(n->max(zero(grid), (food_availability[n] - J)) * p[n], Val(N))) + + concentration_limited_grazing = max(0, available_total_food - min(available_total_food / 2, food_threshold_concentration)) + + total_specific_grazing = base_grazing_rate * concentration_limited_grazing / (K + total_food) + + P = concentration(val_prey_name, i, j, k, fields) + + return grazing_preference(val_prey_name, p) * max(0, P - J) * total_specific_grazing / (available_total_food + eps(0.0)) * I +end + +@inline function flux_feeding(zoo::QualityDependantZooplankton, val_name, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + g₀ = zoo.maximum_flux_feeding_rate + b = zoo.temperature_sensetivity + + I = zooplankton_concentration(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + + sinking_flux = flux_rate(val_prey_name, i, j, k, grid, fields, auxiliary_fields) + + base_flux_feeding_rate = g₀ * b ^ T + + total_specific_flux_feeding = base_flux_feeding_rate * sinking_flux + + return total_specific_flux_feeding * I +end + +include("grazing_waste.jl") +include("mortality_waste.jl") \ No newline at end of file diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/grazing_waste.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/grazing_waste.jl new file mode 100644 index 000000000..e7f08bfd8 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/grazing_waste.jl @@ -0,0 +1,71 @@ +include("iron_grazing.jl") + +@inline function non_assimilated_waste(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + σ = zoo.non_assililated_fraction + + gI, = grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + gfI = flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return σ * (gI + gfI) +end + +@inline function excretion(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + σ = zoo.non_assililated_fraction + + gI, e = grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + gfI = flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (1 - σ - e) * (gI + gfI) +end + +@inline function inorganic_excretion(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + γ = zoo.dissolved_excretion_fraction + + return γ * excretion(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) +end + +@inline function organic_excretion(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + γ = zoo.dissolved_excretion_fraction + + return (1 - γ) * excretion(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) +end + +@inline function non_assimilated_iron_waste(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + σ = zoo.non_assililated_fraction + + gI = iron_grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + gfI = iron_flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return σ * (gI + gfI) +end + +@inline function non_assimilated_iron(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + θ = zoo.iron_ratio + σ = zoo.non_assililated_fraction + + gI, growth_efficiency = grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + gfI = flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + zoo_assimilated_iron = θ * growth_efficiency * (gI + gfI) + + gIFe = iron_grazing(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + gfIFe = iron_flux_feeding(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + lost_to_particles = σ * (gIFe + gfIFe) + + total_iron_grazed = gIFe + gfIFe + + return total_iron_grazed - lost_to_particles - zoo_assimilated_iron # feels like a more straight forward way to write it +end + +@inline function calcite_loss(zoo::QualityDependantZooplankton, val_name, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + η = zoo.undissolved_calcite_fraction + + g = grazing(zoo, val_name, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return η * g +end diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/iron_grazing.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/iron_grazing.jl new file mode 100644 index 000000000..8ab91c55f --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/iron_grazing.jl @@ -0,0 +1,53 @@ + +@inline function iron_grazing(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + # food quantity + g₀ = zoo.maximum_grazing_rate + b = zoo.temperature_sensetivity + p = zoo.food_preferences + food = prey_names(bgc, val_name) + J = zoo.specific_food_thresehold_concentration + K = zoo.grazing_half_saturation + food_threshold_concentration = zoo.food_threshold_concentration + + N = length(food) + + I = zooplankton_concentration(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + + base_grazing_rate = g₀ * b ^ T + + food_availability = extract_food_availability(bgc, i, j, k, fields, food) + + total_food = sum(ntuple(n->food_availability[n] * p[n], Val(N))) + + available_total_food = sum(ntuple(n->max(zero(grid), (food_availability[n] - J)) * p[n], Val(N))) + + concentration_limited_grazing = max(0, available_total_food - min(available_total_food / 2, food_threshold_concentration)) + + total_specific_grazing = base_grazing_rate * concentration_limited_grazing / (K + total_food) + + iron_ratios = extract_iron_availability(bgc, i, j, k, fields, food) + + total_specific_iron_grazing = sum(ntuple(n->max(zero(grid), (food_availability[n] - J)) * p[n] * iron_ratios[n], Val(N))) * total_specific_grazing / (available_total_food + eps(0.0)) + + return total_specific_iron_grazing * I +end + +@inline function iron_flux_feeding(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + g₀ = zoo.maximum_flux_feeding_rate + b = zoo.temperature_sensetivity + + I = zooplankton_concentration(val_name, i, j, k, fields) + + T = @inbounds fields.T[i, j, k] + + sinking_flux = edible_iron_flux_rate(bgc.particulate_organic_matter, i, j, k, grid, fields, auxiliary_fields) + + base_flux_feeding_rate = g₀ * b ^ T + + total_specific_flux_feeding = base_flux_feeding_rate * sinking_flux + + return total_specific_flux_feeding * I +end + diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/micro_and_meso.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/micro_and_meso.jl new file mode 100644 index 000000000..8a9d0993d --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/micro_and_meso.jl @@ -0,0 +1,136 @@ +using Oceananigans.Grids: znode, Center + +@kwdef struct MicroAndMeso{μ, M, FT} + micro :: μ + meso :: M + + 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 # + + 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³ +end + +required_biogeochemical_tracers(zoo::MicroAndMeso) = (required_biogeochemical_tracers(zoo.micro, :Z)..., + required_biogeochemical_tracers(zoo.meso, :M)...) + +@inline zooplankton_concentration(::Val{:Z}, i, j, k, fields) = @inbounds fields.Z[i, j, k] +@inline zooplankton_concentration(::Val{:M}, i, j, k, fields) = @inbounds fields.M[i, j, k] + +@inline parameterisation(::Val{:Z}, zoo::MicroAndMeso) = zoo.micro +@inline parameterisation(::Val{:M}, zoo::MicroAndMeso) = zoo.meso + +@inline predator_parameterisation(val_name, zoo) = nothing +@inline predator_parameterisation(::Val{:Z}, zoo::MicroAndMeso) = zoo.meso + +@inline predator_name(val_name, zoo) = nothing +@inline predator_name(::Val{:Z}, zoo::MicroAndMeso) = Val(:M) + +@inline grazing(::Nothing, ::Nothing, val_prey_name, i, j, k, grid, args...) = zero(grid) + +@inline function (bgc::PISCES{<:Any, <:MicroAndMeso})(i, j, k, grid, val_name::Union{Val{:Z}, Val{:M}}, clock, fields, auxiliary_fields) + zoo = parameterisation(val_name, bgc.zooplankton) + + net_production = growth_death(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + # M preying on Z + predator_zoo = predator_parameterisation(val_name, bgc.zooplankton) + val_predator_name = predator_name(val_name, bgc.zooplankton) + + predatory_grazing = grazing(predator_zoo, val_predator_name, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return net_production - predatory_grazing +end + +@inline grazing(zoo::MicroAndMeso, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (grazing(zoo.micro, Val(:Z), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + grazing(zoo.meso, Val(:M), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline flux_feeding(zoo::MicroAndMeso, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (flux_feeding(zoo.micro, Val(:Z), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + flux_feeding(zoo.meso, Val(:M), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline inorganic_excretion(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (inorganic_excretion(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + inorganic_excretion(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline organic_excretion(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (organic_excretion(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + organic_excretion(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline non_assimilated_iron(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (non_assimilated_iron(zoo.micro, Val(:Z), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + non_assimilated_iron(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields)) + +@inline upper_trophic_excretion(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_excretion(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_respiration(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_respiration(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_dissolved_iron(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_dissolved_iron(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_fecal_production(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_fecal_production(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_fecal_iron_production(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_fecal_iron_production(zoo.meso, Val(:M), i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline function bacteria_concentration(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + bZ = zoo.microzooplankton_bacteria_concentration + bM = zoo.mesozooplankton_bacteria_concentration + a = zoo.bacteria_concentration_depth_exponent + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + zₘₓₗ = @inbounds auxiliary_fields.zₘₓₗ[i, j, k] + zₑᵤ = @inbounds auxiliary_fields.zₑᵤ[i, j, k] + + Z = @inbounds fields.Z[i, j, k] + M = @inbounds fields.M[i, j, k] + + 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 + +@inline function bacteria_activity(zoo::MicroAndMeso, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + K_DOC = zoo.doc_half_saturation_for_bacterial_activity + K_NO₃ = zoo.nitrate_half_saturation_for_bacterial_activity + K_NH₄ = zoo.ammonia_half_saturation_for_bacterial_activity + K_PO₄ = zoo.phosphate_half_saturation_for_bacterial_activity + K_Fe = zoo.iron_half_saturation_for_bacterial_activity + + NH₄ = @inbounds fields.NH₄[i, j, k] + NO₃ = @inbounds fields.NO₃[i, j, k] + PO₄ = @inbounds fields.PO₄[i, j, k] + Fe = @inbounds fields.Fe[i, j, k] + DOC = @inbounds fields.DOC[i, j, k] + + 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 + +@inline calcite_loss(zoo::MicroAndMeso, val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (calcite_loss(zoo.micro, Val(:Z), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + calcite_loss(zoo.meso, Val(:M), val_prey_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields)) diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/mortality_waste.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/mortality_waste.jl new file mode 100644 index 000000000..baa3c4898 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/mortality_waste.jl @@ -0,0 +1,41 @@ + +@inline function upper_trophic_excretion(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + γ = zoo.dissolved_excretion_fraction + + R = upper_trophic_respiration_product(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return (1 - γ) * R +end + +@inline function upper_trophic_respiration(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + γ = zoo.dissolved_excretion_fraction + + R = upper_trophic_respiration_product(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + + return γ * R +end + +@inline upper_trophic_dissolved_iron(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + zoo.iron_ratio * upper_trophic_respiration_product(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_respiration_product(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + (1 - zoo.minimum_growth_efficiency - zoo.non_assililated_fraction) * upper_trophic_waste(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_fecal_production(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + zoo.non_assililated_fraction * upper_trophic_waste(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + +@inline upper_trophic_fecal_iron_production(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) = + upper_trophic_fecal_production(zoo, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) * zoo.iron_ratio + +@inline function upper_trophic_waste(zoo::QualityDependantZooplankton, val_name, i, j, k, grid, bgc, clock, fields, auxiliary_fields) + e₀ = zoo.minimum_growth_efficiency + b = zoo.temperature_sensetivity + m₀ = zoo.quadratic_mortality + + T = @inbounds fields.T[i, j, k] + I = zooplankton_concentration(val_name, i, j, k, fields) + + temperature_factor = b^T + + return 1 / (1 - e₀) * m₀ * temperature_factor * I^2 +end diff --git a/src/Models/AdvectedPopulations/PISCES/zooplankton/zooplankton.jl b/src/Models/AdvectedPopulations/PISCES/zooplankton/zooplankton.jl new file mode 100644 index 000000000..871502a23 --- /dev/null +++ b/src/Models/AdvectedPopulations/PISCES/zooplankton/zooplankton.jl @@ -0,0 +1,19 @@ +module Zooplankton + +export MicroAndMesoZooplankton, QualityDependantZooplankton, MicroAndMeso + +using Oceananigans.Units + +using OceanBioME.Models.PISCESModel: anoxia_factor, PISCES, flux_rate + +import Oceananigans.Biogeochemistry: required_biogeochemical_tracers +import OceanBioME.Models.PISCESModel: mortality + +function edible_flux_rate end +function edible_iron_flux_rate end + +include("food_quality_dependant.jl") +include("micro_and_meso.jl") +include("defaults.jl") + +end # module \ No newline at end of file diff --git a/src/Models/CarbonChemistry/calcite_concentration.jl b/src/Models/CarbonChemistry/calcite_concentration.jl index 9fca773f2..8f2ca007f 100644 --- a/src/Models/CarbonChemistry/calcite_concentration.jl +++ b/src/Models/CarbonChemistry/calcite_concentration.jl @@ -52,17 +52,17 @@ function carbonate_concentration(cc::CarbonChemistry; return DIC * K1 * K2 / denom1 / denom2 end -function carbonate_saturation(cc::CarbonChemistry; - DIC, T, S, Alk = 0, pH = nothing, - P = nothing, - boron = 0.000232 / 10.811 * S / 1.80655, - sulfate = 0.14 / 96.06 * S / 1.80655, - fluoride = 0.000067 / 18.9984 * S / 1.80655, - calcium_ion_concentration = 0.0103 * S / 35, - silicate = 0, - phosphate = 0, - upper_pH_bound = 14, - lower_pH_bound = 0) +function calcite_saturation(cc::CarbonChemistry; + DIC, T, S, Alk = 0, pH = nothing, + P = nothing, + boron = 0.000232 / 10.811 * S / 1.80655, + sulfate = 0.14 / 96.06 * S / 1.80655, + fluoride = 0.000067 / 18.9984 * S / 1.80655, + calcium_ion_concentration = 0.0103 * S / 35, + silicate = 0, + phosphate = 0, + upper_pH_bound = 14, + lower_pH_bound = 0) CO₃²⁻ = carbonate_concentration(cc; DIC, Alk, T, S, pH, @@ -75,7 +75,8 @@ function carbonate_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 end \ No newline at end of file diff --git a/src/Models/Models.jl b/src/Models/Models.jl index f7269e855..a7b256509 100644 --- a/src/Models/Models.jl +++ b/src/Models/Models.jl @@ -4,7 +4,8 @@ export Sediments export NPZD, NutrientPhytoplanktonZooplanktonDetritus, - LOBSTER + LOBSTER, + PISCES, DepthDependantSinkingSpeed, PrescribedLatitude, ModelLatitude, PISCESModel export SLatissima @@ -26,12 +27,14 @@ include("Individuals/SLatissima.jl") include("seawater_density.jl") include("CarbonChemistry/CarbonChemistry.jl") include("GasExchange/GasExchange.jl") +include("AdvectedPopulations/PISCES/PISCES.jl") using .Sediments using .LOBSTERModel using .NPZDModel +using .PISCESModel using .SLatissimaModel using .CarbonChemistryModel using .GasExchangeModel -end # module \ No newline at end of file +end # module diff --git a/src/Models/Sediments/Sediments.jl b/src/Models/Sediments/Sediments.jl index 044df4956..eb7699e2f 100644 --- a/src/Models/Sediments/Sediments.jl +++ b/src/Models/Sediments/Sediments.jl @@ -4,12 +4,12 @@ export SimpleMultiG, InstantRemineralisation using KernelAbstractions -using OceanBioME: Biogeochemistry, BoxModelGrid +using OceanBioME: DiscreteBiogeochemistry, ContinuousBiogeochemistry, 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/Models/Sediments/coupled_timesteppers.jl b/src/Models/Sediments/coupled_timesteppers.jl index a603dede4..8cfa0d9d4 100644 --- a/src/Models/Sediments/coupled_timesteppers.jl +++ b/src/Models/Sediments/coupled_timesteppers.jl @@ -8,7 +8,11 @@ using Oceananigans.Architectures: AbstractArchitecture import Oceananigans.TimeSteppers: ab2_step!, rk3_substep! -@inline function ab2_step!(model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Biogeochemistry{<:Any, <:Any, <:FlatSediment}}, Δt) +const BGC_WITH_FLAT_SEDIMENT = Union{<:DiscreteBiogeochemistry{<:Any, <:Any, <:FlatSediment}, + <:ContinuousBiogeochemistry{<:Any, <:Any, <:FlatSediment}} + +# This is definitly type piracy +@inline function ab2_step!(model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:BGC_WITH_FLAT_SEDIMENT}, Δt) workgroup, worksize = work_layout(model.grid, :xyz) arch = model.architecture step_field_kernel! = ab2_step_field!(device(arch), workgroup, worksize) @@ -46,7 +50,7 @@ import Oceananigans.TimeSteppers: ab2_step!, rk3_substep! return nothing end -@inline function ab2_step!(model::HydrostaticFreeSurfaceModel{<:Any, <:Any, <:AbstractArchitecture, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Biogeochemistry{<:Any, <:Any, <:FlatSediment}}, Δt) +@inline function ab2_step!(model::HydrostaticFreeSurfaceModel{<:Any, <:Any, <:AbstractArchitecture, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:BGC_WITH_FLAT_SEDIMENT}, Δt) χ = model.timestepper.χ # Step locally velocity and tracers @@ -78,7 +82,7 @@ end @inbounds u[i, j, 1] += Δt * ((one_point_five + χ) * Gⁿ[i, j, 1] - (oh_point_five + χ) * G⁻[i, j, 1]) end -function rk3_substep!(model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Biogeochemistry{<:Any, <:Any, <:FlatSediment}}, Δt, γⁿ, ζⁿ) +function rk3_substep!(model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:BGC_WITH_FLAT_SEDIMENT}, Δt, γⁿ, ζⁿ) workgroup, worksize = work_layout(model.grid, :xyz) arch = model.architecture substep_field_kernel! = rk3_substep_field!(device(arch), workgroup, worksize) diff --git a/src/OceanBioME.jl b/src/OceanBioME.jl index 335a8c865..375304cb6 100644 --- a/src/OceanBioME.jl +++ b/src/OceanBioME.jl @@ -4,8 +4,9 @@ between ocean biogeochemistry, carbonate chemistry, and physics. """ module OceanBioME -# Biogeochemistry models -export Biogeochemistry, LOBSTER, NutrientPhytoplanktonZooplanktonDetritus, NPZD, redfield +# Biogeochemistry models and useful things +export Biogeochemistry, LOBSTER, PISCES, NutrientPhytoplanktonZooplanktonDetritus, NPZD, redfield +export DepthDependantSinkingSpeed, PrescribedLatitude, ModelLatitude, PISCESModel # Macroalgae models export SLatissima @@ -40,7 +41,7 @@ export ScaleNegativeTracers, ZeroNegativeTracers export ColumnField, isacolumn using Oceananigans.Architectures: architecture, device, CPU -using Oceananigans.Biogeochemistry: AbstractContinuousFormBiogeochemistry +using Oceananigans.Biogeochemistry: AbstractBiogeochemistry, AbstractContinuousFormBiogeochemistry using Oceananigans.Grids: RectilinearGrid, Flat using Adapt @@ -57,25 +58,24 @@ import Oceananigans.Biogeochemistry: required_biogeochemical_tracers, import Adapt: adapt_structure import Base: show, summary -struct Biogeochemistry{B, L, S, P, M} <: AbstractContinuousFormBiogeochemistry +struct ContinuousBiogeochemistry{B, L, S, P, M} <: AbstractContinuousFormBiogeochemistry underlying_biogeochemistry :: B light_attenuation :: L sediment :: S particles :: P modifiers :: M - - Biogeochemistry(underlying_biogeochemistry::B, - light_attenuation::L, - sediment::S, - particles::P, - modifiers::M) where {B, L, S, P, M} = - new{B, L, S, P, M}(underlying_biogeochemistry, - light_attenuation, - sediment, - particles, - modifiers) end +struct DiscreteBiogeochemistry{B, L, S, P, M} <: AbstractBiogeochemistry + underlying_biogeochemistry :: B + light_attenuation :: L + sediment :: S + particles :: P + modifiers :: M +end + +const CompleteBiogeochemistry = Union{<:ContinuousBiogeochemistry, <:DiscreteBiogeochemistry} + """ Biogeochemistry(underlying_biogeochemistry; light_attenuation = nothing, @@ -99,45 +99,61 @@ Biogeochemistry(underlying_biogeochemistry; sediment = nothing, particles = nothing, modifiers = nothing) = - Biogeochemistry(underlying_biogeochemistry, - light_attenuation, - sediment, - particles, - modifiers) + DiscreteBiogeochemistry(underlying_biogeochemistry, + light_attenuation, + sediment, + particles, + modifiers) -required_biogeochemical_tracers(bgc::Biogeochemistry) = required_biogeochemical_tracers(bgc.underlying_biogeochemistry) +Biogeochemistry(underlying_biogeochemistry::AbstractContinuousFormBiogeochemistry; + light_attenuation = nothing, + sediment = nothing, + particles = nothing, + modifiers = nothing) = + ContinuousBiogeochemistry(underlying_biogeochemistry, + light_attenuation, + sediment, + particles, + modifiers) -required_biogeochemical_auxiliary_fields(bgc::Biogeochemistry) = required_biogeochemical_auxiliary_fields(bgc.underlying_biogeochemistry) +required_biogeochemical_tracers(bgc::CompleteBiogeochemistry) = required_biogeochemical_tracers(bgc.underlying_biogeochemistry) -biogeochemical_drift_velocity(bgc::Biogeochemistry, val_name) = biogeochemical_drift_velocity(bgc.underlying_biogeochemistry, val_name) +required_biogeochemical_auxiliary_fields(bgc::CompleteBiogeochemistry) = required_biogeochemical_auxiliary_fields(bgc.underlying_biogeochemistry) -biogeochemical_auxiliary_fields(bgc::Biogeochemistry) = merge(biogeochemical_auxiliary_fields(bgc.underlying_biogeochemistry), - biogeochemical_auxiliary_fields(bgc.light_attenuation)) +biogeochemical_drift_velocity(bgc::CompleteBiogeochemistry, val_name) = biogeochemical_drift_velocity(bgc.underlying_biogeochemistry, val_name) -@inline chlorophyll(bgc::Biogeochemistry, model) = chlorophyll(bgc.underlying_biogeochemistry, model) +biogeochemical_auxiliary_fields(bgc::CompleteBiogeochemistry) = merge(biogeochemical_auxiliary_fields(bgc.underlying_biogeochemistry), + biogeochemical_auxiliary_fields(bgc.light_attenuation)) -@inline adapt_structure(to, bgc::Biogeochemistry) = adapt(to, bgc.underlying_biogeochemistry) +@inline chlorophyll(bgc::CompleteBiogeochemistry, model) = chlorophyll(bgc.underlying_biogeochemistry, model) -function update_tendencies!(bgc::Biogeochemistry, model) +@inline adapt_structure(to, bgc::ContinuousBiogeochemistry) = adapt(to, bgc.underlying_biogeochemistry) + +@inline adapt_structure(to, bgc::DiscreteBiogeochemistry) = + DiscreteBiogeochemistry(adapt(to, bgc.underlying_biogeochemistry), + adapt(to, bgc.light_attenuation), + nothing, + nothing, + nothing) + + +function update_tendencies!(bgc::CompleteBiogeochemistry, model) update_tendencies!(bgc, bgc.sediment, model) update_tendencies!(bgc, bgc.particles, model) update_tendencies!(bgc, bgc.modifiers, model) - synchronize(device(architecture(model))) end update_tendencies!(bgc, modifier, model) = nothing update_tendencies!(bgc, modifiers::Tuple, model) = [update_tendencies!(bgc, modifier, model) for modifier in modifiers] -# do we still need this for CPU kernels??? -@inline biogeochemical_transition(i, j, k, grid, bgc::Biogeochemistry, val_tracer_name, clock, fields) = - biogeochemical_transition(i, j, k, grid, bgc.underlying_biogeochemistry, val_tracer_name, clock, fields) - -@inline (bgc::Biogeochemistry)(args...) = bgc.underlying_biogeochemistry(args...) +@inline (bgc::ContinuousBiogeochemistry)(args...) = bgc.underlying_biogeochemistry(args...) -function update_biogeochemical_state!(bgc::Biogeochemistry, model) +function update_biogeochemical_state!(bgc::CompleteBiogeochemistry, model) + # TODO: change the order of arguments here since they should definitly be the other way around update_biogeochemical_state!(model, bgc.modifiers) - synchronize(device(architecture(model))) + #synchronize(device(architecture(model))) update_biogeochemical_state!(model, bgc.light_attenuation) + update_biogeochemical_state!(model, bgc.underlying_biogeochemistry) end update_biogeochemical_state!(model, modifiers::Tuple) = [update_biogeochemical_state!(model, modifier) for modifier in modifiers] @@ -162,27 +178,28 @@ Returns the redfield ratio of `tracer_name` from `bgc` when it is constant acros @inline redfield(val_tracer_name, bgc) = NaN # fallbacks -@inline redfield(i, j, k, val_tracer_name, bgc::Biogeochemistry, tracers) = redfield(i, j, k, val_tracer_name, bgc.underlying_biogeochemistry, tracers) -@inline redfield(val_tracer_name, bgc::Biogeochemistry) = redfield(val_tracer_name, bgc.underlying_biogeochemistry) -@inline redfield(val_tracer_name, bgc::Biogeochemistry, tracers) = redfield(val_tracer_name, bgc.underlying_biogeochemistry, tracers) +@inline redfield(i, j, k, val_tracer_name, bgc::CompleteBiogeochemistry, tracers) = redfield(i, j, k, val_tracer_name, bgc.underlying_biogeochemistry, tracers) +@inline redfield(val_tracer_name, bgc::CompleteBiogeochemistry) = redfield(val_tracer_name, bgc.underlying_biogeochemistry) +@inline redfield(val_tracer_name, bgc::CompleteBiogeochemistry, tracers) = redfield(val_tracer_name, bgc.underlying_biogeochemistry, tracers) @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::CompleteBiogeochemistry, 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) = +summary(bgc::CompleteBiogeochemistry) = string("Biogeochemical model based on $(summary(bgc.underlying_biogeochemistry))") +show(io::IO, model::CompleteBiogeochemistry) = print(io, summary(model.underlying_biogeochemistry), " \n", " 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/Particles/Particles.jl b/src/Particles/Particles.jl index 910b77d67..0b570dcdb 100644 --- a/src/Particles/Particles.jl +++ b/src/Particles/Particles.jl @@ -1,7 +1,7 @@ module Particles using Oceananigans: NonhydrostaticModel, HydrostaticFreeSurfaceModel -using OceanBioME: Biogeochemistry +using OceanBioME: DiscreteBiogeochemistry, ContinuousBiogeochemistry import Oceananigans.Biogeochemistry: update_tendencies! import Oceananigans.Models.LagrangianParticleTracking: update_lagrangian_particle_properties!, step_lagrangian_particles! @@ -12,18 +12,18 @@ abstract type BiogeochemicalParticles end # TODO: add model.particles passing +const BGC_WITH_PARTICLES = Union{<:DiscreteBiogeochemistry{<:Any, <:Any, <:Any, <:BiogeochemicalParticles}, + <:ContinuousBiogeochemistry{<:Any, <:Any, <:Any, <:BiogeochemicalParticles}} + @inline step_lagrangian_particles!(::Nothing, - model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, - <:Biogeochemistry{<:Any, <:Any, <:Any, <:BiogeochemicalParticles}}, + model::NonhydrostaticModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:BGC_WITH_PARTICLES}, Δt) = update_lagrangian_particle_properties!(model, model.biogeochemistry, Δt) @inline step_lagrangian_particles!(::Nothing, - model::HydrostaticFreeSurfaceModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, - <:Biogeochemistry{<:Any, <:Any, <:Any, <:BiogeochemicalParticles}, - <:Any, <:Any, <:Any, <:Any, <:Any,}, + model::HydrostaticFreeSurfaceModel{<:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, <:BGC_WITH_PARTICLES}, Δt) = update_lagrangian_particle_properties!(model, model.biogeochemistry, Δt) -@inline update_lagrangian_particle_properties!(model, bgc::Biogeochemistry{<:Any, <:Any, <:Any, <:BiogeochemicalParticles}, Δt) = +@inline update_lagrangian_particle_properties!(model, bgc::BGC_WITH_PARTICLES, Δt) = update_lagrangian_particle_properties!(bgc.particles, model, bgc, Δt) @inline update_lagrangian_particle_properties!(::BiogeochemicalParticles, model, bgc, Δt) = nothing @@ -51,4 +51,5 @@ function fetch_output(particles::BiogeochemicalParticles, model) end include("tracer_tendencies.jl") + end #module diff --git a/src/Particles/tracer_tendencies.jl b/src/Particles/tracer_tendencies.jl index 8aeaa4553..39632d1f6 100644 --- a/src/Particles/tracer_tendencies.jl +++ b/src/Particles/tracer_tendencies.jl @@ -2,3 +2,5 @@ using Oceananigans.Grids: AbstractGrid, Bounded, Periodic @inline get_node(::Bounded, i, N) = min(max(i, 1), N) @inline get_node(::Periodic, i, N) = ifelse(i < 1, N, ifelse(i > N, 1, i)) + +# TODO: update this with giant kelp stuff \ No newline at end of file diff --git a/src/Utils/Utils.jl b/src/Utils/Utils.jl index 5c857f838..b7ad03178 100644 --- a/src/Utils/Utils.jl +++ b/src/Utils/Utils.jl @@ -1,3 +1,33 @@ +using Oceananigans.Units + include("timestep.jl") include("negative_tracers.jl") -include("sinking_velocity_fields.jl") \ No newline at end of file +include("sinking_velocity_fields.jl") + +""" + (day_length::CBMDayLength)(t, φ) + +Returns the length of day in seconds at the latitude `φ`, `t` seconds after the start of the year. +""" +@kwdef struct CBMDayLength{FT} + day_length_coefficient :: FT = 0.833 +end + +# TODO: add methods for DateTime times etc +@inline function (day_length::CBMDayLength{FT})(t, φ) where FT + # as per Forsythe et al., 1995 (https://doi.org/10.1016/0304-3800(94)00034-F) + p = day_length.day_length_coefficient + + # day of year + J = floor(Int, mod(t, 365days)/day) + + # revolution angle + θ = 0.216310 + 2 * atan(0.9671396 * tan(0.00860 * (J - 186))) + + # solar declination + ϕ = asind(0.39795 * cos(θ)) + + L = max(-one(t), min(one(t), (sind(p) + sind(φ)*sind(ϕ)) / (cosd(φ) * cosd(ϕ)))) + + return convert(FT, (24 - 24 / 180 * acosd(L)) * hour) +end 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..0f849a7bf --- /dev/null +++ b/test/test_PISCES.jl @@ -0,0 +1,181 @@ +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 = 0.5, PChl = 0.02, PFe = 0.005, + D = 0.1, DChl = 0.004, DFe = 0.001, DSi = 0.01, + Z = 0.1, M = 0.7, + DOC = 2.1, + POC = 7.8, SFe = 0.206, + GOC = 38, BFe = 1.1, PSi = 0.1, CaCO₃ = 10^-10, + NO₃ = 2.3, NH₄ = 0.9, PO₄ = 0.6, Fe = 0.13, Si = 8.5, + DIC = 2205, Alk = 2566, O₂ = 317, + 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 + @info "Testing PISCES element conservation (C, Fe, P, Si, N)" + + validation_warning = "This implementation of PISCES is in early development and has not yet been validated against the operational version" + + 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(excess_scavenging_enhancement = 0.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)]) + + # double precision floats are only valid to 17 bits so this tollerance is actually good + @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^-30) + @test isapprox(total_phosphate_tendencies, 0, atol = 10^-22) + @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) + @info "Testing PISCES auxiliary field computation and timestepping" + # 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-2, κ = FunctionField{Center, Center, Center}((z) -> ifelse(z > -25, 2, 1), 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) + @info "Testing PISCES negativity protection" + 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 + test_PISCES_conservation() + #test_PISCES_box_model() #TODO + end + + 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/test/test_light.jl b/test/test_light.jl index 91162e294..abb56045a 100644 --- a/test/test_light.jl +++ b/test/test_light.jl @@ -91,10 +91,10 @@ function test_multi_band(grid, bgc, model_type) expected_PAR1 = on_architecture(CPU(), exp.(znodes(grid, Center()) * (0.01 + 0.1 * 2 ^ 2)) / 2) expected_PAR2 = on_architecture(CPU(), exp.(znodes(grid, Center()) * (0.02 + 0.2 * 2 ^ 1.5)) / 2) - PAR, PAR¹, PAR² = map(v-> on_architecture(CPU(), v), values(biogeochemical_auxiliary_fields(light_attenuation_model))) + PAR, PAR₁, PAR₂ = map(v-> on_architecture(CPU(), v), values(biogeochemical_auxiliary_fields(light_attenuation_model))) - @test all(interior(PAR¹, 1, 1, :) .≈ expected_PAR1) - @test all(interior(PAR², 1, 1, :) .≈ expected_PAR2) + @test all(interior(PAR₁, 1, 1, :) .≈ expected_PAR1) + @test all(interior(PAR₂, 1, 1, :) .≈ expected_PAR2) @test all(PAR[1, 1, 1:grid.Nz] .≈ expected_PAR1 .+ expected_PAR2) # binary operation so we can't `interior` it # check all the models work as expected diff --git a/validation/PISCES/box.jl b/validation/PISCES/box.jl new file mode 100644 index 000000000..209ef6331 --- /dev/null +++ b/validation/PISCES/box.jl @@ -0,0 +1,109 @@ +# # [Box model](@id box_example) +# In this example we setup a [LOBSTER](@ref LOBSTER) biogeochemical model in a single box configuration. +# This example demonstrates: +# - How to setup OceanBioME's biogeochemical models as a stand-alone box model + +# ## Install dependencies +# First we check we have the dependencies installed +# ```julia +# using Pkg +# pkg"add OceanBioME" +# ``` + +# ## Model setup +# Load the packages and setup the initial and forcing conditions +using OceanBioME, Oceananigans, Oceananigans.Units + +using Oceananigans.Fields: FunctionField, ConstantField + +using OceanBioME.Models.PISCESModel: SimpleIron, NitrateAmmonia + +const year = years = 365day + +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) +@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(excess_scavenging_enhancement = 0.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(; grid, biogeochemistry, clock, prescribed_tracers = (; T = temp)) + +set!(model, P = 0.1, PChl = 0.025, PFe = 0.005, + D = 0.01, DChl = 0.003, DFe = 0.0006, DSi = 0.004, + Z = 0.06, M = 0.5, + DOC = 4, + POC = 5.4, SFe = 0.34, + GOC = 8.2, BFe = 0.5, PSi = 0.04, CaCO₃ = 10^-10, + NO₃ = 10, NH₄ = 0.1, PO₄ = 5.0, Fe = 0.6, Si = 8.6, + DIC = 2205, Alk = 2560, O₂ = 317, S = 35) + + +simulation = Simulation(model; Δt = 40minutes, stop_time = 4years) + +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) + + +prog(sim) = @info "$(prettytime(time(sim))) in $(prettytime(simulation.run_wall_time))" + +simulation.callbacks[:progress] = Callback(prog, TimeInterval(182days)) + +@info "Running the model..." +run!(simulation) + +# load and plot results +timeseries = FieldDataset("box.jld2") + +times = timeseries.fields["P"].times + +PAR_timeseries = FieldTimeSeries("box_light.jld2", "PAR") + +using CairoMakie + +fig = Figure(size = (2400, 3600), fontsize = 24) + +axs = [] + +n_start = 1 + +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[n_start:end] / year, timeseries["$name"][n_start:end], linewidth = 3) +end + +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 new file mode 100644 index 000000000..6e0b8b976 --- /dev/null +++ b/validation/PISCES/column.jl @@ -0,0 +1,217 @@ +# # [One-dimensional column example](@id OneD_column) +# In this example we setup a simple 1D column with the [LOBSTER](@ref LOBSTER) biogeochemical model and observe its evolution. The example demonstrates: +# - How to setup OceanBioME's biogeochemical models +# - How to visualise results +# This is forced by idealised mixing layer depth and surface photosynthetically available radiation (PAR) which are setup first. + +# ## Install dependencies +# First we check we have the dependencies installed +# ```julia +# using Pkg +# pkg"add OceanBioME, Oceananigans, CairoMakie" +# ``` + +# ## Model setup +# We load the packages and choose the default LOBSTER parameter set +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) = 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) + +@inline fmld1(t) = H(t, 50days, year) * (1 / (1 + exp(-(t - 100days) / 5days))) * (1 / (1 + exp((t - 330days) / 25days))) + +@inline MLD(t) = - (10 + 340 * (1 - fmld1(year - eps(year)) * exp(-mod(t, year) / 25days) - fmld1(mod(t, year)))) + +@inline κₜ(z, t) = (1e-2 * (1 + tanh((z - MLD(t)) / 10)) / 2 + 1e-4) + +@inline temp(z, t) = 2.4 * (1 + cos(t * 2π / year + 50days)) * ifelse(z > MLD(t), 1, exp((z - MLD(t))/20)) + 8 + +grid = RectilinearGrid(topology = (Flat, Flat, Bounded), size = (100, ), extent = (400, )) + +clock = Clock(; time = 0.0) + +# we can keep this in the column version where we are compleltly divorced from the physics but it be the default to compute it +# JSW will implement somewhere else and it can be pulled in and made the default at some point before merge +zₘₓₗ = FunctionField{Center, Center, Nothing}(MLD, grid; clock) +κ_field = FunctionField{Center, Center, Center}(κₜ, grid; clock) + +# ## Model +# First we define the biogeochemical model including carbonate chemistry (for which we also define temperature (``T``) and salinity (``S``) fields) +# and scaling of negative tracers(see discussion in the [positivity preservation](@ref pos-preservation)) +# and then setup the Oceananigans model with the boundary condition for the DIC based on the air-sea CO₂ flux. + +carbon_chemistry = CarbonChemistry() + +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) + +CO₂_flux = CarbonDioxideGasExchangeBoundaryCondition(; carbon_chemistry) +O₂_flux = OxygenGasExchangeBoundaryCondition() + +@info "Setting up the model..." +model = HydrostaticFreeSurfaceModel(; grid, + velocities = PrescribedVelocityFields(), + tracer_advection = TracerAdvection(nothing, nothing, WENOFifthOrder(grid)), + momentum_advection = nothing, + 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 = 0.1, PChl = 0.025, PFe = 0.005, + D = 0.01, DChl = 0.003, DFe = 0.0006, DSi = 0.004, + Z = 0.06, M = 0.5, + DOC = 4, + POC = 5.4, SFe = 0.34, + GOC = 8.2, BFe = 0.5, PSi = 0.04, CaCO₃ = 10^-10, + NO₃ = 10, NH₄ = 0.1, PO₄ = 5.0, Fe = 0.6, Si = 8.6, + DIC = 2205, Alk = 2560, O₂ = 317, S = 35) + +# maybe get to 1.5hours after initial stuff +simulation = Simulation(model, Δt = 30minutes, stop_time = 5years) + +progress_message(sim) = @printf("Iteration: %04d, time: %s, Δt: %s, wall time: %s\n", + iteration(sim), + prettytime(sim), + prettytime(sim.Δt), + prettytime(sim.run_wall_time)) + +add_callback!(simulation, progress_message, TimeInterval(10day)) + +# prescribe the temperature +using KernelAbstractions: @index, @kernel +using Oceananigans.Architectures: architecture +using Oceananigans.Grids: znode, Center +using Oceananigans.Utils: launch! + +@kernel function fill_T!(T, grid, temp, t) + i, j, k = @index(Global, NTuple) + + z = znode(i, j, k, grid, Center(), Center(), Center()) + + @inbounds T[i, j, k] = temp(z, t) + +end +function update_temperature!(simulation) + t = time(simulation) + + launch!(architecture(grid), grid, :xyz, fill_T!, model.tracers.T, grid, temp, t) + + return nothing +end + +add_callback!(simulation, update_temperature!, IterationInterval(1)) + +filename = "column" +simulation.output_writers[:tracers] = JLD2OutputWriter(model, model.tracers, + filename = "$filename.jld2", + schedule = TimeInterval(3day), + 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, + filename = "$(filename)_internal_fields.jld2", + schedule = TimeInterval(3day), + overwrite_existing = true) +# ## Run! +# We are ready to run the simulation +run!(simulation) + + +# ## Load saved output +# Now we can load the results and do some post processing to diagnose the air-sea CO₂ flux. Hopefully, this looks different to the example without kelp! + +tracers = FieldDataset("$filename.jld2") +internal_fields = FieldDataset("$(filename)_internal_fields.jld2") + +x, y, z = nodes(tracers["P"]) +times = tracers["P"].times + +# We compute the air-sea CO₂ flux at the surface (corresponding to vertical index `k = grid.Nz`) and +# the carbon export by computing how much carbon sinks below some arbirtrary depth; here we use depth +# that corresponds to `k = grid.Nz - 20`. +air_sea_CO₂_flux = zeros(length(times)) +carbon_export = zeros(length(times)) + +S = ConstantField(35) + +using Oceananigans.Biogeochemistry: biogeochemical_drift_velocity +using CUDA + +CUDA.@allowscalar for (n, t) in enumerate(times) + clock.time = t + + 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)) + + 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_export + GOC_export +end + +using CairoMakie + +fig = Figure(size = (4000, 2100), fontsize = 20); + +start_day = 366 +end_day = length(times) + +axis_kwargs = (xlabel = "Time (days)", ylabel = "z (m)", limits = ((times[start_day], times[end_day]) ./ days, (-200, 0))) + +for (n, name) in enumerate(keys(model.tracers)) + 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 = "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)')) +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, 7], xlabel = "Time (days)", ylabel = "Flux (kgCO₂/m²/year)", + title = "Air-sea CO₂ flux and Sinking", limits = ((times[start_day], times[end_day]) ./ days, nothing)) + +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 \ No newline at end of file