From 801244a905b62539fa4cd01051a32c2ca48427b4 Mon Sep 17 00:00:00 2001 From: Daines Date: Thu, 20 Apr 2023 19:15:51 +0100 Subject: [PATCH 1/2] Work-in-progress adding examples --- Project.toml | 7 + docs/src/PALEOocean Reactions.md | 16 + examples/ocean3box/PALEO_examples_oaonly.jl | 70 ++ .../PALEO_examples_oaonly_abiotic.jl | 78 ++ .../ocean3box/PALEO_examples_oaopencarb.jl | 74 ++ .../PALEO_examples_ocean3box_cfg.yaml | 818 ++++++++++++++++ .../PTBClarkson2014/PALEO_examples_PTB3box.jl | 78 ++ .../PALEO_examples_PTB3box_cfg.yaml | 633 ++++++++++++ .../PTBClarkson2014/config_PTB3box_expts.jl | 174 ++++ .../ocean3box/PTBClarkson2014/runtests.jl | 71 ++ examples/ocean3box/README.md | 8 + examples/ocean3box/config_ocean3box_expts.jl | 69 ++ examples/ocean3box/plot_ocean_3box.jl | 138 +++ examples/ocean3box/runtests.jl | 168 ++++ src/ocean/BioProd.jl | 737 ++++++++++++++ src/ocean/Ocean.jl | 11 + src/ocean/OceanTransport3box.jl | 244 +++++ src/ocean/OceanTransport6box.jl | 289 ++++++ src/ocean/OceanTransportTMM.jl | 612 ++++++++++++ src/ocean/VerticalTransport.jl | 908 ++++++++++++++++++ 20 files changed, 5203 insertions(+) create mode 100644 examples/ocean3box/PALEO_examples_oaonly.jl create mode 100644 examples/ocean3box/PALEO_examples_oaonly_abiotic.jl create mode 100644 examples/ocean3box/PALEO_examples_oaopencarb.jl create mode 100644 examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml create mode 100644 examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl create mode 100644 examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml create mode 100644 examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl create mode 100644 examples/ocean3box/PTBClarkson2014/runtests.jl create mode 100644 examples/ocean3box/README.md create mode 100644 examples/ocean3box/config_ocean3box_expts.jl create mode 100644 examples/ocean3box/plot_ocean_3box.jl create mode 100644 examples/ocean3box/runtests.jl create mode 100644 src/ocean/BioProd.jl create mode 100644 src/ocean/OceanTransport3box.jl create mode 100644 src/ocean/OceanTransport6box.jl create mode 100644 src/ocean/OceanTransportTMM.jl create mode 100644 src/ocean/VerticalTransport.jl diff --git a/Project.toml b/Project.toml index b0bdd48..a422f6d 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,11 @@ version = "0.3.0" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +MAT = "23992714-dd62-5051-b70f-ba57cb901cac" +PALEOaqchem = "673cec3b-17d1-411f-9fcd-71c01c593120" PALEOboxes = "804b410e-d900-4b2a-9ecd-f5a06d4c1fd4" +Preferences = "21216c6a-2e73-6563-6e65-726566657250" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SIMD = "fdea26ae-647d-5447-a871-4b548cad5224" SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -16,8 +20,11 @@ TestEnv = "1e6cf692-eddd-4d53-88a5-2d735e33781b" [compat] Documenter = "0.27" Interpolations = "0.13, 0.14" +MAT = "0.10.4" +PALEOaqchem = "0.3.1" PALEOboxes = "0.20.4, 0.21" PALEOmodel = "0.15.8" +Preferences = "1.3" SIMD = "3.4" SnoopPrecompile = "1.0" TestEnv = "1.0" diff --git a/docs/src/PALEOocean Reactions.md b/docs/src/PALEOocean Reactions.md index dd55051..7d69d18 100644 --- a/docs/src/PALEOocean Reactions.md +++ b/docs/src/PALEOocean Reactions.md @@ -7,6 +7,22 @@ CurrentModule = PALEOocean.Ocean ```@docs OceanNoTransport.ReactionOceanNoTransport +OceanTransport3box.ReactionOceanTransport3box +OceanTransport6box.ReactionOceanTransport6box +OceanTransportTMM.ReactionOceanTransportTMM +``` + +### Vertical Transport +```@docs +VerticalTransport.ReactionExportDirect +VerticalTransport.ReactionExportDirectColumn +VerticalTransport.ReactionSinkFloat +``` + +### Production +```@docs +BioProd.ReactionBioProdPrest +BioProd.ReactionBioProdMMPop ``` ## Ocean surface diff --git a/examples/ocean3box/PALEO_examples_oaonly.jl b/examples/ocean3box/PALEO_examples_oaonly.jl new file mode 100644 index 0000000..cbe482e --- /dev/null +++ b/examples/ocean3box/PALEO_examples_oaonly.jl @@ -0,0 +1,70 @@ + +using Logging +using DiffEqBase +using Sundials + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + + +global_logger(ConsoleLogger(stderr, Logging.Info)) + +include("config_ocean3box_expts.jl") +include("plot_ocean_3box.jl") + +# model = config_ocean3box_expts("oaonly", ["baseline"]); tspan=(-1e4,1e4) +model = config_ocean3box_expts("oaonly", ["killbio", "lowO2", "lowSO4"]); tspan=(-1e4,1e4) + +initial_state, modeldata = PALEOmodel.initialize!(model) +statevar_norm = PALEOmodel.get_statevar_norm(modeldata.solver_view_all) + +# call ODE function to check derivative +println("initial_state", initial_state) +println("statevar_norm", statevar_norm) +initial_deriv = similar(initial_state) +PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) +println("initial_deriv", initial_deriv) + +run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +# With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling +# (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) + +# Solve as DAE with sparse Jacobian +PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) +) + +# Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) +# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) + + +######################################## +# Plot output +######################################## + +# individual plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# assemble plots onto screens with 6 subplots +gr(size=(1200, 900)) +pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) + +plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) +plot_ocean_tracers( + run.output; + tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "CH4_conc", "P_conc", "SO4_delta", "H2S_delta", "CH4_delta"], + pager=pager +) +plot_oaonly_abiotic(run.output; pager=pager) +pager(:newpage) # flush output diff --git a/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl b/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl new file mode 100644 index 0000000..c35191d --- /dev/null +++ b/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl @@ -0,0 +1,78 @@ + +using Logging +using DiffEqBase +using Sundials + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + + +global_logger(ConsoleLogger(stderr, Logging.Info)) + + + +include("config_ocean3box_expts.jl") +include("plot_ocean_3box.jl") + +model = config_ocean3box_expts("oaonly_abiotic", ["baseline"]); tspan=(0,1e4) +# model = config_ocean3box_expts("oaonly_abiotic", ["fastexchange"]); tspan=(0,1e4) +# model = config_ocean3box_expts("oaonly_abiotic", ["slowexchange"]); tspan=(0,1e4) + + +initial_state, modeldata = PALEOmodel.initialize!(model) +statevar_norm = PALEOmodel.get_statevar_norm(modeldata.solver_view_all) + +# call ODE function to check derivative +println("initial_state", initial_state) +println("statevar_norm", statevar_norm) +initial_deriv = similar(initial_state) +PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) +println("initial_deriv", initial_deriv) + +run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +# With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling +# (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) + +# Solve as DAE with sparse Jacobian +PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) +) + +# Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) +# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) + + +######################################## +# Plot output +######################################## + +# individual plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# assemble plots onto screens with 6 subplots +gr(size=(1200, 900)) +pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) + +plot_totals(run.output; species=["C", "TAlk", "TAlkerror"], pager=pager) +plot_ocean_tracers(run.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot"], pager=pager) +plot_oaonly_abiotic(run.output; pager=pager) +pager(:newpage) # flush output + +##################################### +# Additional tests for solvers +######################################## + +# Solve as DAE without Jacobian +# PALEOmodel.ODE.integrateDAE(run, initial_state, modeldata, tspan, alg=IDA(), +# solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), save_start=false)) diff --git a/examples/ocean3box/PALEO_examples_oaopencarb.jl b/examples/ocean3box/PALEO_examples_oaopencarb.jl new file mode 100644 index 0000000..05ac61d --- /dev/null +++ b/examples/ocean3box/PALEO_examples_oaopencarb.jl @@ -0,0 +1,74 @@ + +using Logging +using DiffEqBase +using Sundials + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean +import PALEOcopse + + +global_logger(ConsoleLogger(stderr, Logging.Info)) + +include("config_ocean3box_expts.jl") +include("plot_ocean_3box.jl") + + +model = config_ocean3box_expts("oaopencarb", ["killbio", "lowO2"]); tspan=(-10e6, 10e6) # tspan=(-10e6,1000.0) # + +initial_state, modeldata = PALEOmodel.initialize!(model) +statevar_norm = PALEOmodel.get_statevar_norm(modeldata.solver_view_all) + +# call ODE function to check derivative +println("initial_state", initial_state) +println("statevar_norm", statevar_norm) +initial_deriv = similar(initial_state) +PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) +println("initial_deriv", initial_deriv) + +run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +# With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling +# (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) + +# Solve as DAE with sparse Jacobian +PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) +) + +# Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) +# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) + + +######################################## +# Plot output +######################################## + +# individual plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# assemble plots onto screens with 6 subplots +gr(size=(1200, 900)) + +pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) + +plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) +plot_ocean_tracers( + run.output; + tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "P_conc", + "SO4_delta", "H2S_delta", "pHtot", "OmegaAR"], + pager=pager +) +plot_oaonly_abiotic(run.output; pager=pager) +plot_carb_open(run.output; pager=pager) +pager(:newpage) # flush output diff --git a/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml b/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml new file mode 100644 index 0000000..9444494 --- /dev/null +++ b/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml @@ -0,0 +1,818 @@ + + + +ocean3box_oaonly_abiotic_base: + parameters: + CIsotope: IsotopeLinear + + domains: + global: + # scalar domain + + reactions: + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC] + variable_links: + sum: total_C + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["CO2::CIsotope"] + + atm: + + + reactions: + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + parameters: + temp: [21.5, 2.0, 2.0] + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2208.1e-3 # 2150e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2403.2e-3 # 2340e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega"] + defaultconcs: ["TS", "TF", "TB", "Ca"] + solve_pH: constraint # Hfree as DAE variable + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + variable_links: + TCi_conc: DIC_conc + CO2: CO2_conc + pCO2: pCO2 + + + oceansurface: + reactions: + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston: 3.12 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: + + +ocean3box_oaonly_base: + parameters: + CIsotope: IsotopeLinear + SIsotope: IsotopeLinear + + domains: + global: + # scalar domain + + reactions: + force_enable_bioprod: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: enable_bioprod + + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC, ocean.CH4] + variable_links: + sum: total_C + + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2] + variable_links: + sum: total_O2 + + total_S: + class: ReactionSum + parameters: + vars_to_add: [ocean.SO4, ocean.H2S] + variable_links: + sum: total_S + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["CO2::CIsotope", "O2"] + + atm: + + + reactions: + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.71e19 # 0.21 + R:initial_value: 3.71e19 # PAL + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + parameters: + temp: [21.5, 2.0, 2.0] + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 300.0e-3 # concentration mol m-3 ~ 300e-6 mol/kg * 1027 kg m-3 + R:norm_value: 100.0e-3 # for scaling only + + reservoir_SO4: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: SO4* + variable_attributes: + R:initial_value: 28756.0e-3 # concentration mol m-3 ~ 28e-3 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_H2S: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: H2S* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_CH4: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: CH4* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2208.1e-3 # 2150e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2403.2e-3 # 2340e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega", "H2S"] + defaultconcs: ["TF", "TB", "Ca"] + solve_pH: constraint # Hfree as DAE variable + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + variable_links: + TCi_conc: DIC_conc + TS_conc: SO4_conc + CO2: CO2_conc + TH2S_conc: H2S_conc + pCO2: pCO2 + + const_cisotopes: + class: ReactionScalarConst + parameters: + constnames: ["D_mccb_DIC", "D_B_mccb_mocb"] + variable_attributes: + D_mccb_DIC:initial_value: 0.0 + D_B_mccb_mocb:initial_value: 25.0 + + bioprod: + class: ReactionBioProdPrest + + parameters: + rCcarbCorg: 0.2 + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + export: + class: ReactionExportDirect + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything into bottom cell + + variable_links: + # remin_P: # default is OK + + remin: + class: ReactionReminO2_SO4_CH4 + + parameters: + SO4reminlimit: 1000.0e-3 + + variable_links: + soluteflux_*: "*_sms" + + redox_H2S_O2: + class: ReactionRedoxH2S_O2 + + parameters: + R_H2S_O2: 3.65e3 # (mol m-3) yr-1 + + redox_CH4_O2: + class: ReactionRedoxCH4_O2 + + parameters: + R_CH4_O2: 1.0e3 # (mol m-3) yr-1 + + oceansurface: + reactions: + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston: 3.12 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston: 3.12 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + + oceanfloor: + reactions: + +# Open atm-ocean carbonate system, with weathering input and burial output +# Closed organic carbon, ocean sulphur systems (no burial) +ocean3box_oaopencarb_base: + parameters: + CIsotope: IsotopeLinear + SIsotope: IsotopeLinear + Senabled: false + domains: + fluxRtoOcean: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["DIC::CIsotope", "TAlk", "Ca", "P", "SO4::SIsotope"] + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["CO2::CIsotope", "O2"] + + fluxOceanfloor: + reactions: + particulatefluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] # fluxlist_BioParticulate + + solutefluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["DIC::CIsotope", "TAlk", "Ca"] # , "P", "O2", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] # fluxlist_Solute + + + fluxOceanBurial: + reactions: + target: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["Ccarb::CIsotope"] + + fluxLandtoSedCrust: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["Ccarb::CIsotope", "Corg::CIsotope", "PYR::SIsotope", "GYP::SIsotope"] + + fluxAtoLand: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["O2", "CO2::CIsotope"] + + global: + # scalar domain + + reactions: + force_enable_bioprod: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: enable_bioprod + + force_solar: + class: ReactionForce_CK_Solar + + force_UDWE: + class: ReactionForce_UDWEbergman2004 + + force_CPlandrel: + class: ReactionForce_CPlandrelbergman2004 + + temp_CK_1992: + class: ReactionGlobalTemperatureCK1992 + + parameters: + temp_DAE: true + variable_links: + pCO2atm: atm.pCO2atm + + CIsotopes: + class: ReactionCIsotopes + parameters: + f_cisotopefrac: fixed + + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC] + variable_links: + sum: total_C + + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2] + variable_links: + sum: total_O2 + + total_S: + class: ReactionSum + parameters: + vars_to_add: [ocean.SO4, ocean.H2S] + variable_links: + sum: total_S + + + + atm: + + + reactions: + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.71e19 # 0.21 + R:initial_value: 3.71e19 # PAL + + constant_degassing: + class: ReactionFluxPerturb + parameters: + field_data: external%CIsotope + perturb_times: [-1e30, 1e30] + perturb_totals: [6.7e12, 6.7e12] + perturb_deltas: [0.0, 0.0] # 0 per mil as we have no Corg burial + variable_links: + F*: CO2_sms + tforce: global.tforce + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + transfer_AtoLand: + class: ReactionFluxTransfer + parameters: + input_fluxes: fluxAtoLand.flux_$fluxname$ + output_fluxes: $fluxname$_sms + transfer_multiplier: -1.0 + + land: + + + reactions: + land_Bergman2004: + class: ReactionLandBergman2004 + + parameters: # disable everything except carbonate and silicate weathering + k5_locb: 0.0 + k_silw: 6.7e12 # balance constant degassing + k14_carbw: 13.35e12 + k10_phosw: 0.0 + k17_oxidw: 0.0 + + enableS: external%Senabled # no S weathering + + sedcrust: + + + + reactions: + + reservoir_C: + class: ReactionReservoirScalar + parameters: + field_data: external%CIsotope + variable_links: + R*: C* + variable_attributes: + R:norm_value: 5e21 + R:initial_value: 5e21 + R:initial_delta: 1.0 # per mil + + reservoir_G: + class: ReactionReservoirScalar + parameters: + field_data: external%CIsotope + variable_links: + R*: G* + variable_attributes: + R:norm_value: 1.25e21 + R:initial_value: 1.25e21 + R:initial_delta: -26.0 # per mil + + # transfer_OceanBurial: + # class: ReactionFluxTransfer + # parameters: + # transfer_matrix: Distribute + # input_fluxes: fluxOceanBurial.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # variable_links: + # variable_links: + # output_Corg: G_sms # fix naming Corg = G + # output_Ccarb: C_sms # fix naming Ccarb = C + + # transfer_LandtoSedCrust: + # class: ReactionFluxTransfer + # parameters: + # input_fluxes: fluxLandtoSedCrust.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # variable_links: + # output_Corg: G_sms # fix naming Corg = G + # output_Ccarb: C_sms # fix naming Ccarb = C + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + parameters: + temp: [21.5, 2.0, 2.0] + temp_trackglobal: true + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 300.0e-3 # concentration mol m-3 ~ 300e-6 mol/kg * 1027 kg m-3 + R:norm_value: 100.0e-3 # for scaling only + + reservoir_SO4: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: SO4* + variable_attributes: + R:initial_value: 28756.0e-3 # concentration mol m-3 ~ 28e-3 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_H2S: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: H2S* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2208.1e-3 # 2150e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2403.2e-3 # 2340e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega", "H2S"] + defaultconcs: ["TF", "TB", "Ca"] + solve_pH: constraint # Hfree as DAE variable + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + variable_links: + TCi_conc: DIC_conc + TS_conc: SO4_conc + CO2: CO2_conc + CO3: CO3_conc + TH2S_conc: H2S_conc + pCO2: pCO2 + OmegaAR: OmegaAR + + bioprod: + class: ReactionBioProdPrest + + parameters: + rCcarbCorg: 0.2 + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + exportOrg: + class: ReactionExportDirect + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything into deep ocean cell + + variable_links: + # remin_P: # default is OK + + exportCarb: + class: ReactionExportDirect + + parameters: + fluxlist: ["Ccarb::CIsotope"] + transportfloor: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything onto deep ocean floor + + variable_links: + # remin_Ccarb: fluxOceanfloor.particulateflux_Ccarb # default should be OK + + remin: + class: ReactionReminO2_SO4 + + parameters: + + + variable_links: + soluteflux_*: "*_sms" + + redox_H2S_O2: + class: ReactionRedoxH2S_O2 + + parameters: + + R_H2S_O2: 3.65e3 # (mol m-3) yr-1 + + + oceansurface: + reactions: + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston: 3.12 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston: 3.12 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + + transfer_RtoOcean: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + input_fluxes: fluxRtoOcean.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + + oceanfloor: + reactions: + shelfcarb: + class: ReactionShelfCarb + parameters: + shelfareanorm: [1.0, 0.0, 0.0] # only low lat surface box + carbsedshallow: 1.4355e12 + deepcarb: + class: ReactionBurialEffCarb + parameters: + hascarbseddeep: [false, false, true] # only deep box + variable_links: + particulateflux_Ccarb: particulateflux_Ccarb + + transfer_particulatefluxOceanfloor: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + variable_links: + + transfer_solutefluxOceanfloor: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + + diff --git a/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl b/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl new file mode 100644 index 0000000..e13f7f4 --- /dev/null +++ b/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl @@ -0,0 +1,78 @@ + +using Logging +using DiffEqBase +using Sundials + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOreactions +import PALEOcopse + + +global_logger(ConsoleLogger(stderr,Logging.Info)) + +include("config_PTB3box_expts.jl") +include("../plot_ocean_3box.jl") + +# model = config_PTB3box_expts("Co2HOmLWCpp", ["baseline"]); tspan=(-260e6,-240e6) # tspan=(-10e6,10e6) +# model = config_PTB3box_expts("Co2LOmHWC4pp", ["baseline"]); tspan=(-260e6,-240e6) # tspan=(-10e6,10e6) + +# Clarkson etal (2015) CO2LO scenario +model = config_PTB3box_expts("Co2LOmHWC4pp", ["Sw_2Ts", "Pp_PEes", "Lk_2", "Cia_s2", "Cib_1"]); tspan=(-260e6,-240e6) # tspan=(-10e6,10e6) + +# Clarkson etal (2015) CO2LO + kill marine biota at EP2 +# model = config_PTB3box_expts("Co2LOmHWC4pp", ["Sw_2Ts", "Pp_PEes", "Lk_2", "Cia_s2", "Cib_1", "killbioEP2"]); tspan=(-260e6,-240e6) + + +initial_state, modeldata = PALEOmodel.initialize!(model) + +# call ODE function to check derivative +initial_deriv = similar(initial_state) +PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) +println("initial_state", initial_state) +println("initial_deriv", initial_deriv) + +run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +# With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling +# (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) + +# Solve as DAE with sparse Jacobian +PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + reltol=1e-5, + save_start=false, + dtmax=0.5e5, # , tstops=[-251.95e6])) + ) +) + + +######################################## +# Plot output +######################################## + +# individual plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# assemble plots onto screens with 6 subplots +gr(size=(1200, 900)) + +pager=PALEOmodel.PlotPager((2, 3), (xlim=(-252.15e6, -251.80e6), xflip=true, legend_background_color=nothing, )) + +plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "S", "P"], pager=pager) +plot_ocean_tracers( + run.output; + tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "P_conc", + "SO4_delta", "H2S_delta", "OmegaAR", "DIC_delta", "pHtot", "BOH4_delta"], + pager=pager +) +plot_oaonly_abiotic(run.output; pager=pager) +plot_PTB3box(run.output; pager=pager) +plot_carb_open(run.output; pager=pager) +pager(:newpage) # flush output diff --git a/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml b/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml new file mode 100644 index 0000000..c6fd98f --- /dev/null +++ b/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml @@ -0,0 +1,633 @@ + + + + + + + +# Open atm-ocean carbonate system, with weathering input and burial output +# Closed organic carbon, ocean sulphur systems (no burial) +ocean3box_oaopencarb_base: + parameters: + CIsotope: IsotopeLinear + SIsotope: IsotopeLinear + BIsotope: IsotopeLinear + Senabled: false + domains: + fluxAtoLand: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["O2", "CO2::CIsotope"] + + + fluxRtoOcean: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["DIC::CIsotope", "TAlk", "Ca", "P", "SO4::SIsotope"] #, "U::UIsotope"] + + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["O2", "CO2::CIsotope"] + + fluxOceanfloor: + reactions: + particulatetarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["Corg::CIsotope", "N", "P", "Ccarb::CIsotope"] + + solutetarget: + class: ReactionFluxTarget + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["DIC::CIsotope", "TAlk", "O2", "P", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] #, "U::UIsotope"] + + + fluxOceanBurial: + reactions: + transfer: + class: ReactionFluxTarget + parameters: + flux_totals: true + fluxlist: ["Corg::CIsotope", "Ccarb::CIsotope", "GYP::SIsotope", "PYR::SIsotope", "P", "Pauth", "PFe", "Porg"] + + # fluxSedCrusttoAOcean: + # + # reactions: + # target: + # class: ReactionFluxTarget + + # parameters: + # fluxlist: ["C::CIsotope", "S::SIsotope", "Redox"] + + + fluxLandtoSedCrust: + + reactions: + target: + class: ReactionFluxTarget + parameters: + fluxlist: ["Ccarb::CIsotope", "Corg::CIsotope", "PYR::SIsotope", "GYP::SIsotope"] + + global: + # scalar domain + + reactions: + constant_tforce: + class: ReactionScalarConst + variable_links: + constvar: constant_tforce + variable_attributes: + constvar:initial_value: -250e6 # end Permian + + force_enable_bioprod: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: enable_bioprod + + shelfarea_force: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: shelfarea_force + + force_solar: + class: ReactionForce_CK_Solar + variable_links: + tforce: constant_tforce + + # force_UDWE: + # class: ReactionForce_UDWEbergman2004 + + force_UPLIFT: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: UPLIFT + + force_W: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: W + + force_VEG: + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: VEG + + force_locbpert: # land organic carbon burial forcing + class: ReactionForceInterp + parameters: + force_times: [-1e30, 1e30] + force_values: [1.0, 1.0] + variable_links: + F: locbpert + + Ppulse: + class: ReactionFluxPerturb + parameters: + field_data: ScalarData + perturb_times: [-1e30, 1e30] + perturb_totals: [0.0, 0.0] + perturb_deltas: [0.0, 0.0] + variable_links: + F: fluxRtoOcean.flux_P + FApplied: Ppulse + tforce: global.tforce + + temp_CK_1992: + class: ReactionGlobalTemperatureCK1992 + + parameters: + temp_DAE: true + variable_links: + pCO2atm: atm.pCO2atm + + # CIsotopes: + # class: ReactionCIsotopes + # parameters: + # do_D_mccb_DIC: false + # do_D_B_mccb_mocb: false + # do_D_P_CO2_locb: false + # f_cisotopefrac: copse_base + + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC] + variable_links: + sum: total_C + + + total_S: + class: ReactionSum + parameters: + vars_to_add: [ocean.SO4, ocean.H2S] + variable_links: + sum: total_S + + atm: + + + reactions: + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + constant_pO2: + class: ReactionScalarConst + parameters: + constnames: ["pO2PAL", "pO2atm"] + variable_attributes: + pO2PAL:initial_value: 1.0 + pO2atm:initial_value: 0.21 + + constant_degassing: + class: ReactionFluxPerturb + parameters: + field_data: external%CIsotope + perturb_times: [-1e30, 1e30] + perturb_totals: [11.8e12, 11.8e12] + perturb_deltas: [-4.9, -4.9] + variable_links: + F: CO2_sms + FApplied: ccdeg + tforce: global.tforce + + CO2pulse_Cia: + class: ReactionFluxPerturb + parameters: + field_data: external%CIsotope + perturb_times: [-1e30, 1e30] + perturb_totals: [0.0, 0.0] + perturb_deltas: [0.0, 0.0] + variable_links: + F: CO2_sms + FApplied: Cia + tforce: global.tforce + + CO2pulse_Cib: + class: ReactionFluxPerturb + parameters: + field_data: external%CIsotope + perturb_times: [-1e30, 1e30] + perturb_totals: [0.0, 0.0] + perturb_deltas: [0.0, 0.0] + variable_links: + F: CO2_sms + FApplied: Cib + tforce: global.tforce + + transfer_AtoLand: + class: ReactionFluxTransfer + parameters: + input_fluxes: fluxAtoLand.flux_$fluxname$ + output_fluxes: $fluxname$_sms + transfer_multiplier: -1.0 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + # transfer_SedCrusttoAOcean: + # class: ReactionFluxTransfer + # parameters: + # transfer_multiplier: 1.0 + # input_fluxes: fluxSedCrusttoAOcean.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # variable_links: + # # S to oceansurface + # output_C: CO2_sms + # output_Redox: O2_sms + + + land: + + + reactions: + constant_D_P_CO2_locb: + class: ReactionScalarConst + parameters: + constnames: ["D_P_CO2_locb"] + variable_attributes: + D_P_CO2_locb:initial_value: 19.0 + + land_Bergman2004: + class: ReactionLandBergman2004 + + parameters: # disable everything except carbonate and silicate weathering + f_landbiota: Prescribed + f_locb: Prescribed + + k5_locb: 5.0e12 + k_silw: 6.6e12 # balance constant degassing + k14_carbw: 13.3e12 + k10_phosw: 0.0 + k17_oxidw: 0.0 + + enableS: external%Senabled # no S weathering + + sedcrust: + + + + reactions: + + reservoir_C: + class: ReactionReservoirScalar + parameters: + field_data: external%CIsotope + const: true + variable_links: + R*: C* + variable_attributes: + R:norm_value: 5e21 + R:initial_value: 5e21 + R:initial_delta: 2.65 # per mil + + reservoir_G: + class: ReactionReservoirScalar + parameters: + field_data: external%CIsotope + const: true + variable_links: + R*: G* + variable_attributes: + R:norm_value: 1.25e21 + R:initial_value: 1.25e21 + R:initial_delta: -25.0 # per mil + + # transfer_OceanBurial: + # class: ReactionFluxTransfer + # parameters: + # input_fluxes: fluxOceanBurial.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # transfer_matrix: Distribute + # variable_links: + # output_Corg: G_sms # fix naming Corg = G + # output_Ccarb: C_sms # fix naming Ccarb = C + # # output_Sr: Sr_sed_sms # fix naming + + # transfer_LandtoSedCrust: + # class: ReactionFluxTransfer + # parameters: + # input_fluxes: fluxLandtoSedCrust.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # variable_links: + # output_Corg: G_sms # fix naming Corg = G + # output_Ccarb: C_sms # fix naming Ccarb = C + # # output_Sr: Sr_sed_sms # fix naming + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + parameters: + temp: [21.5, 2.0, 2.0] + temp_trackglobal: true + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 300.0e-3 # concentration mol m-3 ~ 300e-6 mol/kg * 1027 kg m-3 + R:norm_value: 100.0e-3 # for scaling only + + reservoir_SO4: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: SO4* + variable_attributes: + R:initial_value: 28756.0e-3 # concentration mol m-3 ~ 28e-3 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_H2S: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: H2S* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2208.1e-3 # 2150e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2403.2e-3 # 2340e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_B: + class: ReactionReservoirConst + parameters: + field_data: external%BIsotope + variable_links: + R*: B* + variable_attributes: + R_conc:initial_value: 0.4269239 # contemporary value + R_conc:initial_delta: 34.0 + # R_conc:norm_value: 0.4269239 # for scaling only + + B_isotope: + class: ReactionBoronIsotope + variable_links: + # defaults OK for B_conc, BOH4_conc, B_delta, BOH4_delta + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega", "H2S"] + defaultconcs: ["TF", "Ca"] + solve_pH: constraint # Hfree as DAE variable + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR", "BAlk", "TB"] + variable_links: + TCi_conc: DIC_conc + TS_conc: SO4_conc + TB_conc: B_conc + # TB: TB_out # should be identical to TB_conc + BAlk: BOH4_conc + CO2: CO2_conc + CO3: CO3_conc + TH2S_conc: H2S_conc + pCO2: pCO2 + OmegaAR: OmegaAR + OmegaCA: OmegaCA + + const_cisotopes: + class: ReactionScalarConst + parameters: + constnames: ["D_mccb_DIC", "D_B_mccb_mocb"] + variable_attributes: + D_mccb_DIC:initial_value: 0.0 + D_B_mccb_mocb:initial_value: 25.0 + + bioprod: + class: ReactionBioProdPrest + + parameters: + rCcarbCorg: 0.0 + rCorgPO4: 161.0 + rNPO4: 16.0 + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + exportOrg: + class: ReactionExportDirect + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [0.0, 1.0, 1.0]] #dump everything from 2 into 3 (deep box) + transportfloor: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 0.0, 0.0]] #dump everything from 1 onto 3 (deep ocean floor) + + variable_links: + # remin_P: # default is OK + + exportCarb: + class: ReactionExportDirect + + parameters: + fluxlist: ["Ccarb::CIsotope"] + transportfloor: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything onto ocean floor + + variable_links: + # remin_Ccarb: fluxOceanfloor.particulateflux_Ccarb # default should be OK + + remin: + class: ReactionReminO2_SO4 + + parameters: + + + variable_links: + soluteflux_*: "*_sms" + + redox_H2S_O2: + class: ReactionRedoxH2S_O2 + + parameters: + + R_H2S_O2: 3.65e3 # (mol m-3) yr-1 + + + oceansurface: + reactions: + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston: 3.0 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston: 3.0 # m d-1 + moistair: false # no sat H2O correction + + variable_links: + + transfer_RtoOcean: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + input_fluxes: fluxRtoOcean.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + # output_U: ocean.oceansurface.U_vec_sms + + # transfer_SedCrusttoAOcean: + # class: ReactionFluxTransfer + # parameters: + # transfer_matrix: Distribute + # transfer_multiplier: 1.0 + # input_fluxes: fluxSedCrusttoAOcean.flux_$fluxname$ + # output_fluxes: $fluxname$_sms + # variable_links: + # # C, Redox to atm + # output_S: ocean.oceansurface.SO4_sms + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + + + oceanfloor: + reactions: + shelfcarb: + class: ReactionShelfCarb + parameters: + shelfareanorm: [1.0, 0.0, 0.0] # only low lat surface box + carbsedshallow: 1.4355e12 + deepcarb: + class: ReactionBurialEffCarb + parameters: + hascarbseddeep: [false, false, true] # only deep box + variable_links: + particulateflux_Ccarb: particulateflux_Ccarb + + sedBEcorgP: + class: ReactionBurialEffCorgP + parameters: + burial_eff_function: ConstantBurialRate + + BECorgNorm: 5e12 # mol C yr-1 + + BECorg: [0.0, 0.0, 1.0] # all Corg burial from box 3 + + BPorgCorg: [0.0] # prescribed P:Corg, COPSE Bergman (2004) + BPFeCorg: [0.0] # prescribed P:Corg, COPSE Bergman (2004) + BPauthCorg: [0.0] # prescribed P:Corg, COPSE Bergman (2004) + variable_links: + reminflux_Corg: remin_Corg + reminflux_N: remin_N + reminflux_P: remin_P + + remin: + class: ReactionReminO2_SO4 + + parameters: + + variable_links: + O2_conc: ocean.oceanfloor.O2_conc + SO4_delta: ocean.oceanfloor.SO4_delta + soluteflux_*: fluxOceanfloor.soluteflux_* + + transferparticulate_fluxOceanfloor: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + + transfersolute_fluxOceanfloor: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + # output_U: ocean.oceanfloor.U_vec_sms \ No newline at end of file diff --git a/examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl b/examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl new file mode 100644 index 0000000..94643f6 --- /dev/null +++ b/examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl @@ -0,0 +1,174 @@ + +"test cases and examples for 3 box ocean" +function config_PTB3box_expts(baseconfig, expts) + + if baseconfig=="oaopencarb" + # Open atmosphere-ocean with silicate carbonate weathering input and carbonate burial + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base" + ) + + elseif baseconfig=="Co2HOmLWCpp" + # Clarkson (2014) CO2Hi + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_PTB3box_cfg.yaml"), "ocean3box_oaopencarb_base" + ) + + # constant_degassing = PB.get_reaction(model, "atm", "constant_degassing") + # PB.setvalue!(constant_degassing.pars.perturb_totals, 11.8e12.*[1.0, 1.0]) # TODO d13C + + land_Bergman2004 = PB.get_reaction(model, "land", "land_Bergman2004") + PB.setvalue!(land_Bergman2004.pars.k_silw, 2.40e12) + PB.setvalue!(land_Bergman2004.pars.k17_oxidw, 5.00e12) + + B.set_variable_attribute!(model, "ocean", "B_conc", :initial_delta, 36.8) + + shelfcarb = PB.get_reaction(model, "oceanfloor", "shelfcarb") + PB.setvalue!(shelfcarb.pars.carbsedshallow, 18.43e12) + + elseif baseconfig=="Co2LOmHWC4pp" + # Clarkson (2014) CO2Lo + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_PTB3box_cfg.yaml"), "ocean3box_oaopencarb_base") + + + # constant_degassing = PB.get_reaction(model, "atm", "constant_degassing") + # PB.setvalue!(constant_degassing.pars.perturb_totals, 11.8e12.*[1.0, 1.0]) # TODO d13C + + land_Bergman2004 = PB.get_reaction(model, "land", "land_Bergman2004") + PB.setvalue!(land_Bergman2004.pars.k_silw, 6.60e12) + PB.setvalue!(land_Bergman2004.pars.k17_oxidw, 5.92e12) + + PB.set_variable_attribute!(model, "ocean", "B_conc", :initial_delta, 34.0) + + shelfcarb = PB.get_reaction(model, "oceanfloor", "shelfcarb") + PB.setvalue!(shelfcarb.pars.carbsedshallow, 1.44e12) + + else + error("unrecognized baseconfig='$(baseconfig)'") + end + + ########################### + # configure expt + ############################ + + for expt in expts + println("Add expt: ", expt) + if expt == "baseline" + # defaults + elseif expt == "fastexchange" + react_airseaCO2 = PB.get_reaction(model, "oceansurface", "airsea_CO2") + PB.setvalue!(PB.get_parameter(react_airseaCO2, "piston"), 3.1e3) # m/day 'fast exchange' + elseif expt == "slowexchange" + react_airseaCO2 = PB.get_reaction(model, "oceansurface", "airsea_CO2") + PB.setvalue!(PB.get_parameter(react_airseaCO2, "piston"), 3.1e-3) # m/day 'slow exchange' + elseif expt == "killbioEP2" + # disable ocean biology at t=-251.88e6 + tkill = -251.88e6 + react_enable_bioprod = PB.get_reaction(model, "global", "force_enable_bioprod") + PB.setvalue!(PB.get_parameter(react_enable_bioprod, "force_times"), [-1e30, tkill, tkill+1.0, 1e30]) + PB.setvalue!(PB.get_parameter(react_enable_bioprod, "force_values"), [1.0, 1.0, 0.0, 0.0]) + elseif expt == "lowO2" + # start with low oxygen to test marine sulphur system + PB.set_variable_attribute!(model, "atm", "O2", :initial_value, 0.1*3.71e19) + elseif expt == "lowSO4" + # start with low SO4 to test methane cycling + PB.set_variable_attribute!!(model, "ocean", "SO4", :initial_value, 100e-3) # ~100 uM + + elseif expt in ("Sw_T", "Sw_TL", "Sw_2T", "Sw_2Tsx2", "Sw_2Ts") + # shelf area pH rise mechanism + shelfarea_force = PB.get_reaction(model, "global", "shelfarea_force") + reduce_facs = Dict( + "Sw_T" => 0.078, # Take high shelf area case 'Co2HOmL' to low area of 'CoHLOmH' + "Sw_TL" => 0.5*0.078, # Take high shelf area case 'Co2HOmL' to lower than area of 'CoHLOmH' + "Sw_2T" => 8.22e10/1.44e12, # Take high shelf area case 'Co2LOmHW' to low area of 'CoLOmHHW' + "Sw_2Ts" => 3*8.22e10/1.44e12, # Take high shelf area case 'Co2LOmHW' to 3*low area of 'CoLOmHHW' + "Sw_2Tsx2"=>1.5*8.22e10/1.44e12, # Double / half 2Ts + ) + reduce_fac = reduce_facs[expt] + PB.setvalue!(PB.get_parameter(shelfarea_force, "force_times"), [-1e30, -252.05e6, -252.05e6+1.0, 1e30]) + PB.setvalue!(PB.get_parameter(shelfarea_force, "force_values"), [1.0, 1.0, reduce_fac, reduce_fac]) + + elseif expt in ("Pp_PE", "Pp_PEe", "Pp_PEes") + # increase marine prod -> anoxia + # mol P dur yr start yr + Pperts = Dict( + "Pp_PE" => (1.3*3e15, 1e5, -251.95e6 - 2e5), + "Pp_PEe" => (1.3*3e15, 2e5, -251.95e6 -3e5), + "Pp_PEes" => (1.3*3e15, 2e5, -251.95e6 - 3e5) + ) + + (Ptot, duration, tstart) = Pperts[expt] + tend = tstart + duration + Ppulse = PB.get_reaction(model, "global", "Ppulse") + PB.setvalue!( + PB.get_parameter(Ppulse, "perturb_times"), + [-1e30, tstart, tstart+1.0, tend, tend+1.0, 1e30] + ) + PB.setvalue!(PB.get_parameter( + Ppulse, "perturb_totals"), + [0.0, 0.0, Ptot/duration, Ptot/duration, 0.0, 0.0] + ) + PB.setvalue!(PB.get_parameter( + Ppulse, "perturb_deltas"), + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ) + + elseif expt == "Lk_2" + # stop land organic carbon burial at 252.0Ma + force_locbpert = PB.get_reaction(model, "global", "force_locbpert") + PB.setvalue!(PB.get_parameter(force_locbpert, "force_times"), [-1e30, -252.0e6, -252.0e6+1.0, 1e30]) + PB.setvalue!(PB.get_parameter(force_locbpert, "force_values"), [1.0, 1.0, 0.0, 0.0]) + + elseif expt in ("Cia_1", "Cia_s2") + # isotopically light carbon injection at 251.95Ma + CO2pulse_Cia = PB.get_reaction(model, "atm", "CO2pulse_Cia") + CO2sizes_deltas = Dict( + "Cia_1"=>(4.86e17, -50.0), + "Cia_s2"=>(2/3*2*4.86e17, -25.0) + ) + (size, delta) = CO2sizes_deltas[expt] # mol C + duration = 0.5e5 # yr + PB.setvalue!(PB.get_parameter(CO2pulse_Cia, "perturb_times"), + [-1e30, -251.95e6, -251.95e6+1.0, -251.95e6+duration, -251.95e6+duration+1.0, 1e30]) + PB.setvalue!(PB.get_parameter(CO2pulse_Cia, "perturb_totals"), + [0.0, 0.0, size/duration, size/duration, 0.0, 0.0]) + PB.setvalue!(PB.get_parameter(CO2pulse_Cia, "perturb_deltas"), + delta.*[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) + + elseif expt in ("Cib_1", "Cib_2") + # isotopically neutral carbon injection at 251.89Ma + CO2pulse_Cib = PB.get_reaction(model, "atm", "CO2pulse_Cib") + CO2sizes=Dict("Cib_1"=>2e18, "Cib_2"=>4e18) + size = CO2sizes[expt] + ep2d13C = 2.65 + duration = 1e4 # yr + PB.setvalue!(PB.get_parameter(CO2pulse_Cib, "perturb_times"), + [-1e30, -251.89e6, -251.89e6+1.0, -251.89e6+duration, -251.89e6+duration+1.0, 1e30]) + PB.setvalue!(PB.get_parameter(CO2pulse_Cib, "perturb_totals"), + [0.0, 0.0, size/duration, size/duration, 0.0, 0.0]) + PB.setvalue!(PB.get_parameter(CO2pulse_Cib, "perturb_deltas"), + ep2d13C.*[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) + + else + error("unrecognized expt='$(expt)'") + end + end + + return model +end + +function plot_PTB3box( + output; + pager=PALEOmodel.DefaultPlotPager() +) + pager(plot(title="Degass Weathering burial", output, + ["atm.ccdeg", "land.silw", "land.carbw", "land.oxidw", "fluxOceanBurial.flux_total_Ccarb", + "oceanfloor.shelf_Ccarb_total", "fluxOceanBurial.flux_total_Corg", "land.locb", "atm.Cia", "atm.Cib"], + ylabel="flux (mol C yr-1)")) + + return nothing +end diff --git a/examples/ocean3box/PTBClarkson2014/runtests.jl b/examples/ocean3box/PTBClarkson2014/runtests.jl new file mode 100644 index 0000000..f2c2576 --- /dev/null +++ b/examples/ocean3box/PTBClarkson2014/runtests.jl @@ -0,0 +1,71 @@ +using Test +using Logging +using DiffEqBase +using Sundials +import DataFrames + +import PALEOboxes as PB + +import PALEOreactions +import PALEOmodel + + +@testset "PTB examples" begin + +skipped_testsets = [ + # "PTB_2015", +] + +!("PTB_2015" in skipped_testsets) && @testset "PTB_2014" begin + + include("config_PTB3box_expts.jl") + + # Clarkson etal (2015) CO2LO scenario + model = config_PTB3box_expts("Co2LOmHWC4pp", ["Sw_2Ts", "Pp_PEes", "Lk_2", "Cia_s2", "Cib_1"]) + tspan=(-260e6, -251.88e6) # stop just after second C pulse # (-260e6,-240e6) + + initial_state, modeldata = PALEOmodel.initialize!(model) + run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + sol = PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + reltol=1e-5, + save_start=false, + dtmax=0.5e5, + ) + ) + + println("conservation checks:") + conschecks = [ + ("global", "total_S", :v, 1e-6 ), + # ("global", "total_S", :v_moldelta, 1e-6), # starts at 0.0 so rtol isn't useful + ] + for (domname, varname, propertyname, rtol) in conschecks + startval, endval = PB.get_property( + PB.get_data(run.output, domname*"."*varname), + propertyname=propertyname + )[[1, end]] + println(" check $domname.$varname $startval $endval $rtol") + @test isapprox(startval, endval, rtol=rtol) + end + + println("check values at end of run:") + checkvals = [ + ("ocean", "pHtot", [7.4870, 7.4438, 7.1207], 1e-4 ), + ("atm", "pCO2atm", 5926.39e-6, 2.5e-3), + ("atm", "CO2_delta", -7.0371, 1e-4), + ("ocean", "P_total", 6.8448e15, 1e-3), + ] + for (domname, varname, checkval, rtol) in checkvals + outputval = PB.get_data(run.output, domname*"."*varname)[end] + println(" check $domname.$varname $outputval $checkval $rtol") + @test isapprox(outputval, checkval, rtol=rtol) + end + +end + + +end diff --git a/examples/ocean3box/README.md b/examples/ocean3box/README.md new file mode 100644 index 0000000..00e19e1 --- /dev/null +++ b/examples/ocean3box/README.md @@ -0,0 +1,8 @@ +# 3 Box Ocean Examples + + julia> cd("PALEOocean/examples/ocean3box") + julia> include("PALEO_examples_ocean3box.jl") + +These examples demonstrate the 3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model, +standalone and coupled to the COPSE land surface and sediment/crust (as used in [Clarkson2015](@cite)). + diff --git a/examples/ocean3box/config_ocean3box_expts.jl b/examples/ocean3box/config_ocean3box_expts.jl new file mode 100644 index 0000000..b993365 --- /dev/null +++ b/examples/ocean3box/config_ocean3box_expts.jl @@ -0,0 +1,69 @@ + +"test cases and examples for 3 box ocean" +function config_ocean3box_expts(baseconfig, expts) + + if baseconfig == "oaonly_abiotic" + # Ocean-atmosphere only, test case cf Sarmiento & Toggweiler (2007) book, Fig 10.4, p436-7 + # This tests the effect of air-sea exchange rate for an abiotic model. + + # set k_piston below to show effect of default/fast/slow air-sea exchange rates + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_abiotic_base") + + elseif baseconfig == "oaonly" + # Ocean-atmosphere only (no weathering or burial) + # Use in conjunction with expt='killbio' (disables production at t=0 yr) to + # demonstrate effect of biological pump. + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_base") + + elseif baseconfig=="oaopencarb" + # Open atmosphere-ocean with silicate carbonate weathering input and carbonate burial + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base") + + elseif baseconfig=="oaopencorg" + # Semi-open atmosphere-ocean with organic carbon and sulphur burial + + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencorg_base") + + else + error("unrecognized baseconfig='$(baseconfig)'") + end + + ########################### + # configure expt + ############################ + + for expt in expts + println("Add expt: ", expt) + if expt == "baseline" + # defaults + elseif expt == "fastexchange" + react_airseaCO2 = PB.get_reaction(model, "oceansurface", "airsea_CO2") + PB.setvalue!(PB.get_parameter(react_airseaCO2, "piston"), 3.1e3) # m/day 'fast exchange' + elseif expt == "slowexchange" + react_airseaCO2 = PB.get_reaction(model, "oceansurface", "airsea_CO2") + PB.setvalue!(PB.get_parameter(react_airseaCO2, "piston"), 3.1e-3) # m/day 'slow exchange' + elseif expt == "killbio" + # disable biology at t=0 + react_enable_bioprod = PB.get_reaction(model, "global", "force_enable_bioprod") + PB.setvalue!(PB.get_parameter(react_enable_bioprod, "force_times"), [-1e30, 0.0, 1.0e-16, 1e30]) + PB.setvalue!(PB.get_parameter(react_enable_bioprod, "force_values"), [1.0, 1.0, 0.0, 0.0]) + elseif expt == "lowO2" + # start with low oxygen to test marine sulphur system + PB.set_variable_attribute!(model, "atm", "O2", :initial_value, 0.1*3.71e19) + elseif expt == "lowSO4" + # start with low SO4 to test methane cycling + PB.set_variable_attribute!(model, "ocean", "SO4", :initial_value, 100e-3) # ~100 uM + else + error("unrecognized expt='$(expt)'") + end + end + + return model +end diff --git a/examples/ocean3box/plot_ocean_3box.jl b/examples/ocean3box/plot_ocean_3box.jl new file mode 100644 index 0000000..3b68faa --- /dev/null +++ b/examples/ocean3box/plot_ocean_3box.jl @@ -0,0 +1,138 @@ +function plot_totals( + output; + species=["C", "TAlk", "TAlkerror", "P", "O2", "S"], + pager=PALEOmodel.DefaultPlotPager() +) + # conservation checks + if "C" in species + pager(plot(title="Total C", output, ["global.total_C"], ylabel="C (mol)",)) + pager(plot(title="Total C moldelta", output, ["global.total_C.v_moldelta"], ylabel="C_moldelta (mol * delta)",)) + pager((plot(title="Total C components", output, ["global.total_C", "atm.CO2"]); + plot!(output, ["ocean.DIC"], (cell=[:s, :h, :d], ), ylabel="C (mol)"))) + end + if "TAlk" in species + pager(plot(title="Total TAlk", output, ["ocean.TAlk_total"], ylabel="TAlk (mol)",)) + end + if "TAlkerror" in species + pager(plot(title="Ocean TAlk error", output, ["ocean.pHfree_constraint"], (cell=[:s, :h, :d], ), ylabel="TAlk error (mol)",)) + end + if "P" in species + pager(plot(title="Total P", output, ["ocean.P_total"], ylabel="P (mol)",)) + end + if "O2" in species + pager(plot(title="Total O2", output, ["global.total_O2"], ylabel="O2 (mol)",)) + end + if "S" in species + pager(plot(title="Total S", output, ["global.total_S"], ylabel="S (mol)",)) + pager(plot(title="Total S moldelta", output, ["global.total_S.v_moldelta"], ylabel="S_moldelta (mol * delta)",)) + end + + pager(:newpage) + + return nothing +end + +function plot_ocean_tracers( + output; + tracers=[ + "DIC_conc", "TAlk_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "CH4_conc","P_conc", "Ca_conc", + "H2S_delta", "SO4_delta", "CH4_delta", "OmegaAR"], + pager=PALEOmodel.DefaultPlotPager() +) + for tr in tracers + pager(plot(title=tr, output, ["ocean.$tr"], (cell=[:s, :h, :d], ) )) + end + + return nothing +end + + +function plot_oaonly_abiotic( + output; + pager=PALEOmodel.DefaultPlotPager() +) + pager((plot(title="d13C", output, ["atm.CO2_delta"]); + plot!(output, ["ocean.DIC_delta"], (cell=[:s, :h, :d], ), ylabel="per mil",))) + pager((plot(title="Air-sea CO2", output, ["fluxAtmtoOceansurface.flux_total_CO2"]); + plot!(output, "fluxAtmtoOceansurface.flux_CO2", (cell=[:s, :h], ), ylabel="mol yr-1",))) + pager((plot(title="pCO2", output, ["atm.pCO2atm"], ); + plot!(output, ["ocean.pCO2"], (cell=[:s, :h, :d], ), ylabel="pCO2 (atm)",))) + if PB.has_variable(output, "atm.pO2PAL") + pager(plot(title="pO2", output, ["atm.pO2PAL"])) + end + + return nothing +end + +function plot_carb_open( + output; + pager=PALEOmodel.DefaultPlotPager() +) + pager(plot(title="flys (fraction of Ccarb export buried)", output, ["oceanfloor.flys"], (cell=[:s, :h, :d],), ylabel="flys")) + + pager((plot(title="Weathering burial", output, ["land.silw", "land.carbw", + "fluxOceanBurial.flux_total_Ccarb", "oceanfloor.shelf_Ccarb_total", "oceanfloor.deep_Ccarb_total"]); + plot!(output, "fluxOceanfloor.particulateflux_Ccarb", (cell=:d, ), ylabel="flux (mol C yr-1)", ylim=(0, 3e13)))) + + pager((plot(title="Temperature", output, ["global.TEMP"]); + plot!(output, "ocean.temp", (cell=[:s, :h, :d], ), ylabel="temp (K)",))) + return nothing +end + +function plot_corg_open( + output; + pager=PALEOmodel.DefaultPlotPager() +) + pager(plot(title="P input output", output, + ["fluxRtoOcean.flux_P", "fluxOceanBurial.flux_total_P"], ylabel="P flux (mol P yr-1)", ylim=(0, 1e11))) + pager(plot(title="Corg burial", output, + ["fluxOceanBurial.flux_total_Corg"], ylabel="Corg burial flux (mol C yr-1)", ylim=(0, 1e13))) + pager(plot(title="S input output", output, + ["fluxRtoOcean.flux_SO4", "fluxOceanBurial.flux_total_GYP", "fluxOceanBurial.flux_total_PYR"], + ylabel="S flux (mol S yr-1)", ylim=(0, 5e12))) + + return nothing +end + +function plot_copse_totals( + output; + pager=PALEOmodel.DefaultPlotPager() +) + pager(plot(title="Carbon", output, ["global.total_C", "sedcrust.C", "sedcrust.G", "atm.CO2", "ocean.DIC_total", "ocean.CH4_total"], ylabel="mol C",)), + pager(plot(title="Total carbon", output, ["global.total_C"], ylabel="mol C",)) + pager(plot(title="Total carbon moldelta", output, ["global.total_C.v_moldelta"], ylabel="mol C * per mil",)) + pager(plot(title="Sulphur", output, ["global.total_S", "ocean.SO4_total", "ocean.H2S_total", "sedcrust.PYR", "sedcrust.GYP"], ylabel="mol S",)) + pager(plot(title="Total sulphur", output, ["global.total_S"], ylabel="mol S",)) + pager(plot(title="Total sulphur moldelta",output, ["global.total_S.v_moldelta"], ylabel="mol S * per mil",)) + pager(plot(title="Total redox", output, ["global.total_redox"], ylabel="mol O2 equiv",)) + + pager(:newpage) + + return nothing +end + +function plot_copse_reloaded( + output; + pager=PALEOmodel.DefaultPlotPager() +) + + pager(plot(title="Physical forcings", output, "global.".*["DEGASS", "UPLIFT", "PG"], ylim=(0, 2.0), ylabel="normalized forcing",)) + pager(plot(title="Land area forcings", output, ["land.BA_AREA", "land.GRAN_AREA", "global.ORGEVAP_AREA"], ylim=(0, 2.5), ylabel="normalized forcing",)) + pager(plot(title="Basalt area forcings", output, ["global.CFB_area", "land.oib_area"], ylabel="area (km^2)",)) + pager(plot(title="Evolutionary forcings", output, "global.".*["EVO", "W", "Bforcing", "PELCALC", "CPland_relative", "F_EPSILON", "COAL"], ylabel="normalized forcing",)) + + pager(plot(title="Silicate weathering", output, ["land.silw", "land.granw", "land.basw", "oceanfloor.sfw_total"], ylabel="flux (mol/yr)",)) + + pager((plot(title="Sulphur isotopes", output, ["sedcrust.PYR_delta", "sedcrust.GYP_delta"]); + plot!(output, ["ocean.SO4_delta", "ocean.H2S_delta"], (cell=[:s, :h, :d], ), ylabel="delta 34S (per mil)",))) + pager((plot(title="Carbon isotopes", output, ["sedcrust.G_delta", "sedcrust.C_delta", "atm.CO2_delta"]); + plot!(output, ["ocean.DIC_delta", "ocean.CH4_delta"], (cell=[:s, :h, :d], ), ylabel="per mil",))) + + pager(plot(title="Sr sed reservoir", output, ["sedcrust.Sr_sed"], ylabel="Sr_sed (mol)",)) + pager(plot(title="Sr ocean reservoir", output, ["ocean.Sr"], ylabel="Sr (mol)",)) + pager(plot(title="Sr fluxes", output, ["fluxRtoOcean.flux_Sr", "fluxOceanBurial.flux_total_Sr", "fluxOceanfloor.soluteflux_total_Sr" ], ylabel="Sr flux (mol yr-1)",)) + pager((plot(title="Sr isotopes", output, ["sedcrust.Sr_mantle_delta", "sedcrust.Sr_new_ig_delta", "sedcrust.Sr_old_ig_delta", "sedcrust.Sr_sed_delta"]); + plot!(output, "ocean.Sr_delta", (cell=1, ), ylabel="87Sr",))) + + return nothing +end diff --git a/examples/ocean3box/runtests.jl b/examples/ocean3box/runtests.jl new file mode 100644 index 0000000..650f87f --- /dev/null +++ b/examples/ocean3box/runtests.jl @@ -0,0 +1,168 @@ +using Test +using Logging +using DiffEqBase +using Sundials +import DataFrames + +import PALEOboxes as PB + +import PALEOocean +import PALEOcopse +import PALEOmodel + + +@testset "ocean3box examples" begin + +skipped_testsets = [ + # "oaonly_abiotic", + # "oaonly", + # "oaopencarb", +] + +!("oaonly_abiotic" in skipped_testsets) && @testset "oaonly_abiotic" begin + + include("config_ocean3box_expts.jl") + + model = config_ocean3box_expts("oaonly_abiotic", ["baseline"]); tspan=(0,1e4) + + initial_state, modeldata = PALEOmodel.initialize!(model) + run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + sol = PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) + ) + + println("conservation checks:") + conschecks = [ + ("global", "total_C", :v, 1e-6 ), + ("global", "total_C", :v_moldelta, 1e-6), + ("ocean", "TAlk_total", nothing, 1e-6), + ] + for (domname, varname, propertyname, rtol) in conschecks + startval, endval = PB.get_property( + PB.get_data(run.output, domname*"."*varname), + propertyname=propertyname + )[[1, end]] + println(" check $domname.$varname $startval $endval $rtol") + @test isapprox(startval, endval, rtol=rtol) + end + + println("check values at end of run:") + checkvals = [ + ("ocean", "pHfree", [8.236714539086925, 8.22352510474905, 8.149122544874377], 1e-4 ), + ("atm", "pCO2atm", 301.244e-6, 1e-4), + ("atm", "CO2_delta", -10.161, 1e-4), + ] + for (domname, varname, checkval, rtol) in checkvals + outputval = PB.get_data(run.output, domname*"."*varname)[end] + println(" check $domname.$varname $outputval $checkval $rtol") + @test isapprox(outputval, checkval, rtol=rtol) + end + +end + +!("oaonly" in skipped_testsets) && @testset "oaonly" begin + + include("config_ocean3box_expts.jl") + + model = config_ocean3box_expts("oaonly", ["baseline"]); tspan=(0,1e4) + + initial_state, modeldata = PALEOmodel.initialize!(model) + run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + sol = PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) + ) + + println("conservation checks:") + conschecks = [ + ("global", "total_C", :v, 1e-6 ), + ("global", "total_C", :v_moldelta, 1e-6), + ("global", "total_O2", nothing, 1e-6 ), + ("global", "total_S", :v, 1e-6 ), + ("global", "total_S", :v_moldelta, 1.0 ), # starts at 0.0 so no reltol test + ("ocean", "TAlk_total",nothing, 1e-6) + ] + for (domname, varname, propertyname, rtol) in conschecks + startval, endval = PB.get_property( + PB.get_data(run.output, domname*"."*varname), + propertyname=propertyname + )[[1, end]] + println(" check $domname.$varname $startval $endval $rtol") + @test isapprox(startval, endval, rtol=rtol) + end + + println("check values at end of run:") + checkvals = [ + ("ocean", "pHfree", [8.38775021448627, 8.367024330312566, 8.107940989697116], 1e-4 ), + ("atm", "pCO2atm", 191.423e-6, 1e-4), + ("atm", "CO2_delta", -8.1965, 1e-4), + ("atm", "pO2atm", 0.2102, 1e-4), + ] + for (domname, varname, checkval, rtol) in checkvals + outputval = PB.get_data(run.output, domname*"."*varname)[end] + println(" check $domname.$varname $outputval $checkval $rtol") + @test isapprox(outputval, checkval, rtol=rtol) + end + +end + +!("oaopencarb" in skipped_testsets) && @testset "oaopencarb" begin + + include("config_ocean3box_expts.jl") + + model = config_ocean3box_expts("oaopencarb", ["killbio", "lowO2"]); tspan=(-10e6,10e6) + + initial_state, modeldata = PALEOmodel.initialize!(model) + run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + sol = PALEOmodel.ODE.integrateDAEForwardDiff( + run, initial_state, modeldata, tspan, + alg=IDA(linear_solver=:KLU), + solvekwargs=( + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + save_start=false + ) + ) + + println("conservation checks:") + conschecks = [ + ("global", "total_S", :v, 1e-6 ), + ("global", "total_S", :v_moldelta, 1), # run starts with 0.0 + ("ocean", "P_total", nothing, 1e-6) + ] + for (domname, varname, propertyname, rtol) in conschecks + startval, endval = PB.get_property( + PB.get_data(run.output, domname*"."*varname), + propertyname=propertyname + )[[1, end]] + println(" check $domname.$varname $startval $endval $rtol") + @test isapprox(startval, endval, rtol=rtol) + end + + println("check values at end of run:") + checkvals = [ + ("ocean", "pHtot", [8.32404, 8.40819, 8.3423], 1e-4 ), + ("atm", "pCO2atm", 0.000209573, 1e-4), + ("atm", "CO2_delta", -8.51631 , 1e-4), + ] + for (domname, varname, checkval, rtol) in checkvals + outputval = PB.get_data(run.output, domname*"."*varname)[end] + println(" check $domname.$varname $outputval $checkval $rtol") + @test isapprox(outputval, checkval, rtol=rtol) + end + +end + + +end diff --git a/src/ocean/BioProd.jl b/src/ocean/BioProd.jl new file mode 100644 index 0000000..aa52275 --- /dev/null +++ b/src/ocean/BioProd.jl @@ -0,0 +1,737 @@ +"Ocean production Reactions" +module BioProd + +import PALEOboxes as PB +using PALEOboxes.DocStrings + +import PALEOaqchem + +# import Infiltrator # Julia debugger + +""" + ReactionBioProdPrest + +P-limited biological production, configurable to restore P to specified level or to consume fraction of nutrients + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionBioProdPrest{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParIntVec("bioprod", Int[], allowed_values=[0,1,2,3], + description="production type (per cell): 0 - none, 1 - restore to absolute conc, 2 - consume fraction of nutrients supplied, 3 - restore to fraction of ocean mean"), + PB.ParDoubleVec("bioprodval", Float64[], units="m-3 or none", + description="conc or frac corresponding to 'bioprod'"), + PB.ParDouble("rCorgPO4", 106.0, units="", + description="Corg:P Redfield ratio of organic matter produced"), + PB.ParDouble("rNPO4", 16.0, units="", + description="N:P Redfield ratio of organic matter produced"), + PB.ParDouble("rCcarbCorg", 0.0, units="", + description="ratio of Ccarb to Corg produced"), + PB.ParDouble("trest", 0.1, units="yr", + description="restoring timescale"), + PB.ParType(PB.AbstractData, "CIsotope", PB.ScalarData, + external=true, + allowed_values=PB.IsotopeTypes, + description="disable / enable carbon isotopes and specify isotope type"), + ) +end + +function PB.register_methods!(rj::ReactionBioProdPrest) + + CIsotopeType = rj.pars.CIsotope[] + PB.setfrozen!(rj.pars.CIsotope) + + vars = [ + PB.VarDep("volume", "m^3", "ocean cell volume"), + PB.VarDepScalar("volume_total", "m^3", "ocean total volume"), + PB.VarDep("P_conc", "mol m^-3", "total P concentration"), + + PB.VarProp("%reaction%Prod_Corg", "mol yr-1", "organic carbon production rate", + attributes=(:field_data=>CIsotopeType, :calc_total=>true,)), + PB.VarProp("%reaction%Prod_Ccarb", "mol yr-1", "carbonate production rate", + attributes=(:field_data=>CIsotopeType, :calc_total=>true,)), + PB.VarContrib("P_sms", "mol yr-1", "total dissolved P source minus sink"), + PB.VarContrib("(O2_sms)", "mol yr-1", "O2 source minus sink"), + PB.VarContrib("(TAlk_sms)", "mol yr-1", "TAlk source minus sink"), + PB.VarContrib("(DIC_sms)", "mol yr-1", "DIC source minus sink", + attributes=(:field_data=>CIsotopeType,)), + + PB.VarDepScalar("(global.enable_bioprod)", "", "optional forcing, =0.0 to disable, !=0.0 to enable"), + + PB.VarDepScalar("(global.PELCALC)", "", "optional forcing for pelagic calcification"), + ] + + # add optional Variables needed for par_bioprod options + if any(rj.pars.bioprod .== 2) + push!(vars, PB.VarDep("P_transport_input", "mol yr-1", "transport P input")) + end + if any(rj.pars.bioprod .== 3) + push!(vars, PB.VarDepScalar("P_total", "mol", "total P")) + end + PB.setfrozen!(rj.pars.bioprod) + + # add Variable required for isotopes + if CIsotopeType <: PB.AbstractIsotopeScalar + push!(vars, + PB.VarDep("DIC_delta", "per mil", "d13C DIC"), + PB.VarDepScalar("D_mccb_DIC", "per mil", "d13C marine calcite burial fractionation relative to ocean DIC"), + PB.VarDepScalar("D_B_mccb_mocb","per mil", "d13C fractionation between marine organic and calcite burial"), + ) + end + + + prod_vars = PB.Fluxes.FluxContrib( + "prod_", ["P", "N", "Corg::$CIsotopeType", "Ccarb::$CIsotopeType"], + ) + + PB.add_method_do!( + rj, + do_bio_prod_Prest, + (PB.VarList_namedtuple(vars), PB.VarList_namedtuple_fields(prod_vars)), + p = CIsotopeType + ) + + PB.add_method_do_totals_default!(rj) + + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +# Override to validate parameters +function PB.check_configuration(rj::ReactionBioProdPrest, model::PB.Model) + configok = true + + length(rj.pars.bioprod) == PB.get_length(rj.domain) || + (configok = false; @error "$(PB.fullname(rj)) invalid Parameter 'bioprod' length != Domain size") + + length(rj.pars.bioprodval) == PB.get_length(rj.domain) || + (configok = false; @error "$(PB.fullname(rj)) invalid Parameter 'bioprodval' length != Domain size") + + return configok +end + +function do_bio_prod_Prest(m::PB.ReactionMethod, pars, (vars, prod_vars), cellrange::PB.AbstractCellRange, deltat) + rj = m.reaction + CIsotopeType = m.p + + for i in cellrange.indices + ProdTotP = zero(vars.P_conc[i]) # use zero() to get correct type eg if using AD + if isnothing(vars.enable_bioprod) || vars.enable_bioprod[] != 0 + if pars.bioprod[i] == 0 + # no production + # ProdTotP = 0.0 + elseif pars.bioprod[i] == 1 + # restore to absolute P level + # mol yr-1 mol m-3 m^3 / yr + ProdTotP += max(vars.P_conc[i] - pars.bioprodval[i], 0.0)*vars.volume[i]/pars.trest[] + elseif pars.bioprod[i] == 2 + # consume a fraction of nutrients supplied by circulation into this box + ProdTotP += pars.bioprodval[i] * vars.P_transport_input[i] + elseif pars.bioprod[i] == 3 + # restore to fraction of ocean mean P level + mean_P_conc = vars.P_total[]/vars.volume_total[] + restore_P_conc = mean_P_conc * pars.bioprodval[i] + # mol yr-1 mol m-3 m^3 / yr + ProdTotP += max(vars.P_conc[i] - restore_P_conc, 0.0)*vars.volume[i]/pars.trest[] + else + error("bioprod=$(pars.bioprod[i]) not supported") + end + end + ProdTotN = ProdTotP * pars.rNPO4[] + + ProdTotCorg_total = ProdTotP * pars.rCorgPO4[] + ProdTotCcarb_total = ProdTotCorg_total*pars.rCcarbCorg[]*PB.get_if_available(vars.PELCALC, 1.0) + if CIsotopeType <: PB.AbstractIsotopeScalar + # calculate d13C + delta13C_prod_ccarb = vars.DIC_delta[i] + vars.D_mccb_DIC[] + delta13C_prod_corg = delta13C_prod_ccarb - vars.D_B_mccb_mocb[] + end + ProdTotCorg = @PB.isotope_totaldelta(CIsotopeType, ProdTotCorg_total, delta13C_prod_corg) + vars.Prod_Corg[i] = ProdTotCorg + ProdTotCcarb = @PB.isotope_totaldelta(CIsotopeType, ProdTotCcarb_total, delta13C_prod_ccarb) + vars.Prod_Ccarb[i] = ProdTotCcarb + + # corresponding O2, TAlk uptake + (uptakeOrgO2, uptakeAlk) = PALEOaqchem.O2AlkUptakeRemin(ProdTotCorg_total, (ProdTotN, 0, 0), ProdTotP, ProdTotCcarb_total) + + # all state variable sms except P_sms are optional to allow use with eg a P only configuration + vars.P_sms[i] += -ProdTotP + PB.add_if_available(vars.O2_sms, i, -uptakeOrgO2) + PB.add_if_available(vars.TAlk_sms, i, -uptakeAlk) + PB.add_if_available(vars.DIC_sms, i, -(ProdTotCorg + ProdTotCcarb)) + + PB.add_if_available(prod_vars.P, i, ProdTotP) + PB.add_if_available(prod_vars.N, i, ProdTotN) + PB.add_if_available(prod_vars.Corg, i, ProdTotCorg) + PB.add_if_available(prod_vars.Ccarb, i, ProdTotCcarb) + + end + + return nothing +end + +""" + ReactionBioProdMMPop + +Ocean phytoplankton biological production. + +Configurable to represent oxygenic photosynthesizers with P, N limitation, nitrogen fixers, anoxygenic photosynthesis limited +by electron donor availability. + +Production is represented as a combination of limiting factors: + +`population_size x nutrient_limitation x light_limitation x temperature_limitation x electron_donor_limitation` + +`population_size` may be either implicit (either a constant or ∝ nutrient concentration, generating GENIE-like parameterisations of +export production), or represented explicitly as a state variable. + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionBioProdMMPop{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + # Parameters controlling stoichiometry of organic matter and carbonate produced + PB.ParDouble("rCorgPO4", 106.0, units="", + description="Corg:P Redfield ratio of organic matter produced"), + PB.ParDouble("rNPO4", 16.0, units="", + description="N:P Redfield ratio of organic matter produced"), + PB.ParDouble("rCcarbCorg", 0.0, units="", + description="ratio of Ccarb to Corg produced"), + PB.ParBool("rCcarbCorg_fixed", true, + description = "Ccarb:Corg rain ratio true for fixed, false for sat. state dependent"), + PB.ParDouble("k_r0", 0.044372, + description = "initial rain-ratio for sat. state dependent rain ratio"), + PB.ParDouble("k_eta", 0.8053406, + description= "exponent for sat. state dependent rain ratio"), + + PB.ParDouble("nuDOM", 0.66, units="", + description="fraction of production to DOM reservoir"), + PB.ParDouble("depthlimit", -200.0, units="m", + description="depth limit for production"), + + # Parameters for population growth rate model + PB.ParString("k_poptype", "Constant", allowed_values=["Constant", "Nutrient", "Pop"], + description = "population / growth rate model"), + # Parameters controlling (max) growth rate and losses + PB.ParDouble("k_uPO4", 2.0e-3, units="mol P / m-3 / yr", + description="for k_poptype = 'Constant': max rate, constant ie of form k_O_uPO4 * (light, nut etc)"), + PB.ParDouble("k_mu", NaN, units="1/yr", + description="for k_poptype = 'Nutrient', 'Pop': max prod/growth rate (at 0C if templim=='Eppley')"), + PB.ParDouble("k_grazeresprate", NaN, units="1/yr", + description="for k_poptype = 'Pop' imposed const loss (mortality) rate"), + + # Parameters for temperature limitation + PB.ParString("k_templim", "Constant", allowed_values=["Constant", "Eppley"], + description="temperature limitation factor"), + + # Parameters for light limitation + PB.ParString("k_lightlim", "linear", allowed_values=["linear","MM","fixed","QE"], + description="Light limitation function"), + PB.ParDouble("k_Irel", 1.0, + description="multiplier for forcing-supplied insolation"), + PB.ParDouble("k_Ic", 30.0, units="W/m^2", + description="saturating irradiance for 'MM' case"), + PB.ParDouble("k_alphaQE", 7.0, units="mgC/mgChl/Wpar m^-2/d-1", + description="chla-specific initial slope of the photosynthesis-light curve for lightlim='QE'"), + PB.ParDouble("k_thetaChlC", 0.03, units="mg Chl / mgC", + description="Chl:Corg ratio for explicit population k_poptype=Pop"), + PB.ParDouble("k_epsilonChl", 0.012, units="m^2/mg Chl", + description="chl absorption coeff (for self shielding) for k_poptype='Pop'"), + + # Parameters for nutrient limitation + PB.ParString("k_nuttype", "PO4MM", allowed_values=["PO4MM", "PO4NMM", "PO4NMMNfix"], + description="Nutrient limitation / nitrogen fixation function"), + PB.ParDouble("k_KPO4", NaN, units="mol P m-3 ", + description="limitation at low [P] MM half-max constant"), + PB.ParDouble("k_KN", 0.0, units="mol NO3+NH4 m-3", + description="limitation at low nitrogen"), + PB.ParDouble("k_prefNH3", 10.0, units="", + description="preference for ammonia over nitrate"), + + # Parameters controlling electron donor + PB.ParString("k_edonor", "H2O", allowed_values=["H2O", "H2S"], + description="electron donor (H2O for oxygenic phototroph"), + PB.ParDouble("k_KH2S", 1.0e-3, units="mol H2S m-3", + description="limitation at low [H2S] MM half-max constant"), + + # Isotopes + PB.ParType(PB.AbstractData, "CIsotope", PB.ScalarData, + external=true, + allowed_values=PB.IsotopeTypes, + description="disable / enable carbon isotopes and specify isotope type"), + ) +end + +function PB.register_methods!(rj::ReactionBioProdMMPop) + + CIsotopeType = rj.pars.CIsotope[] + PB.setfrozen!(rj.pars.CIsotope) + + grid_vars = [ + PB.VarDep("volume", "m^3", "ocean cell volume"), + PB.VarDep("zupper", "m", "cell depth (-ve)"), + ] + + vars = [ + PB.VarDepColumn("oceansurface.open_area_fraction","","fraction of area open to atmosphere"), + + PB.VarDep("P_conc", "mol m^-3", "total P concentration"), + PB.VarDep("(OmegaCA)", "", "calcite saturation state"), + + PB.VarDepScalar("(global.rate_bioprod)", "", "optional forcing, multiplier for productivity"), + PB.VarDepScalar("(global.PELCALC)", "", "optional forcing for pelagic calcification"), + + # set :initialize_to_zero as do_react may only cover some cells (within depth range) + PB.VarProp("%reaction%Prod_Corg", "mol yr-1", "organic carbon production rate", + attributes=(:field_data=>CIsotopeType, :initialize_to_zero=>true, :calc_total=>true)), + PB.VarProp("%reaction%Prod_Ccarb", "mol yr-1", "carbonate production rate", + attributes=(:field_data=>CIsotopeType, :initialize_to_zero=>true, :calc_total=>true)), + + PB.VarContrib("P_sms", "mol yr-1", "total dissolved P source minus sink"), + PB.VarContrib("(TAlk_sms)", "mol yr-1", "TAlk source minus sink"), + PB.VarContrib("(DIC_sms)", "mol yr-1", "DIC source minus sink", + attributes=(:field_data=>CIsotopeType,)), + ] + + append!(vars, + PB.Fluxes.FluxContrib("partprod_", ["P", "N", "Corg::$CIsotopeType", "Ccarb::$CIsotopeType"]) + ) + append!(vars, + PB.Fluxes.FluxContrib("domprod_", ["P", "N", "Corg::$CIsotopeType", "Ccarb::$CIsotopeType"]) + ) + + if CIsotopeType <: PB.AbstractIsotopeScalar + push!(vars, + PB.VarDep("DIC_delta", "per mil", "d13C DIC"), + PB.VarDepScalar("D_mccb_DIC", "per mil", "d13C marine calcite burial relative to ocean DIC"), + PB.VarDepScalar("D_B_mccb_mocb","per mil", "d13C fractionation between marine organic and calcite burial"), + ) + end + + # get functions for edonor, nutrient, light limitation + # these add any required VariableReactions to vars + (f_edonor_lim, edonor_ratestoich) = get_edonor_functions(vars, rj.pars.k_edonor[]) + PB.setfrozen!(rj.pars.k_edonor) + PB.add_method_do!(rj, edonor_ratestoich) + + f_nuttype = get_nuttype_function(rj.pars.k_nuttype[]) + PB.setfrozen!(rj.pars.k_nuttype) + + (f_rate_poptype, f_loss_poptype)= get_rate_poptype_functions(rj, grid_vars, vars, rj.pars.k_poptype[]) + PB.setfrozen!(rj.pars.k_poptype) + + f_lightlim = get_lightlim_function(vars, rj.pars.k_lightlim[]) + PB.setfrozen!(rj.pars.k_lightlim) + + f_templim = get_templim_function(vars, rj.pars.k_templim[]) + PB.setfrozen!(rj.pars.k_templim) + + PB.add_method_do!( + rj, + do_bio_prod_MM_pop, + ( + PB.VarList_namedtuple(grid_vars), + PB.VarList_namedtuple(vars), + ), + p = (CIsotopeType, f_edonor_lim, f_nuttype, f_rate_poptype, f_loss_poptype, f_lightlim, f_templim), + ) + + PB.add_method_do_totals_default!(rj) + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + + +function do_bio_prod_MM_pop( + m::PB.ReactionMethod, + pars, + (grid_vars, vars), + cellrange::PB.AbstractCellRange, + deltat +) + + CIsotopeType, f_edonor_lim, f_nuttype, f_rate_poptype, f_loss_poptype, f_lightlim, f_templim = m.p + + for (isurf, colindices) in cellrange.columns + for i in colindices + if grid_vars.zupper[i] > pars.depthlimit[] + # dimensionless (0-1) + (lim_nut, (fracTNH3, fracNO3, fracNfix)) = f_nuttype(pars, vars, i) + + # dimensionless (0-1) + lim_edonor = f_edonor_lim(pars, vars, i) + + # mol P/m^3/yr + rate_pop = f_rate_poptype(pars, vars, i) + + # dimensionless + lim_temp = f_templim(pars, vars, i) + + # dimensionless + lim_Ifac = f_lightlim(pars, vars, i, pars.k_mu[]*lim_temp) + + # optional forcing for production rate + rate_bioprod = PB.get_if_available(vars.rate_bioprod, 1.0) + + # mol P yr-1 + ProdTotP = ( + rate_bioprod + *rate_pop # mol P m-3 yr-1 + *lim_temp + *lim_nut + *lim_edonor + *lim_Ifac + *vars.open_area_fraction[isurf] + *grid_vars.volume[i] # m^3 + ) + + # mol P yr-1 + LossTotP = f_loss_poptype(pars, vars, i, ProdTotP) + + # stoichiometry + NtoP = pars.rNPO4[] + CorgtoP = pars.rCorgPO4[] + if pars.rCcarbCorg_fixed[] + CcarbtoCorgfac = pars.rCcarbCorg[] + else + CcarbtoCorgfac = pars.k_r0[]*max(vars.OmegaCA[i]-1, 0.0)^pars.k_eta[] + end + CcarbtoP = CcarbtoCorgfac * pars.rCorgPO4[]*PB.get_if_available(vars.PELCALC, 1.0) + + # corresponding O2, TAlk uptake for Corg only (to subtract from tracer _sms) + # -ve + # @Infiltrator.infiltrate + (uptakeOrgO2eqtoP, uptakeAlkOrgtoP) = PALEOaqchem.O2AlkUptakeRemin( + CorgtoP, + NtoP.*(fracNO3, fracTNH3, fracNfix), + 1.0, + 0.0) + + if CIsotopeType <: PB.AbstractIsotopeScalar + # calculate d13C + delta13C_ccarb = vars.DIC_delta[i] + vars.D_mccb_DIC[] + delta13C_corg = delta13C_ccarb - vars.D_B_mccb_mocb[] + end + ProdTotCorg = @PB.isotope_totaldelta(CIsotopeType, ProdTotP*CorgtoP, delta13C_corg) + vars.Prod_Corg[i] = ProdTotCorg + LossTotCorg = @PB.isotope_totaldelta(CIsotopeType, LossTotP*CorgtoP, delta13C_corg) + ProdTotCcarb = @PB.isotope_totaldelta(CIsotopeType, ProdTotP*CcarbtoP, delta13C_ccarb) + vars.Prod_Ccarb[i] = ProdTotCcarb + LossTotCcarb = @PB.isotope_totaldelta(CIsotopeType, LossTotP*CcarbtoP, delta13C_ccarb) + + # Losses -> particulates (export) + PB.add_if_available(vars.partprod_P, i, (1-pars.nuDOM[])*LossTotP) + PB.add_if_available(vars.partprod_N, i, (1-pars.nuDOM[])*LossTotP*NtoP) + PB.add_if_available(vars.partprod_Corg, i, (1-pars.nuDOM[])*LossTotCorg) + PB.add_if_available(vars.partprod_Ccarb,i, (1-pars.nuDOM[])*LossTotCcarb) + + # Losses -> DOM + PB.add_if_available(vars.domprod_P, i, pars.nuDOM[]*LossTotP) + PB.add_if_available(vars.domprod_N, i, pars.nuDOM[]*LossTotP*NtoP) + PB.add_if_available(vars.domprod_Corg, i, pars.nuDOM[]*LossTotCorg) + # NB: no Ccarb -> DOM, so add this back to DIC/TAlk immediately as a 'short circuit' + CcarbtoDOM = pars.nuDOM[]*LossTotCcarb + + # Tendencies (nutrients etc consumed) + # all state variable sms except P_sms are optional to allow use with eg a P only configuration + vars.P_sms[i] += -ProdTotP + vars.edonorO2eq[i] = -uptakeOrgO2eqtoP*ProdTotP + + PB.add_if_available(vars.TAlk_sms, i, -uptakeAlkOrgtoP*ProdTotP - 2.0*PB.get_total(ProdTotCcarb) + 2.0*PB.get_total(CcarbtoDOM)) + PB.add_if_available(vars.DIC_sms, i, -(ProdTotCorg + ProdTotCcarb) + CcarbtoDOM) + # TODO nitrogen + + end + end + end + + return nothing +end + +"Michaelis-Menton like resource limitation" +@inline function lim_MM(val, halfmax) + lim = max(PB.get_total(val), 0.0)/(max(PB.get_total(val), 0.0) + halfmax) + return lim +end + +function get_nuttype_function(nuttype::AbstractString) + + "smooth function approximating step function: + at large k, h(x<0) = 0, h(x>0) = 1" + hsmooth(x, k=100.0) = 1.0/(1.0+exp(-k*x)) + + # Nutrient limitation functions: nut_X(rj, vars, ixd) -> (nutlim, (fracTNH3, fracNO3, fracNfix)) + "P limitation only" + function nut_PO4MM(pars, vars, i) + lim_Pfac = lim_MM(vars.P_conc[i], pars.k_KPO4[]) + return (lim_Pfac, (0.0, 1.0, 0.0)) + end + + "P,N limited phytoplankton: GENIE 2N2T_PO4MM_NO3 (cf Fennel etal 2005)" + function nut_PO4NMM(pars, vars, i) + lim_Pfac = lim_MM(vars.P_conc[i], pars.k_KPO4[]) + cTNH3 = max(vars.TNH3_conc[i], 0.0) + cNO3 = max(vars.NO3_conc[i], 0.0) + lim_Nfac = (cTNH3 + cNO3)/(cTNH3 + cNO3 + pars.k_KN[]) + fracTNH3 = pars.k_prefNH3[]/(pars.k_O_prefNH3[]*cTNH3 + cNO3 + eps()) + fracNO3 = 1.0-fracTNH3 + fracNfix = 0.0 + + return (min(lim_Pfac, lim_Nfac), (fracTNH3, fracNO3, fracNfix)) + end + + "P limited nitrogen fixer: GENIE 2N2T_PO4MM_NO3 (cf Fennel etal 2005)" + function nut_PO4NMMNfix(pars, vars, i) + lim_Pfac = lim_MM(vars.P_conc[i], pars.k_KPO4[]) + + # Nfix where NO3 + TNH3 < abs(obj.k_O_KN) + cN = max(vars.TNH3[i], 0.0) + max(vars.NO3_conc[i], 0.0) + Nlowfac = hsmooth((pars.k_O_KN[] - cN)/pars.k_KN[]) # > 0 when N fix allowed + + # Nfix where N:P < redfield + Nredfac = hsmooth(16.0 - cN./max(vars.P_conc[i],eps())) + + # Combine both criteria: ->1 where N fix permitted + lim_Nfac = Nlowfac*Nredfac + + return (lim_Pfac*lim_Nfac, (0.0, 0.0, 1.0)) + end + + if nuttype == "PO4MM" + return nut_PO4MM + elseif nuttype == "PO4NMM" + return nut_PO4NMM + elseif nuttype == "PO4NMMNfix" + return nut_PO4NMMNfix + else + error("unknown nuttype ", nuttype) + end +end + +"Stoichiometry for H2O as e- donor" +const default_edonorH2O = PB.RateStoich( + PB.VarProp("edonorO2eq", "mol O2eq yr-1", "O2eq e- donor consumption (H2O) by oxygenic photosynthesis", + # set :initialize_to_zero as do_react may only cover some cells (within depth range) + attributes=(:initialize_to_zero=>true, :calc_total=>true)), + ((1.0, "O2"),), + sms_prefix="", sms_suffix="_sms", + processname="production", +) + +"Stoichiometry for H2S as e- donor" +const default_edonorH2S = PB.RateStoich( + PB.VarProp("edonorO2eq", "mol O2 eq yr-1", "O2eq e- donor consumption (H2S) by anoxygenic photosynthesis", + # set :initialize_to_zero as do_react may only cover some cells (within depth range) + attributes=(:initialize_to_zero=>true, :calc_total=>true)), + ((-0.5, "H2S::Isotope"), (0.5, "SO4::Isotope"), (-1.0, "TAlk")), + deltavarname_eta = ("H2S_delta", -35.0), # TODO constant fractionation 35 per mil ?? + sms_prefix="", sms_suffix="_sms", + processname="production", +) + +function get_edonor_functions(vars, edonor::AbstractString) + + function edonorlim_nolimit(pars, vars, i) + return 1.0 # no edonor limitation on production + end + + function edonorlim_H2S(pars, vars, i) + lim_H2Sfac = lim_MM(vars.H2S_conc[i], pars.k_KH2S[]) + return lim_H2Sfac + end + + if edonor == "H2O" + edonor_ratestoich = deepcopy(default_edonorH2O) + push!(vars, edonor_ratestoich.ratevartemplate) # edonorO2eq + return (edonorlim_nolimit, edonor_ratestoich) + elseif edonor == "H2S" + edonor_ratestoich = deepcopy(default_edonorH2S) + push!(vars, edonor_ratestoich.ratevartemplate) # edonorO2eq + # ScalarData as we only want concentration, not isotopes (if any) + push!(vars, PB.VarDep("H2S_conc", "mol m^-3", "total H2S concentration", attributes=(:field_data=>PB.ScalarData,))) + return (edonorlim_H2S, edonor_ratestoich) + else + error("unknown edonor ", edonor) + end + + return nothing +end + +function get_rate_poptype_functions(rj, grid_vars, vars, poptype::AbstractString) + + # (mol P/m^3/yr, 1/yr) + + function rate_constant(pars, vars, i) + return pars.k_uPO4[] + end + + function rate_nutrient(pars, vars, i) + return pars.k_mu[]*max(vars.P_conc[i], 0.0) + end + + "explicit phytoplankton population" + function rate_pop(pars, vars, i) + # 1/yr max growth rate + rate_pop2 = pars.k_mu[]*vars.phytP_conc[i] # mol P / m^3 / yr + # TODO type inference fails (generating spurious allocations and slow code) + # if function name same as (return?) variable name !? + return rate_pop2 # see above - use a different variable name to work around a Julia 1.6 bug + end + + # mol P yr-1 + function poploss_nopop(pars, vars, i, ProdTotP) + return ProdTotP + end + + # mol P yr-1 + function poploss_const(pars, vars, i, ProdTotP) + # mol P yr-1 mol P * yr-1 + LossTotP = vars.phytP[i]*pars.k_grazeresprate[] + vars.phytP_sms[i] += ProdTotP - LossTotP + return LossTotP + end + + if poptype == "Constant" + return (rate_constant, poploss_nopop) + elseif poptype == "Nutrient" + return (rate_nutrient, poploss_nopop) + elseif poptype == "Pop" + phytP = PB.VarStateExplicit("%reaction%phytP", "mol P", "phytoplankton population", + attributes=(:calc_total=>true,) + ) + phytP_sms = PB.VarDeriv("%reaction%phytP_sms", "mol P yr-1", "phytoplankton population source - sink") + phytP_conc = PB.VarProp("%reaction%phytP_conc", "mol P m-3", "phytoplankton population concentration", + attributes=(:advect=>true, :vertical_movement=>0.0, :specific_light_extinction=>0.0) + ) + pop_vars = [ + phytP, + phytP_sms, + phytP_conc, + PB.VarContrib("(opacity)", "m-1", "total optical opacity"), + ] + PB.add_method_setup_initialvalue_vars_default!(rj, [phytP]) + PB.add_method_do!(rj, do_bio_prod_MM_popconc, (PB.VarList_namedtuple(grid_vars), PB.VarList_namedtuple(pop_vars)) ) + + push!(vars, PB.VarContrib(phytP_sms)) + push!(vars, PB.VarDep(phytP)) + push!(vars, PB.VarDep(phytP_conc)) + + return (rate_pop, poploss_const) + else + error("unknown poptype ", poptype) + end +end + +function get_templim_function(vars, templimtype::AbstractString) + function templim_const(pars, vars, i) + return 1.0 + end + + function templim_eppley(pars, vars, i) + # 1.0 at 0 deg C, Eppley curve + return exp(0.0633*(vars.temp[i] - PB.Constants.k_CtoK)) + end + + if templimtype == "Constant" + return templim_const + elseif templimtype == "Eppley" + push!(vars, PB.VarDep("temp", "K", "temperature")) + return templim_eppley + else + error("unknown templim_type ", templim_type) + end +end + +function get_lightlim_function(vars, lightlim::AbstractString) + "GENIE - like proportional to insolation" + function lightlim_linear(pars, vars, i, muMaxT) + lim_Ifac = pars.k_Irel[]*vars.insol[i]/PB.Constants.k_solar_presentday + return lim_Ifac + end + + "MITgcm - like saturating form" + function lightlim_MM(pars, vars, i, muMaxT) + zInsol = pars.k_Irel[]*vars.insol[i] # PAR at depth z including wc attenuation + lim_Ifac = zInsol/(pars.k_Ic[] + zInsol) + return lim_Ifac + end + + "specified value" + function lightlim_fixed(pars, vars, i, muMaxT) + lim_Ifac = pars.k_Irel[] + return lim_Ifac + end + + """ + Saturating light limitation of rate vs (local) irradiance, + derived from photosynthetic QE and chl absorption cross section. + See eg Geider (1987) New Phytol. for summary of notation and unit conversions. + phiQE = 0.09; // mol C (mol photon PAR)-1 + EINSTEINPERWM2 = 5.0; // conversion factor umol photons m-2 s-1 per W PAR m-2 + gC (g Chla)-1 (Wm-2)-1 d-1 + alphaQE = 1.62e-5*EINSTEINPERWM2*SECPERDAY*(phiQE/0.09) = 7.0 gC (g Chla)-1 (Wm-2)-1 d-1 + + thetaChlC * par + check value: alphaQE*0.03 * 4.8 [*(epsilonChl/0.015) ?] = 1.0 d-1 + """ + function lightlim_QE(pars, vars, i, muMaxT) + par = pars.k_Irel[]*vars.insol[i] # W/m^2 PAR at depth z + # mgC/mgChl/Wpar m^-2/d d / yr * mgChl/mgC * Wpar m^2 / (1/yr) + QEfac_lightlim = pars.k_alphaQE[]*PB.Constants.k_daypyr*pars.k_thetaChlC[]*par/muMaxT + lim_Ifac = (1 - exp(-QEfac_lightlim)) + return lim_Ifac + end + + var_insol = PB.VarDep("insol", "W m-2", "photosynthetic radiative flux") + if lightlim == "linear" + push!(vars, var_insol) + return lightlim_linear + elseif lightlim == "MM" + push!(vars, var_insol) + return lightlim_MM + elseif lightlim == "fixed" + return lightlim_fixed + elseif lightlim == "QE" + push!(vars, var_insol) + return lightlim_QE + else + error("unknown lightlim $lightlim") + end + +end + + + +function do_bio_prod_MM_popconc( + m::PB.ReactionMethod, + pars, + (grid_vars, pop_vars), + cellrange::PB.AbstractCellRange, + deltat +) + + @inbounds for i in cellrange.indices + pop_vars.phytP_conc[i] = pop_vars.phytP[i]/grid_vars.volume[i] + # mg Chl m-3 mg Chl / mgC mol C / mol P mol P m-3 mg C / mol C + phytmgchlm3 = pars.k_thetaChlC[]*pars.rCorgPO4[]*pop_vars.phytP_conc[i]*12000 + # m-1 m^2/mg Chl mg Chl / m^3 + PB.add_if_available(pop_vars.opacity, i, pars.k_epsilonChl[] * phytmgchlm3) + end + + return nothing +end + +end diff --git a/src/ocean/Ocean.jl b/src/ocean/Ocean.jl index 1d9983a..5f9bde6 100644 --- a/src/ocean/Ocean.jl +++ b/src/ocean/Ocean.jl @@ -6,4 +6,15 @@ include("OceanTransportMatrix.jl") include("OceanNoTransport.jl") +include("OceanTransport3box.jl") + +include("OceanTransport6box.jl") + +include("OceanTransportTMM.jl") + +include("BioProd.jl") + +include("VerticalTransport.jl") + + end \ No newline at end of file diff --git a/src/ocean/OceanTransport3box.jl b/src/ocean/OceanTransport3box.jl new file mode 100644 index 0000000..3e3316b --- /dev/null +++ b/src/ocean/OceanTransport3box.jl @@ -0,0 +1,244 @@ +module OceanTransport3box + +import SparseArrays +import PALEOboxes as PB +using PALEOboxes.DocStrings + +import PALEOocean + +# import Infiltrator # Julia debugger + +""" + ReactionOceanTransport3box + +3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model. + + --------------------------------------- + | 1 (s) | 2(h) | + | -->--->- | + |-------------------|----| | | + | | | | /|\\ | + | | |__|______|___| + | | | | | + | 3 (d) -<--<-- \\|/ | + | circT fhd | + | | + ---------------------------------------- + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_SETUP) +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionOceanTransport3box{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParDouble("circT", 20*1e6, units="m^3 s^-1", + description="overturning circulation"), + PB.ParDouble("circfhd", 60*1e6, units="m^3 s^-1", + description="high latitude <-> deep exchange rate"), + PB.ParDoubleVec("temp", [21.5, 2.5, 2.5], units="degrees C", + description="ocean temperature"), + PB.ParBool("temp_trackglobal", false, + description="track global temperature (apply offset of global temp -15C"), + ) + + "Ocean circulation (defined as transport matrix) + NB: tracers are column vectors, multiplied by transport + + Transport matrix: [1-> 2->1 3->1 + 1->2 2-> 3->2 + 1->3 2->3 3-> ] + For internal use: Units: m^3 s^-1" + trspt_circ::Matrix{Float64} = Matrix{Float64}(undef, 0, 0) + "Transport matrix: Units yr^{-1} + so dc/dt = trspt_dtm * c [yr^-1] + where c is column vector of tracer concentrations" + trspt_dtm::Matrix{Float64} = Matrix{Float64}(undef, 0, 0) + + # calculate sparse transpose to provide a test case (not an optimisation we need, given this is a 3x3 matrix!) + trspt_dtm_tr::SparseArrays.SparseMatrixCSC{Float64, Int64} = SparseArrays.spzeros(0, 0) +end + + +function PB.set_model_geometry(rj::ReactionOceanTransport3box, model::PB.Model) + + ocean_cells = 3 # Number of cells (= ocean Domain size) + + # Define some named cells for plotting (only) + oceancellnames=[:s, :h, :d] + + isurf=[1,2] + ifloor=[1, 2, 3] + + # set minimal grid (just names for plotting) + oceangrid = PB.Grids.UnstructuredVectorGrid(ncells=ocean_cells, + cellnames=Dict(oceancellnames[i]=>i for i in 1:length(oceancellnames))) + + PB.Grids.set_subdomain!(oceangrid, "oceansurface", PB.Grids.BoundarySubdomain(isurf), true) + @info " set ocean.oceansurface Subdomain size=$(length(isurf))" + + PB.Grids.set_subdomain!(oceangrid, "oceanfloor", PB.Grids.BoundarySubdomain(ifloor), true) + @info " set ocean.oceanfloor Subdomain size=$(length(ifloor))" + + surfacedom_cellnames = Dict(oceancellnames[isurf[i]]=>i for i in eachindex(isurf)) + surfacegrid = PB.Grids.UnstructuredVectorGrid(ncells=length(isurf), + cellnames=surfacedom_cellnames) + PB.Grids.set_subdomain!(surfacegrid, "ocean", PB.Grids.InteriorSubdomain(ocean_cells, isurf), true) + + floordom_cellnames = Dict(oceancellnames[ifloor[i]]=>i for i in eachindex(ifloor)) + floorgrid = PB.Grids.UnstructuredVectorGrid(ncells=length(ifloor), + cellnames=floordom_cellnames) + PB.Grids.set_subdomain!(floorgrid, "ocean", PB.Grids.InteriorSubdomain(ocean_cells, ifloor), true) + + PALEOocean.Ocean.set_model_domains(model, oceangrid, surfacegrid, floorgrid) + + return nothing +end + + + +function PB.register_methods!(rj::ReactionOceanTransport3box) + + physvars = [ + PB.VarProp("sal", "psu", "Ocean salinity"), + PB.VarProp("rho", "kg m^-3", "physical ocean density"), + # no length check as create and set oceansurface Variable from atm Domain + PB.VarProp("oceansurface.open_area_fraction","", "fraction of area open to atmosphere", attributes=(:check_length=>false,)), + ] + + PB.add_method_setup!( + rj, + do_setup_grid, + ( + PB.VarList_namedtuple(PALEOocean.Ocean.grid_vars_all), + PB.VarList_namedtuple(physvars), + ), + ) + + tempvars = [ + PB.VarDepScalar("(global.TEMP)", "K", "global mean temperature"), + PB.VarProp("temp", "Kelvin", "Ocean temperature"), + ] + + PB.add_method_do!( + rj, + do_temperature, + (PB.VarList_namedtuple(tempvars), ), + ) + + return nothing +end + + +function do_setup_grid(m::PB.ReactionMethod, pars, (grid_vars, physvars), cellrange::PB.AbstractCellRange, attribute_name) + attribute_name == :setup || return + + rj = m.reaction + + Atot = 3.6e14 # m^2 total ocean area + grid_vars.Abox .= [0.85, 0.15, 1.0 ]*Atot + + grid_vars.Asurf .= grid_vars.Abox[rj.domain.grid.subdomains["oceansurface"].indices] + + # Define Afloor for all boxes so burial can still be defined from all boxes + # (in reality, surely small but non-zero for 'surface' boxes) + grid_vars.Afloor .= [0.0, 0.0, grid_vars.Abox[3]] + grid_vars.Afloor_total[] = sum(grid_vars.Afloor) + # constant density + grid_vars.rho_ref .= 1027 + + masstot = 1.3697e21 # COPSE 5_14 value of ocean mass + grid_vars.volume_total[] = 1.3697e21/grid_vars.rho_ref[1] # Ocean volume m^3 - specified to keep COPSE 5_14 value of ocean mass + grid_vars.volume[1:2] .= [100*grid_vars.Asurf[1], 250*grid_vars.Asurf[2]] # Volume of ocean boxes, m^3 + grid_vars.volume[3] = grid_vars.volume_total[] - sum(grid_vars.volume[1:2]) + + grid_vars.zupper .= -[0 , 0 , 100] + grid_vars.zlower .= grid_vars.zupper-grid_vars.volume./grid_vars.Abox + grid_vars.zmid .= 0.5.*(grid_vars.zupper .+ grid_vars.zlower) + grid_vars.zfloor .= grid_vars.zlower + + grid_vars.pressure .= -grid_vars.zmid # pressure(dbar) ~ depth (m) + + # set salinity + physvars.sal .= 35.0 + physvars.rho .= 1027 + physvars.open_area_fraction .= 1.0 + + # transport matrix m^3 s-1 + rj.trspt_circ = [ + -pars.circT[] 0 pars.circT[] + pars.circT[] -(pars.circT[]+pars.circfhd[]) pars.circfhd[] + 0 pars.circT[]+pars.circfhd[] -(pars.circT[]+pars.circfhd[]) + ] + + # convert to yr^{-1} + rj.trspt_dtm = PB.Constants.k_secpyr*rj.trspt_circ./repeat(grid_vars.volume, 1, length(grid_vars.volume)) + + # calculate sparse transpose to provide a test case (not an optimisation, given this is a 3x3 matrix!) + rj.trspt_dtm_tr = SparseArrays.sparse(transpose(rj.trspt_dtm)) + + return nothing +end + + +function do_temperature(m::PB.ReactionMethod, pars, (vars, ), cellrange::PB.AbstractCellRange, deltat) + + # Set temperature + if pars.temp_trackglobal[] + vars.temp .= pars.temp .- 15.0 .+ vars.TEMP[] # temperature (K) + else + vars.temp .= pars.temp .+ PB.Constants.k_CtoK # temperature (K) + end + + return nothing +end + +function PB.register_dynamic_methods!(rj::ReactionOceanTransport3box) + + (transport_conc_vars, transport_sms_vars, transport_input_vars) = + PALEOocean.Ocean.find_transport_vars(rj.domain, add_transport_input_vars=true) + + PB.add_method_do!( + rj, + do_transport, + ( + PB.VarList_namedtuple(PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean)), + PB.VarList_components(transport_conc_vars), + PB.VarList_components(transport_sms_vars), + PB.VarList_components(transport_input_vars), + ), + preparefn=PALEOocean.Ocean.prepare_transport + ) + + return nothing +end + + +function do_transport( + m::PB.ReactionMethod, + (grid_vars, transport_conc_components, transport_sms_components, transport_input_components, buffer), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + + PALEOocean.Ocean.do_transport( + grid_vars, transport_conc_components, transport_sms_components, transport_input_components, buffer, + rj.trspt_dtm, + cellrange + ) + + # PALEOocean.Ocean.do_transport_tr( + # grid_vars, transport_conc_components, transport_sms_components, transport_input_components, buffer, + # rj.trspt_dtm_tr, + # cellrange + # ) + return nothing +end + + +end diff --git a/src/ocean/OceanTransport6box.jl b/src/ocean/OceanTransport6box.jl new file mode 100644 index 0000000..c8aac2b --- /dev/null +++ b/src/ocean/OceanTransport6box.jl @@ -0,0 +1,289 @@ +module OceanTransport6box + +import SparseArrays +import PALEOboxes as PB +using PALEOboxes.DocStrings +import PALEOocean + +# import Infiltrator # Julia debugger + +""" + ReactionOceanTransport6box + +4+2-box ocean model from [Daines2016](@cite) +This is based on: +- the four-box model of [Hotinski2000](@cite) +- the five-box model of [Watson1995](@cite) +- the upwelling region representation of [Canfield2006](@cite) + +There are two main ingredients here: +- An open-ocean 'intermediate/thermocline' box(i) from [Hotinski2000](@cite), coupled to the low-latitude surface box via an Ekman pumping term and to high latitude box (h) via an overturning circulation term. This is a more realistic representation of the open ocean than the 3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) model. +- A two-box shelf/slope (boxes r, rc), which can be configured as + - an upwelling region (k_slopetype='OMZ'), cf Canfield (2006) with upwelling from the intermediate/thermocline box + - a low-latitude shelf (k_slopetype='shelf') with exchange terms to low-latitude surface (s) and thermocline (i) boxes + + See Oxygen oases\\Box Model 20150922\\HotinskiCalc5Box.m, HotinskiConstants5Box.m, HotinskiCalcCirc6.m + + -------------------------------------------------- + | 5(r) | 1 (s) | 2(h) | + | | | | + |-----------|------------------------| | + | 6(rc) | | | + | | 3 (i) |_____________| + -----------| | | + | |------------------------ | + | 4(d) | + | | + ----------------------------------------------- + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_SETUP) +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionOceanTransport6box{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParString("slopetype", "OMZ", allowed_values=["OMZ", "shelf"], + description="type of shelf circulation (boxes r, rc)"), + + PB.ParDouble("circT", 19.3e6, units="m^3 s^-1", + description="overturning circulation (exchange high lat (h) to deep (d) and thermocline (i)"), + PB.ParDouble("circfhd", 48.7e6, units="m^3 s^-1", + description="high latitude <-> deep exchange rate"), + PB.ParDouble("circR", 20e6, units="m^3 s^-1", + description="upwelling thermocline (i) to slope (rc) to upwell (r) to low lat surface (s)"), + PB.ParDouble("totalEkman",60e6, units="m^3 s^-1", + description="total wind-driven Ekman pumping"), + PB.ParDouble("circS", 1e6, units="m^3 s^-1", + description="upwell(r) <-> low lat surf box (s) exchange"), + + PB.ParDoubleVec("temp", [21.5, 2.5, 2.5, 2.5, 21.5, 2.5], units="degrees C", + description="ocean temperature"), + + PB.ParBool("temp_trackglobal", false, + description="track global temperature (apply offset of global temp -15C"), + ) + + + "Ocean circulation (defined as transport matrix) + NB: tracers are column vectors, multiplied by transport + + Transport matrix: [1-> 2->1 3->1, 4->1, 5->1, 6-> 1 + 1->2 2-> 3->2 ... + 1->3 2->3 3-> ... + ... + 1->6 ... 6-> ] + For internal use: Units: m^3 s^-1" + trspt_circ::Matrix{Float64} = Matrix{Float64}(undef, 0, 0) + "Transport matrix: Units yr^{-1} + so dc/dt = trspt_dtm * c [yr^-1] + where c is column vector of tracer concentrations" + trspt_dtm::Matrix{Float64} = Matrix{Float64}(undef, 0, 0) + + # calculate sparse transpose to provide a test case (not an optimisation, given this is a small matrix!) + trspt_dtm_tr::SparseArrays.SparseMatrixCSC{Float64, Int64} = SparseArrays.spzeros(0, 0) +end + + +function PB.set_model_geometry(rj::ReactionOceanTransport6box, model::PB.Model) + + ocean_cells = 6 # Number of cells (= ocean Domain size) + + # Define some named cells for plotting (only) + oceancellnames=[:s, :h, :i, :d, :r, :rc] + + isurf=[1,2, 5] + ifloor=[1, 2, 3, 4, 5, 6] + + # set minimal grid (just names for plotting) + oceangrid = PB.Grids.UnstructuredVectorGrid(ncells=ocean_cells, + cellnames=Dict(oceancellnames[i]=>i for i in 1:length(oceancellnames))) + + PB.Grids.set_subdomain!(oceangrid, "oceansurface", PB.Grids.BoundarySubdomain(isurf), true) + @info " set ocean.oceansurface Subdomain size=$(length(isurf))" + + PB.Grids.set_subdomain!(oceangrid, "oceanfloor", PB.Grids.BoundarySubdomain(ifloor), true) + @info " set ocean.oceanfloor Subdomain size=$(length(ifloor))" + + surfacedom_cellnames = Dict(oceancellnames[isurf[i]]=>i for i in eachindex(isurf)) + surfacegrid = PB.Grids.UnstructuredVectorGrid(ncells=length(isurf), + cellnames=surfacedom_cellnames) + PB.Grids.set_subdomain!(surfacegrid, "ocean", PB.Grids.InteriorSubdomain(ocean_cells, isurf), true) + + floordom_cellnames = Dict(oceancellnames[ifloor[i]]=>i for i in eachindex(ifloor)) + floorgrid = PB.Grids.UnstructuredVectorGrid(ncells=length(ifloor), + cellnames=floordom_cellnames) + PB.Grids.set_subdomain!(floorgrid, "ocean", PB.Grids.InteriorSubdomain(ocean_cells, ifloor), true) + + PALEOocean.Ocean.set_model_domains(model, oceangrid, surfacegrid, floorgrid) + + return nothing +end + +function PB.register_methods!(rj::ReactionOceanTransport6box) + + physvars = [ + PB.VarProp("sal", "psu", "Ocean salinity"), + PB.VarProp("rho", "kg m^-3", "physical ocean density"), + # no length check as create and set oceansurface Variable from atm Domain + PB.VarProp("oceansurface.open_area_fraction","", "fraction of area open to atmosphere", attributes=(:check_length=>false,)), + ] + + PB.add_method_setup!( + rj, + do_setup_grid, + ( PB.VarList_namedtuple(PALEOocean.Ocean.grid_vars_all), + PB.VarList_namedtuple(physvars), + ), + ) + + tempvars = [ + PB.VarDepScalar("(global.TEMP)", "K", "global mean temperature"), + PB.VarProp("temp", "Kelvin", "Ocean temperature"), + ] + + PB.add_method_do!( + rj, + do_temperature, + (PB.VarList_namedtuple(tempvars), ), + ) + + return nothing +end + + +function do_setup_grid(m::PB.ReactionMethod, pars, (grid_vars, physvars), cellrange::PB.AbstractCellRange, attribute_name) + attribute_name == :setup || return + + rj = m.reaction + + Atot = 3.6e14 # m^2 total ocean area + # :s :h :i :d :r :rc + grid_vars.Abox .= [0.80, 0.15, 0.80, 1.0, 0.05, 0.05 ]*Atot + + grid_vars.Asurf .= grid_vars.Abox[rj.domain.grid.subdomains["oceansurface"].indices] + + # Define Afloor for all boxes so burial can still be defined from all boxes + # (in reality, surely small but non-zero for 'surface' boxes) + grid_vars.Afloor .= [0.0, 0.0, 0.0, grid_vars.Abox[4], 0.0, 0.0] + grid_vars.Afloor_total[] = sum(grid_vars.Afloor) + + # constant density + grid_vars.rho_ref .= 1027 + + + masstot = 1.3697e21 # COPSE 5_14 value of ocean mass + grid_vars.volume_total[] = 1.3697e21/grid_vars.rho_ref[1] # Ocean volume m^3 - specified to keep COPSE 5_14 value of ocean mass + + if pars.slopetype[] == "shelf" + # TODO this was not used in Daines & Lenton (2016) + error("slopetype 'shelf' not implemented") + elseif pars.slopetype[] == "OMZ" + # OMZ upwelling region, cf [Canfield2006](@cite) + + # :s :h :i :d :r :rc + grid_vars.zupper .= -[0, 0, 100, 900, 0, 100] + grid_vars.zlower .= -[100, 250, 1000, 900, 100, 1000] # box 4 :d filled in later + + grid_vars.volume[4] = 0.0 + grid_vars.volume .= (grid_vars.zupper .- grid_vars.zlower).*grid_vars.Abox # box 4 temporarily zero + grid_vars.volume[4] = grid_vars.volume_total[] - sum(grid_vars.volume) # update volume of box 4 to get correct total + grid_vars.zlower[4] = grid_vars.zupper[4] - grid_vars.volume[4]/grid_vars.Abox[4] # define depth of box 4 to get correct volume + + grid_vars.zmid .= 0.5.*(grid_vars.zupper .+ grid_vars.zlower) + + grid_vars.zfloor .= grid_vars.zlower + + circMR = 0.0 + circTR = pars.circR[] + else + error("unknown slopetype ", pars.slopetype[]) + end + + grid_vars.pressure .= -grid_vars.zmid # pressure(dbar) ~ depth (m) + + # set salinity + physvars.sal .= 35.0 + physvars.rho .= 1027 + physvars.open_area_fraction .= 1.0 + + # calculate transport matrix (m^3 s-1) + + # low lat Ekman = total Ekman pumping - margin upwelling + circU = pars.totalEkman[] - pars.circR[] + # define short names for convenience + T, U, S, R, Fhd, TR, MR = pars.circT[], circU, rj.pars.circS[], pars.circR[], pars.circfhd[], circTR, circMR + + rj.trspt_circ = [ + -(U+S+R) 0 U 0 S+R 0 + 0 -(2*T+Fhd) T Fhd+T 0 0 + U+R T -(U+T+R+TR) 0 0 TR + 0 Fhd+T 0 -(Fhd+T) 0 0 + S 0 0 0 -(S+R+MR) R+MR + 0 0 R+TR 0 MR -(R+MR+TR) + ] + + # convert to yr^{-1} + rj.trspt_dtm = PB.Constants.k_secpyr*rj.trspt_circ./repeat(grid_vars.volume, 1, length(grid_vars.volume)) + + # calculate sparse transpose to provide a test case (not an optimisation, given this is a 3x3 matrix!) + rj.trspt_dtm_tr = SparseArrays.sparse(transpose(rj.trspt_dtm)) + + return nothing +end + + +function do_temperature(m::PB.ReactionMethod, pars, (vars, ), cellrange::PB.AbstractCellRange, deltat) + + # Set temperature + if pars.temp_trackglobal[] + vars.temp .= pars.temp .- 15.0 .+ vars.TEMP[] # temperature (K) + else + vars.temp .= pars.temp .+ PB.Constants.k_CtoK # temperature (K) + end + + return nothing +end + +function PB.register_dynamic_methods!(rj::ReactionOceanTransport6box) + + (transport_conc_vars, transport_sms_vars, transport_input_vars) = + PALEOocean.Ocean.find_transport_vars(rj.domain, add_transport_input_vars=true) + + PB.add_method_do!( + rj, + do_transport, + ( + PB.VarList_namedtuple(PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean)), + PB.VarList_components(transport_conc_vars), + PB.VarList_components(transport_sms_vars), + PB.VarList_components(transport_input_vars), + ), + preparefn=PALEOocean.Ocean.prepare_transport + ) + + return nothing +end + +function do_transport( + m::PB.ReactionMethod, + (grid_vars, transport_conc_components, transport_sms_components, transport_input_components, buffer), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + + PALEOocean.Ocean.do_transport_tr( + grid_vars, transport_conc_components, transport_sms_components, transport_input_components, buffer, + rj.trspt_dtm_tr, + cellrange) + + return nothing +end + + +end diff --git a/src/ocean/OceanTransportTMM.jl b/src/ocean/OceanTransportTMM.jl new file mode 100644 index 0000000..4967b84 --- /dev/null +++ b/src/ocean/OceanTransportTMM.jl @@ -0,0 +1,612 @@ +module OceanTransportTMM + +import SparseArrays +import MAT # Matlab file access +import LinearAlgebra +using Printf + +import SIMD +import Preferences + +# import Infiltrator # Julia debugger + +import PALEOboxes as PB +using PALEOboxes.DocStrings +import PALEOocean + + +""" + ReactionOceanTransportTMM + +GCM ocean transport implementation using transport matrices in format defined by [Khatiwala2007](@cite) +Requires download of Samar Khatiwala's TMM files (for MITgcm, UVic models) as described in + where TM files are from + + +NB: `length(operatorID)` must be 2, to define `operatorID[1]` for explicit and `operatorID[2]` for implicit matrices. + +The `base_path` parameter sets the top level of the folder structure for the downloaded matrices. + +Code based on `TMM/tmm/models/petsc3.4/mitgchem/matlab/make_input_files_for_migchem_dic_biotic_model.m` + +# Parameters +$(PARS) + +# Methods and Variables +$(METHODS_SETUP) +""" +Base.@kwdef mutable struct ReactionOceanTransportTMM{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParString("base_path", "\$TMMDir\$/MITgcm_2.8deg", + description="directory containing transport matrices"), + + PB.ParBool("sal_norm", false, + description="apply salinity normalisation to transport matrix"), + + PB.ParBool("use_annualmean", false, + description="true to read annual mean matrix"), + + PB.ParInt("num_seasonal", 12, + description="number of seasonal matrices"), + + PB.ParInt("Aimp_deltat", 3600*24, units="seconds", + description="timestep to derive upscaling factor for implicit transport matrix"), + + PB.ParBool("kji_order", true, + description="true to sort indices into k,j,i order to optimise memory layout"), + + PB.ParInt("pack_chunk_width", 4, allowed_values=[0, 2, 4, 8, 16], + description="non-zero to enable SIMD packed transport matrix multiply"), + + PB.ParInt("TMfpsize", 64, units="bits", allowed_values=[32, 64], + description="FP size for transport matrix"), + ) + + index_perm = nothing # optional permutation to apply to TM indices to optimise memory layout + index_perm_inverse = nothing + + grid_ocean = nothing + grid_oceansurface = nothing + grid_oceanfloor = nothing + + + "Transpose of transport matrices: Units yr^{-1} + so dc/dt = c * trspt_dtm_tr [yr^-1] + where c is row vector of tracer concentrations + + Multiple matrices are stored in Julia CSC format with a common sparsity pattern" + trspt_dAexp_tr = nothing + trspt_dAimp_tr = nothing + Aimp_mult::Int = -1 # upscaling factor Aimp + TMeltype = nothing # set from par_TMfpsize + pack_datatype = nothing # set from par_pack_chunk_width + + matrix_times::Vector{Float64} = Vector{Float64}() # model times for transport matrices + "time interpolator for transport matrices" + matrix_tinterp = nothing + + config_data = nothing # raw config_data from .mat file + grid = nothing + boxes = nothing + + matrix_name::String = "" + matrix_path::String = "" + matrix_timestep::Float64 = NaN + matrix_start_time::Float64 = NaN + matrix_delta_time::Float64 = NaN + matrix_cycle_time::Float64 = NaN + +end + +# Provide a custom create_reaction implementation so we can set default operatorID +function PB.create_reaction(::Type{ReactionOceanTransportTMM}, base::PB.ReactionBase) + rj = ReactionOceanTransportTMM(base=base) + rj.base.operatorID = [1, 2] + return rj +end + +function PB.set_model_geometry(rj::ReactionOceanTransportTMM, model::PB.Model) + + config_data_path = joinpath(rj.pars.base_path[], "config_data.mat") + PB.setfrozen!(rj.pars.base_path) + + @info "set_model_geometry $(PB.fullname(rj)) reading config_data from file $(config_data_path)" + rj.config_data = MAT.matread(config_data_path) + + _read_grids(rj) + + PALEOocean.Ocean.set_model_domains(model, rj.grid_ocean, rj.grid_oceansurface, rj.grid_oceanfloor) + + return nothing +end + +function _read_grids(rj::ReactionOceanTransportTMM) + + grid_path = joinpath(rj.pars.base_path[], "grid.mat") + @info "$(PB.fullname(rj)) reading grid from file $(grid_path)" + rj.grid = MAT.matread(grid_path) + + # NB: we define grid z coords to be -ve + zmid = -rj.grid["z"][:,1] + zupper = -rj.grid["z"][:,1] .+ rj.grid["dznom"][:,1]./2 + zlower = -rj.grid["z"][:,1] .- rj.grid["dznom"][:,1]./2 + zedges = vcat(zupper, zlower[end]) + + lat = rj.grid["y"][:,1] # mid-point of each bin ? + latedges = Vector(undef, length(lat)+1) + latedges[1:end-1] = rj.grid["y"][:,1] - rj.grid["dth"][1,:]/2 + latedges[end] = rj.grid["y"][end,1] + rj.grid["dth"][1, end]/2 + + lon = rj.grid["x"][:,1] # mid-point of each bin ? + lonedges = Vector(undef, length(lon)+1) + lonedges[1:end-1] = rj.grid["x"][:,1] - rj.grid["dphi"][:,1]/2 + lonedges[end] = rj.grid["x"][end,1] + rj.grid["dphi"][end,1]/2 + + rj.grid_ocean = PB.Grids.CartesianGrid( + PB.Grids.CartesianLinearGrid, + ["lon", "lat", "zt"], [length(lon), length(lat), length(zmid)], [lon, lat, zmid], [lonedges, latedges, zedges]; + zdim=3, + zidxsurface=1, + ztoheight=1.0 + ) + + rj.grid_oceansurface = PB.Grids.CartesianGrid( + PB.Grids.CartesianLinearGrid, + ["lon", "lat"], [length(lon), length(lat)], [lon, lat], [lonedges, latedges] + ) + + rj.grid_oceanfloor = deepcopy(rj.grid_oceansurface) + + # read linear cell <-> grid mapping (transport matrix uses linear cell index) + boxes_path = joinpath(rj.pars.base_path[], rj.config_data["matrixPath"], "Data", "boxes.mat") + @info "$(PB.fullname(rj)) reading linear cell index <-> 3D grid mapping from file $(boxes_path)" + rj.boxes = MAT.matread(boxes_path) + + v_i = Int.(rj.boxes["ixBox"][:, 1]) + v_j = Int.(rj.boxes["iyBox"][:, 1]) + v_k = Int.(rj.boxes["izBox"][:, 1]) + + if rj.pars.kji_order[] + @info " reordering transport matrix indices into k, j, i order" + (rj.index_perm, rj.index_perm_inverse) = PB.Grids.linear_kji_order(rj.grid_ocean, v_i, v_j, v_k) + v_i = v_i[rj.index_perm]; v_j = v_j[rj.index_perm]; v_k = v_k[rj.index_perm] + end + + PB.Grids.set_linear_index(rj.grid_ocean, v_i, v_j, v_k) + @info " set ocean linear <--> cartesian mapping for $(rj.grid_ocean.ncells) cells" + + lsurf = findall(x->x==rj.grid_ocean.zidxsurface, v_k) + PB.Grids.set_linear_index(rj.grid_oceansurface, v_i[lsurf], v_j[lsurf]) + PB.Grids.set_linear_index(rj.grid_oceanfloor, v_i[lsurf], v_j[lsurf]) + @info " set surface, floor linear <--> cartesian mapping for $(rj.grid_oceansurface.ncells) cells" + + # set subdomain mappings + # println("lsurf ", typeof(lsurf), " =", lsurf) + # println("v_k ", typeof(v_k), " =", v_k) + PB.Grids.set_subdomain!(rj.grid_ocean, "oceansurface", PB.Grids.BoundarySubdomain(lsurf), true) + @info " set ocean.oceansurface Subdomain size=$(length(lsurf))" + # find ifloor + lfloor = similar(lsurf) + for l in eachindex(lfloor) + fcart = rj.grid_oceanfloor.cartesian_index[l] + i,j = fcart[1], fcart[2] + for k in reverse(1:size(rj.grid_ocean.linear_index, 3)) + if !ismissing(rj.grid_ocean.linear_index[i,j,k]) + lfloor[l] = rj.grid_ocean.linear_index[i,j,k] + break + end + end + end + PB.Grids.set_subdomain!(rj.grid_ocean, "oceanfloor", PB.Grids.BoundarySubdomain(lfloor), true) + @info " set ocean.oceanfloor Subdomain size=$(length(lfloor))" + + locean = Vector{Union{Missing, Int}}(undef, rj.grid_ocean.ncells) + fill!(locean, missing) + locean[lfloor] .= 1:length(lfloor) + PB.Grids.set_subdomain!(rj.grid_oceanfloor, "ocean", PB.Grids.InteriorSubdomain(locean), true) + @info " set oceanfloor.ocean Subdomain size=$(length(locean))" + + return nothing +end + +function PB.register_methods!(rj::ReactionOceanTransportTMM) + + PB.add_method_setup!( + rj, + setup_grid_TMM, + (PB.VarList_namedtuple(PALEOocean.Ocean.grid_vars_all),) + ) + + return nothing +end + +function setup_grid_TMM( + m::PB.ReactionMethod, + (grid_vars, ), + cellrange::PB.AbstractCellRange, + attribute_name +) + attribute_name == :setup || return + + rj = m.reaction + + z_coord = rj.grid_ocean.coords[3] + z_coord_edges = rj.grid_ocean.coords_edges[3] + + da = rj.grid["da"] + dv = rj.grid["dv"] + for l in 1:rj.grid_ocean.ncells + cartidx = rj.grid_ocean.cartesian_index[l] + kidx = cartidx[3] + + grid_vars.Abox[l] = da[cartidx] + grid_vars.zupper[l] = z_coord_edges[kidx] + grid_vars.zmid[l] = z_coord[kidx] + grid_vars.zlower[l] = z_coord_edges[kidx+1] + + grid_vars.volume[l] = dv[cartidx] + end + grid_vars.volume_total[] = sum(grid_vars.volume) + + grid_vars.Asurf .= grid_vars.Abox[rj.grid_ocean.subdomains["oceansurface"].indices] + grid_vars.Afloor .= grid_vars.Abox[rj.grid_ocean.subdomains["oceanfloor"].indices] + grid_vars.Afloor_total[] = sum(grid_vars.Afloor) + grid_vars.zfloor .= grid_vars.zlower[rj.grid_ocean.subdomains["oceanfloor"].indices] + + grid_vars.rho_ref .= 1027 + grid_vars.pressure .= -grid_vars.zmid # pressure(dbar) ~ depth (m) + + return nothing +end + + + +function PB.register_dynamic_methods!(rj::ReactionOceanTransportTMM) + + @info "register_dynamic_methods! $(nameof(typeof(rj))) $(PB.fullname(rj))" + length(rj.operatorID) == 2 || error(" configuration error: length(operatorID) != 2") + + if rj.pars.TMfpsize[] == 32 + rj.TMeltype = Float32 + elseif rj.pars.TMfpsize[] == 64 + rj.TMeltype = Float64 + else + error("unknown TMfpsize $(rj.pars.TMfpsize[])") + end + + @info " setting TMeltype $(rj.TMeltype)" + + vars = [ + PB.VarDepScalar("global.tforce", "yr", "historical time at which to apply forcings, present = 0 yr"), + ] + + (transport_conc_vars, transport_sms_vars, _, num_components) = + PALEOocean.Ocean.find_transport_vars( + rj.domain, + add_transport_input_vars=false, + ) + + if rj.pars.pack_chunk_width[] == 0 + @info " using unpacked transport" + rj.TMeltype === Float64 || error(" only Float64 supported") + # Aexp operatorID[1] + PB.add_method_do!( + rj, + do_transport_TMM, + ( # need to keep to 4 arguments required by prepare_transport + PB.VarList_namedtuple([PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean); vars]), + PB.VarList_components(transport_conc_vars), + PB.VarList_components(transport_sms_vars), + PB.VarList_nothing(), # not using input_vars + ), + preparefn=prepare_do_transport_TMM, # read matrices, add buffer + operatorID=[rj.operatorID[1]], + p=:trspt_dAexp_tr # field name in rj to use + ) + # Aimp operatorID[2] + PB.add_method_do!( + rj, + do_transport_TMM, + ( # need to keep to 4 arguments required by prepare_transport + PB.VarList_namedtuple([PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean); vars]), + PB.VarList_components(transport_conc_vars), + PB.VarList_components(transport_sms_vars), + PB.VarList_nothing(), # not using input_vars + ), + preparefn=PALEOocean.Ocean.prepare_transport, # add buffer + operatorID=[rj.operatorID[2]], + p=:trspt_dAimp_tr # field name in rj to use + ) + else + + packed_buffer = PALEOocean.Ocean.PackedBuffer( + num_components, + rj.grid_ocean.ncells, + rj.pars.pack_chunk_width[], + rj.TMeltype + ) + @info " packed transport for $num_components concentration components "* + "with packed_buffer: $(typeof(packed_buffer)) "* + "pack_eltype $(PALEOocean.Ocean.pack_eltype(packed_buffer)) "* + "pack_datatype $(PALEOocean.Ocean.pack_datatype(packed_buffer)) "* + "size packed_conc_array $(size(packed_buffer.packed_conc_array))" + + # dummy Variable to create a dependency to guarantee pack_conc is called before transport + sequencer_var = PB.VarPropScalar("%reaction%packed_transport_sequencer", "", "dummy Variable to sequence packed transport") + # pack concentration Variables + PB.add_method_do!( + rj, + do_transport_TMM_pack_conc, + ( + PB.VarList_components(transport_conc_vars), + PB.VarList_single(sequencer_var), + ), + preparefn=prepare_do_transport_TMM_packed, # read matrices + p=packed_buffer, + ) + # Aexp operatorID[1] + PB.add_method_do!( + rj, + do_transport_TMM_packed, + ( + PB.VarList_namedtuple([PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean); vars]), + PB.VarList_components(transport_sms_vars), + PB.VarList_single(PB.VarDep(sequencer_var)), + ), + operatorID=[rj.operatorID[1]], + p=(:trspt_dAexp_tr, packed_buffer), + ) + # Aimp operatorID[2] + PB.add_method_do!( + rj, + do_transport_TMM_packed, + ( + PB.VarList_namedtuple([PB.VarDep.(PALEOocean.Ocean.grid_vars_ocean); vars]), + PB.VarList_components(transport_sms_vars), + PB.VarList_single(PB.VarDep(sequencer_var)), + ), + operatorID=[rj.operatorID[2]], + p=(:trspt_dAimp_tr, packed_buffer), + ) + + end + + return nothing +end + + +function prepare_do_transport_TMM(m::PB.ReactionMethod, vardata) + rj = m.reaction + + # read matrix data in prepare so it is deferred until initialize + _read_matrix_data(rj) + + return PALEOocean.Ocean.prepare_transport(m, vardata) +end + +function prepare_do_transport_TMM_packed(m::PB.ReactionMethod, vardata) + rj = m.reaction + + # read matrix data in prepare so it is deferred until initialize + _read_matrix_data(rj) + + return vardata +end + + +function _read_matrix_data(rj::ReactionOceanTransportTMM) + + # calculate model time for list of matrices + if rj.pars.use_annualmean[] + rj.matrix_times = [NaN] + rj.matrix_tinterp = nothing + else + rj.matrix_start_time = 0.5/rj.pars.num_seasonal[] + rj.matrix_delta_time = 1.0/rj.pars.num_seasonal[] + rj.matrix_cycle_time = 1.0 + rj.matrix_times = collect( + range( + rj.matrix_start_time, + step=rj.matrix_delta_time, + length=rj.pars.num_seasonal[] + ) + ) + rj.matrix_tinterp = PB.LinInterp(rj.matrix_times, 1.0) + end + + tmp_Aexp_tr= [] + tmp_Aimp_tr = [] + + # transpose, convert datatype, optionally permute + function permute_indices_transpose(A) + I, J, V = SparseArrays.findnz(A) + if rj.pars.kji_order[] + A = SparseArrays.sparse(rj.index_perm_inverse[J], rj.index_perm_inverse[I], rj.TMeltype.(V)) # swap I, J to transpose + else + A = SparseArrays.sparse(J, I, rj.TMeltype.(V)) + end + return A + end + + + if rj.pars.use_annualmean[] + Aexp_paths = [joinpath(rj.pars.base_path[], config_data["explicitAnnualMeanMatrixFile"]*".mat")] + Aexp_name = "Aexpms" + Aimp_paths = [joinpath(rj.pars.base_path[], config_data["implicitAnnualMeanMatrixFile"]*".mat")] + Aimp_name = "Aimpms" + num_matrices = 1 + else + num_matrices = rj.pars.num_seasonal[] + Aexp_paths = [ + joinpath(rj.pars.base_path[], rj.config_data["explicitMatrixFileBase"]*@sprintf("_%02i.mat",i)) + for i in 1:num_matrices + ] + Aexp_name = "Aexp" + Aimp_paths = [ + joinpath(rj.pars.base_path[], rj.config_data["implicitMatrixFileBase"]*@sprintf("_%02i.mat",i)) + for i in 1:num_matrices + ] + Aimp_name = "Aimp" + end + + tmp_Aexp_tr = Vector{Any}(nothing, rj.pars.num_seasonal[]) + tmp_Aimp_tr = Vector{Any}(nothing, rj.pars.num_seasonal[]) + + # work out upscaling factor for implicit matrix + deltaT = rj.grid["deltaT"] + rj.pars.Aimp_deltat[] % deltaT == 0 || + error("requested Aimp_deltat $(rj.pars.Aimp_deltat[]) is not a multiple of matrix deltaT = $deltaT") + rj.Aimp_mult = rj.pars.Aimp_deltat[]/deltaT + Aimp_deltat_yr = rj.pars.Aimp_deltat[]/PB.Constants.k_secpyr + + # For large (1 deg) matrices, attempt to minimise memory use + # NB: the underlying hdf5 library is not thread safe so we can't use threads without + # adding a lot of complexity / using a lot of memory + # tlock = ReentrantLock() + # Threads.@threads + for i in 1:num_matrices + matAexp = nothing + matAimp = nothing + # @Base.lock tlock begin # serialize disk reads to avoid hammering the disk + Aexp_path = Aexp_paths[i] + @info " reading explicit matrix data from $Aexp_path" + file = MAT.matopen(Aexp_path) + matAexp = MAT.read(file, Aexp_name) + close(file) + + Aimp_path = Aimp_paths[i] + @info " reading implicit matrix data from $Aimp_path" + file = MAT.matopen(Aimp_path) + matAimp = MAT.read(file, Aimp_name) + close(file) + # end + + tmp_Aexp_tr[i] = permute_indices_transpose(matAexp) + matAexp = nothing + if rj.pars.sal_norm[] + error("TODO sal_norm") + # matrix_dt = calcSalNorm(matrix_dt.tocsr(), sal) + end + # Aexp is already in differential form, convert s-1 to yr-1 + tmp_Aexp_tr[i] .= PB.Constants.k_secpyr.*tmp_Aexp_tr[i] + + # convert implicit matrix + tmp_Aimp_tr[i] = permute_indices_transpose(matAimp) + matAimp = nothing + @info " converting implicit matrix $i deltaT=$deltaT sec x $(rj.Aimp_mult) "* + "-> $(rj.pars.Aimp_deltat[]) sec ($Aimp_deltat_yr yr)" + if rj.pars.sal_norm[] + error("TODO sal_norm") + # matrix_dt = calcSalNorm(matrix_dt.tocsr(), sal) + end + # Aimp is in 'discrete' form (C_n+1 = Aimp * C_n) with timestep deltaT sec + # Upscale by factor rj.Aimp_mult to timestep rj.pars.Aimp_deltat[] seconds, Aimp_deltat_yr years, + # and convert to differential form, units yr-1 + tmp_Aimp_tr[i] = (tmp_Aimp_tr[i]^rj.Aimp_mult - LinearAlgebra.I)./Aimp_deltat_yr + + end + + # convert to CSR format with common sparsity pattern for fast multiply x vector + rj.trspt_dAexp_tr = PALEOocean.Ocean.create_common_sparsity_tr!( + tmp_Aexp_tr, + do_transpose=false, + TMeltype=rj.TMeltype + ) + tmp_Aexp_tr = [] + + # convert to CSR format with common sparsity pattern for fast multiply x vector + rj.trspt_dAimp_tr = PALEOocean.Ocean.create_common_sparsity_tr!( + tmp_Aimp_tr, + do_transpose=false, + TMeltype=rj.TMeltype + ) + tmp_Aimp_tr = [] + + return nothing +end + +function do_transport_TMM( + m::PB.ReactionMethod, + (grid_tforce_vars, conc_components, sms_components, _, buffer), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + tm_field = m.p + + recswts = PALEOocean.Ocean.get_weights( + rj.matrix_tinterp, + grid_tforce_vars.tforce[], + ) + + # need to supply a type explicitly as tm member Variable is untyped + TrsptCSCType = PALEOocean.Ocean.TrsptCSC{Float64} + PALEOocean.Ocean.do_transport_tr( + grid_tforce_vars, conc_components, sms_components, nothing, buffer, + getfield(rj, tm_field)::TrsptCSCType, recswts, + cellrange + ) + + return nothing +end + + +"calculate packed concentration in a separate reaction so all tiles available for do_transport" +function do_transport_TMM_pack_conc( + m::PB.ReactionMethod, + (conc_components, dummy_var), + cellrange::PB.AbstractCellRange, + deltat, +) + packed_buffer = m.p + + PALEOocean.Ocean.transport_pack_conc!( + packed_buffer, conc_components, + cellrange) + + return nothing +end + +function do_transport_TMM_packed( + m::PB.ReactionMethod, + (grid_tforce_vars, sms_components, dummy_var), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + tm_field, packed_buffer = m.p + + recswts = PALEOocean.Ocean.get_weights( + rj.matrix_tinterp, + grid_tforce_vars.tforce[], + ) + + # need to supply a type explicitly as tm member Variable is untyped + TrsptCSCType = PALEOocean.Ocean.TrsptCSC{eltype(packed_buffer.packed_conc_array)} + PALEOocean.Ocean.do_transport_tr( + grid_tforce_vars, sms_components, packed_buffer, + getfield(rj, tm_field)::TrsptCSCType, recswts, + cellrange, + ) + + return nothing +end + + +"set default TMMDir key if not present in LocalPreferences.toml" +function __init__() + + defaultTMMdir = normpath(@__DIR__, "../../../TMM") + for mod in [@__MODULE__, PB] # forcing Reactions in PALEOboxes may also need TMMDir + if !Preferences.has_preference(mod, "TMMDir") + @info "OceanTransportTMM adding default TMMDir => $defaultTMMdir to [$mod] LocalPreferences.toml (modify for your local setup)" + Preferences.set_preferences!(mod, "TMMDir" => defaultTMMdir) + end + end + + return nothing +end + +end # module diff --git a/src/ocean/VerticalTransport.jl b/src/ocean/VerticalTransport.jl new file mode 100644 index 0000000..5c077de --- /dev/null +++ b/src/ocean/VerticalTransport.jl @@ -0,0 +1,908 @@ +"Ocean vertical transport Reactions" +module VerticalTransport + +import SparseArrays + +import PALEOboxes as PB +using PALEOboxes.DocStrings + +# import Infiltrator + + +""" + ReactionLightColumn + +Calculate light availability `insol` in ocean interior, given surface insolation `surface_insol`. + +Includes: (i) a `background_opacity`; (ii) contributions from any Variables representing concentrations with non-initialize_to_zero +`specific_light_extinction` attribute; (iii) any other opacity contributions added to the Target Variable `opacity`. + +# Parameters +$(PARS) +""" +Base.@kwdef mutable struct ReactionLightColumn{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParDouble("background_opacity", 0.04, units="m-1", + description="background opacity"), + ) + + "names of Variables with non-zero :specific_light_extinction" + names_specific_light_extinction::Vector{String} = String[] + "values read from :specific_light_extinction attributes" + vec_specific_light_extinction::Vector{Float64} = Float64[] +end + +PB.register_methods!(rj::ReactionLightColumn) = nothing + +# find all Variables (representing concentrations) with attribute :specific_light_extinction != 0.0 +function _find_opacity_variables(domain::PB.Domain) + filter_opacity(v) = PB.get_attribute(v, :specific_light_extinction, 0.0) != 0.0 + return PB.get_variables(domain, filter_opacity) +end + +function PB.register_dynamic_methods!(rj::ReactionLightColumn) + + vars = [ + PB.VarDepColumn("oceansurface.surface_insol", "W m-2", "surface downwelling radiative flux"), + PB.VarProp("insol", "W m-2", "interior downwelling radiative flux"), + PB.VarTarget("opacity", "m-1", "total opacity from all contributions"), + + PB.VarDep("zupper", "m", "depth of upper surface of box (m) 0 is surface, -100 is depth of 100 m"), + PB.VarDep("zlower", "m", "depth of lower surface of box (m) 0 is surface, -100 is depth of 100 m"), + PB.VarDep("zmid", "m", "depth of mid point of box (m)"), + ] + + # find all Variables (representing concentrations) with attribute :specific_light_extinction != 0.0 + # (value will be reread later in setup_light_column to allow configuration updates) + rj.names_specific_light_extinction = [v.name for v in _find_opacity_variables(rj.domain)] + + # create ReactionVariables (which will be linked to domvars_conc_opacity) + vars_conc_opacity = [ + PB.VarDep(name, "", "") + for name in rj.names_specific_light_extinction + ] + io = IOBuffer() + println(io, "register_dynamic_methods! $(PB.fullname(rj)) "* + "adding opacity contributions from $(length(vars_conc_opacity)) Variables:") + for v in vars_conc_opacity + println(io, " $(v.localname)") + end + @info String(take!(io)) + + PB.add_method_setup!( + rj, + setup_light_column, + (PB.VarList_vector(vars_conc_opacity), ), + ) + + PB.add_method_do!( + rj, + do_light_column, + (PB.VarList_namedtuple(vars), PB.VarList_vector(vars_conc_opacity), ), + ) + + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +"read :specific_light_extinction attributes " +function setup_light_column( + m::PB.ReactionMethod, + (vars_conc_opacity, ), + cellrange::PB.AbstractCellRange, + attribute_name +) + attribute_name == :setup || return + + rj = m.reaction + # check no new Variables with non-zero opacity + current_names = [v.name for v in _find_opacity_variables(rj.domain)] + new_names = setdiff(current_names, rj.names_specific_light_extinction) + isempty(new_names) || + error("setup_light_column $(PB.fullname(rj)): Variables $new_names have been updated "* + "from zero to non-zero :specific_light_extinction since model creation "* + "(fix: set :specific_light_extinction attribute to a dummy but non-zero value in the .yaml config file") + + # read values from Variable :specific_light_extinction attribute + empty!(rj.vec_specific_light_extinction) + (vars_conc_opacity, ) = PB.get_variables_tuple(m) + for v in vars_conc_opacity + # get specific_light_extinction from the linked VariableDomain + push!( + rj.vec_specific_light_extinction, + PB.get_domvar_attribute(v, :specific_light_extinction)::Float64 + ) + end + + io = IOBuffer() + println(io, "setup_light_column $(PB.fullname(rj)): Variables with non-zero :specific_light_extinction :") + for (name, sle) in PB.IteratorUtils.zipstrict(rj.names_specific_light_extinction, rj.vec_specific_light_extinction) + println(io, " ", name, sle) + end + @info String(take!(io)) + + return nothing +end + +function do_light_column( + m::PB.ReactionMethod, + pars, + (vars, vars_conc_opacity), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + + @inbounds for (isurf, colindices) in cellrange.columns + surface_flux = vars.surface_insol[isurf] + optical_path_length = zero(first(vars.opacity)) # optical path through this column from surface + for i in colindices + # Accumulate contributions to opacity in this cell: + # Background opacity + vars.opacity[i] += pars.background_opacity[] + # Contributions from Variables with non-zero :specific_light_extinction : + for (sle, v_conc) in PB.IteratorUtils.zipstrict(rj.vec_specific_light_extinction, vars_conc_opacity) + vars.opacity[i] += sle*v_conc[i] + end + + # Calculate optical path length and light transport: + # Add contribution from cell upper to cell mid-point + optical_path_length += vars.opacity[i]*(vars.zupper[i] - vars.zmid[i]) + vars.insol[i] = exp(-optical_path_length)*surface_flux + # Add contribution from cell mid-point to cell lower (will be used on next iteration) + optical_path_length += vars.opacity[i]*(vars.zmid[i] - vars.zlower[i]) + end + end + + return nothing +end + + +""" + ReactionExportDirect + +Vertical particle sinking represented as instantaneous transport, described by fixed matrices (suitable for small ocean models) + +Transports a list of fluxes defined by parameter `fluxlist`, from input Target Variables with local name +`export_` to output Contributor Variables `remin_`. + +Matrices are defined by parameter `transportocean` for sinking within water column, and parameter `transportfloor` for flux to ocean floor. + +For larger, column-based ocean models use [`ReactionExportDirectColumn`](@ref). + +# Parameters +$(PARS) +""" +Base.@kwdef mutable struct ReactionExportDirect{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParStringVec("fluxlist", ["P", "N", "Corg"], + description="names of fluxes to transport"), + PB.ParDoubleVecVec("transportocean", + description="matrix describing ocean export"), + PB.ParDoubleVecVec("transportfloor", + description="matrix describing oceanfloor export"), + + PB.ParDouble("conserv_errthresh", 1e-5, + description="error threshold for conservation check (fraction of input)"), + ) + + trspt_ocean_tr::SparseArrays.SparseMatrixCSC{Float64, Int64} = SparseArrays.spzeros(0, 0) + trspt_floor_tr::SparseArrays.SparseMatrixCSC{Float64, Int64} = SparseArrays.spzeros(0, 0) + + do_transportocean = false + do_transportfloor = false + + domain_oceanfloor = nothing +end + +function PB.register_methods!(rj::ReactionExportDirect, model::PB.Model) + + @info "register_methods! $(PB.fullname(rj)) add ocean fluxlist=$(rj.pars.fluxlist.v)" + vars_input_target = PB.Fluxes.FluxTarget("export_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="input particulate export flux" + ) + # We need to access vars_input_target from two different methods (ocean and oceanfloor), + # which isn't possible for a Target variable. + # So add as a do_nothing Target, then access (twice) as VarDep. + PB.add_method_do_nothing!(rj, vars_input_target) + + if !isempty(rj.pars.transportocean.v) + rj.do_transportocean = true + @info "$(PB.fullname(rj)) add ocean fluxlist=$(rj.pars.fluxlist.v)" + vars_oceanremin = PB.Fluxes.FluxContrib("remin_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="output particulate export flux" + ) + vars_input = [PB.VarDep(v) for v in vars_input_target] + PB.add_method_do!( + rj, + do_export_direct_ocean, + (PB.VarList_components(vars_input), PB.VarList_components(vars_oceanremin)), + # preparefn=prepare_export_direct, + ) + end + + if !isempty(rj.pars.transportfloor.v) + rj.do_transportfloor = true + rj.domain_oceanfloor = PB.get_domain(model, "oceanfloor") + !isnothing(rj.domain_oceanfloor) || + error("$(PB.fullname(rj)) configuration error: no Domain oceanfloor") + + @info "$(PB.fullname(rj)) add oceanfloor fluxlist=$(rj.pars.fluxlist.v)" + vars_oceanfloor = PB.Fluxes.FluxContrib( + "fluxOceanfloor.particulateflux_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="output particulate export flux" + ) + # no length check, as we access ocean from oceanfloor Domain + vars_input_unchecked = [] + for v in vars_input_target + vv = PB.VarDep(v) + PB.set_attribute!(vv, :check_length, false; allow_create=true,) + PB.reset_link_namestr!(vv, "ocean."*v.localname) + push!(vars_input_unchecked, vv) + end + PB.add_method_do!( + rj, + do_export_direct_oceanfloor, + (PB.VarList_components(vars_input_unchecked), PB.VarList_components(vars_oceanfloor)), + domain=rj.domain_oceanfloor, + ) + end + + PB.add_method_setup!( + rj, + setup_export_direct, + (), + ) + + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +function setup_export_direct( + m::PB.ReactionMethod, + pars, + _, + cellrange::PB.AbstractCellRange, + attribute_name, +) + rj = m.reaction + attribute_name == :setup || return + + # calculate matrices and check matrix sizes + if rj.do_transportocean + @info "setup_export_direct: $(PB.fullname(rj)) ocean vertical transport from Parameter 'transportocean' = $(pars.transportocean.v)" + + trspt_ocean = PB.vecvecpar_matrix(pars.transportocean) + size(trspt_ocean) == (PB.get_length(rj.domain), PB.get_length(rj.domain)) || + error("$(PB.fullname(rj)) invalid Parameter 'transportocean' matrix size != ocean ncells") + + rj.trspt_ocean_tr = SparseArrays.sparse(transpose(trspt_ocean)) # store transpose so it is easy to multiply + else + isempty(pars.transportocean.v) || + error("$(PB.fullname(rj)) ocean vertical transport not enabled: set Parameter 'transportocean' to non-empty in config file") + end + + if rj.do_transportfloor + @info "setup_export_direct: $(PB.fullname(rj)) ocean -> oceanfloor transport from Parameter 'transportfloor' = $(pars.transportfloor.v)" + + trspt_floor = PB.vecvecpar_matrix(pars.transportfloor) + size(trspt_floor) == (PB.get_length(rj.domain_oceanfloor), PB.get_length(rj.domain)) || + error("$(PB.fullname(rj)) invalid Parameter 'transportfloor' matrix size != ocean ncells") + + rj.trspt_floor_tr = SparseArrays.sparse(transpose(trspt_floor)) # store transpose so it is easy to multiply + else + isempty(pars.transportfloor.v) || + error("$(PB.fullname(rj)) ocean -> oceanfloor transport not enabled: set Parameter 'transportfloor' to non-empty in config file") + end + + # Check conservation + for i=1:PB.get_length(rj.domain) + sumoutput = 0.0 + if rj.do_transportocean + sumoutput += sum(trspt_ocean[:, i]) + end + if rj.do_transportfloor + sumoutput += sum(trspt_floor[:, i]) + end + abs(sumoutput-1.0) < pars.conserv_errthresh[] || + error("Reaction $(PB.fullname(rj)) conservation error exceeds threshold: ocean cell $i output $sumoutput != 1.0") + end + + return nothing +end + + +# output[jrange] = input*A_tr[:, jrange] +function _mul_sparse_jrange!(output, jrange, A_tr::SparseArrays.SparseMatrixCSC, input) + for j in jrange + for idx in SparseArrays.nzrange(A_tr, j) + i = A_tr.rowval[idx] + output[j] += A_tr.nzval[idx]*input[i] + end + end + return nothing +end +# ignore unlinked output variable +_mul_sparse_jrange!(output::Nothing, jrange, A_tr::SparseArrays.SparseMatrixCSC, input) = nothing + +function do_export_direct_ocean( + m::PB.ReactionMethod, + (components_input, components_oceanremin), + cellrange::PB.AbstractCellRange, + deltat +) + PB.IteratorUtils.check_lengths_equal( + components_input, components_oceanremin; + errmsg="do_export_direct_ocean: components length mismatch components_input, components_oceanremin (check :field_data (ScalarData, IsotopeLinear etc) match)" + ) + + for (input, output) in PB.IteratorUtils.zipstrict(components_input, components_oceanremin) + _mul_sparse_jrange!(output, cellrange.indices, m.reaction.trspt_ocean_tr, input) + end + + return nothing +end + +function do_export_direct_oceanfloor( + m::PB.ReactionMethod, + (components_input, components_oceanfloor), + cellrange::PB.AbstractCellRange, + deltat +) + PB.IteratorUtils.check_lengths_equal( + components_input, components_oceanfloor; + errmsg="do_export_direct_ocean: components length mismatch components_input, components_oceanfloor (check :field_data (ScalarData, IsotopeLinear etc) match)" + ) + + for (input, output) in PB.IteratorUtils.zipstrict(components_input, components_oceanfloor) + _mul_sparse_jrange!(output, cellrange.indices, m.reaction.trspt_floor_tr, input) + end + + return nothing +end + + +""" + ReactionExportDirectColumn + +Vertical particle sinking represented as instantaneous transport. As [`ReactionExportDirect`](@ref), +but for regular column-based models with functional form of flux vs depth defined by `exportfunction` parameter. + +Transports a list of fluxes defined by parameter `fluxlist`, from input Target Variables with local name +`export_` to output Contributor Variables `remin_`. + +# Parameters +$(PARS) +""" +Base.@kwdef mutable struct ReactionExportDirectColumn{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParStringVec("fluxlist", String[], + description="names of fluxes to transport"), + + PB.ParBool("transportfloor", true, + description="true to provide oceanfloor flux, false to recycle flux into lowest ocean cell"), + + PB.ParString("exportfunction", "SumExp", allowed_values=keys(EXPORT_FUNCTIONS), + description="functional form for particle flux vs depth"), + + PB.ParDoubleVec("input_frac", [1.0], + description="fractions of input for each component ([1.0] for Martin, length=number of components for SumExp"), + + PB.ParDoubleVec("sumexp_scale", [500.0], units="m", + description="length scales for each component of exponential decay of flux with depth"), + + PB.ParDouble("martin_rovera", 0.858, + description="Martin power law exponent: flux \\propto depth^rovera"), + PB.ParDouble("martin_depthmin", 100.0, units="m", + description="Martin power law minimum depth for start of decay with depth"), + ) + + Ncomps::Int64 = -1 +end + +PB.register_methods!(rj::ReactionExportDirectColumn) = nothing + +function PB.register_dynamic_methods!(rj::ReactionExportDirectColumn) + + @info "register_dynamic_methods!: $(PB.fullname(rj)) "* + "export_function $(rj.pars.exportfunction[]) ocean fluxlist=$(rj.pars.fluxlist.v)" + + vars = [ + PB.VarDep("zupper", "m", "depth of upper surface of box (m) 0 is surface, -100 is depth of 100 m"), + PB.VarDep("zlower", "m", "depth of lower surface of box (m)"), + PB.VarDep("Abox", "m^2", "horizontal area of box"), + # NB: we access Afloor via ocean subdomain which will provide ocean -> oceanfloor indices mapping + PB.VarDep("oceanfloor.ocean.Afloor", "m^2", "horizontal area of seafloor at base of box"), + ] + + vars_input = PB.Fluxes.FluxTarget("export_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="input particulate export flux" + ) + + vars_oceanremin = PB.Fluxes.FluxContrib("remin_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="output particulate export flux" + ) + + if rj.pars.transportfloor[] + @info " add oceanfloor fluxlist=$(rj.pars.fluxlist.v)" + vars_oceanfloor = values(PB.Fluxes.FluxContrib( + "fluxOceanfloor.particulateflux_", rj.pars.fluxlist, + isotope_data=rj.external_parameters, + description="output particulate export flux" + )) + # no length check here as we access fluxOceanfloor from ocean Domain using indices from Afloor + vars_oceanfloor_unchecked = PB.set_attribute!.(vars_oceanfloor, :check_length, false; allow_create=true,) + else + vars_oceanfloor = [] + vars_oceanfloor_unchecked = vars_oceanfloor + end + + # export function to use + export_function, ncomps_function = EXPORT_FUNCTIONS[rj.pars.exportfunction[]] + PB.setfrozen!(rj.pars.exportfunction) + + PB.add_method_do!( + rj, + do_export_direct_column, + ( + PB.VarList_namedtuple(vars), + PB.VarList_components(vars_input), + PB.VarList_components(vars_oceanremin), + isempty(vars_oceanfloor_unchecked) ? PB.VarList_nothing() : PB.VarList_components(vars_oceanfloor_unchecked), + ), + p=(export_function, ncomps_function), + preparefn=prepare_do_export_direct_column, + ) + + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +function _export_functions_dict() + + # exponential decay of export flux with depth + function fracfluxout_SumExp(pars, vars, i, ic) + # fraction of flux into top of box that leaves bottom of box + frac_fluxout = exp((vars.zlower[i]-vars.zupper[i])/pars.sumexp_scale[ic]) + return frac_fluxout + end + # number of components in flux vs depth function + function ncomps_SumExp(pars, currentNcomps) + Ncomps = length(pars.sumexp_scale) + currentNcomps == -1 || currentNcomps == Ncomps || + error("ncomps_SumExp $(PB.fullname(rj)): length(sumexp_scale) has changed") + return Ncomps + end + + # Martin power-law decay of export flux with depth + function fracfluxout_Martin(pars, vars, i, ic) + # fraction of flux into top of box that leaves bottom of box + zlower_eff = min(vars.zlower[i], -pars.martin_depthmin[]) + zupper_eff = min(vars.zupper[i], -pars.martin_depthmin[]) + frac_fluxout = (zupper_eff/zlower_eff)^pars.martin_rovera[] + return frac_fluxout + end + # number of components in flux vs depth function + ncomps_Martin(pars, currentNcomps) = 1 + + return Dict( + "SumExp" => (fracfluxout_SumExp, ncomps_SumExp), + "Martin" => (fracfluxout_Martin, ncomps_Martin), + ) +end + +const EXPORT_FUNCTIONS = _export_functions_dict() + +# create buffers +function prepare_do_export_direct_column( + m::PB.ReactionMethod, + ( + vars, + components_input, # Vector of component Arrays + components_oceanremin, + components_oceanfloor, + ) +) + rj = m.reaction + export_function, ncomps_function = m.p + + rj.Ncomps = ncomps_function(rj.pars, rj.Ncomps) + + (_, rvars_input, _, _) = PB.get_variables_tuple(m) + + # buffer to accumulate per-cell fluxes (one per thread) + flux_buf = [ + Array{eltype(first(components_input))}(undef, length(components_input), rj.Ncomps) + for t in 1:Threads.nthreads() + ] + # add flux_buf to the end of the Tuple + return (vars, components_input, components_oceanremin, components_oceanfloor, flux_buf) +end + + +function do_export_direct_column( + m::PB.ReactionMethod, + pars, + ( + vars, + components_input, + components_oceanremin, + components_oceanfloor, + flux_buf, + ), + cellrange::PB.AbstractCellRange, + deltat, +) + rj = m.reaction + export_function, ncomps_function = m.p + + Ncomps = ncomps_function(pars, rj.Ncomps) + PB.check_parameter_sum(pars.input_frac, Ncomps, tol=1e-6) || + error("do_export_direct_column: input_frac does not have the correct number of components and sum to 1.0") + length(components_input) == length(components_oceanremin) || + error("do_export_direct_column: components length mismatch components_input, components_oceanremin (check :field_data (ScalarData, IsotopeLinear etc) match)") + isnothing(components_oceanfloor) || length(components_input) == length(components_oceanfloor) || + error("do_export_direct_column: components length mismatch components_input, components_oceanfloor (check :field_data (ScalarData, IsotopeLinear etc) match)") + + flux = flux_buf[Threads.threadid()] + + (data_Afloor, oceanfloor_indices) = vars.Afloor + + for (icol, colindices) in cellrange.columns + fill!(flux, 0.0) + for i in colindices + if ismissing(oceanfloor_indices[i]) + # no oceanfloor under this box + floor_frac = 0.0 + else + # calculate areas and partition into oceanfloor flux and sinking flux + floor_idx = oceanfloor_indices[i]::Int + floor_frac = data_Afloor[floor_idx]/vars.Abox[i] + end + + for n in 1:Ncomps + # fraction of flux into top of box that leaves bottom of box + fracfluxout = export_function(pars, vars, i, n) + for j in 1:length(components_input) + # flux is flux in to top of cell i + flux_remin = (1.0 - fracfluxout)*flux[j,n] + components_oceanremin[j][i] += flux_remin + + flux_out = fracfluxout*flux[j,n] + components_input[j][i]*pars.input_frac[n] + + if floor_frac > 0.0 + floor_flux = floor_frac*flux_out + + if isnothing(components_oceanfloor) + # no explicit ocean floor: assume uniform column and + # recycle flux out of bottom box back into bottom box + components_oceanremin[j][i] += floor_flux + else + # flux to ocean floor + components_oceanfloor[j][floor_idx] += floor_flux + end + end + + flux[j,n] = (1.0 - floor_frac)*flux_out + + end + end + end + end + + return nothing +end + +"Simplified version for a regular column with oceanfloor only at base. +Little difference in speed" +function do_export_direct_regular_column( + m::PB.ReactionMethod, + pars, + ( + vars, + components_input, + components_oceanremin, + components_oceanfloor, + flux_buf, + ), + cellrange::PB.AbstractCellRange, + deltat, +) + rj = m.reaction + export_function, ncomps_function = m.p + + Ncomps = ncomps_function(rj, rj.Ncomps) + PB.check_parameter_sum(rj.pars.input_frac, Ncomps, tol=1e-6) || + error("do_export_direct_regular_column: input_frac does not have the correct number of components and sum to 1.0") + length(components_input) == length(components_oceanremin) || + error("do_export_direct_regular_column: components length mismatch components_input, components_oceanremin (check :field_data (ScalarData, IsotopeLinear etc) match)") + isnothing(components_oceanfloor) || length(components_input) == length(components_oceanfloor) || + error("do_export_direct_regular_column: components length mismatch components_input, components_oceanfloor (check :field_data (ScalarData, IsotopeLinear etc) match)") + + + flux = flux_buf[Threads.threadid()] + + (data_Afloor, oceanfloor_indices) = vars.Afloor + + for (icol, colindices) in cellrange.columns + fill!(flux, 0.0) + lasti = 0 + for i in colindices + + for n in 1:Ncomps + # fraction of flux into top of box that leaves bottom of box + fracfluxout = export_function(rj, vars, i, n) + lastj = 0 + for j in 1:length(components_input) + # flux is flux in to top of cell i + flux_remin = (1.0 - fracfluxout)*flux[j,n] + components_oceanremin[j][i] += flux_remin + + flux_out = fracfluxout*flux[j,n] + components_input[j][i]*pars.input_frac[n] + + flux[j,n] = flux_out + end + end + lasti = i + end + if isnothing(components_oceanfloor) + # no explicit ocean floor: assume uniform column and + # recycle flux out of bottom box back into bottom box + for j in 1:length(components_input) + for n in 1:Ncomps + components_oceanremin[j][lasti] += flux[j, n] + end + end + else + # flux to ocean floor + floor_idx = oceanfloor_indices[lasti]::Int + for j in 1:length(components_input) + for n in 1:Ncomps + components_oceanfloor[j][floor_idx] += flux[j, n] + end + end + end + + end + + return nothing +end + + + +""" + ReactionSinkFloat + +Vertical particle advection. + +Applied to all concentration Variables with non-zero attribute `:vertical_movement` (m d-1, +ve upwards), +using naming convention `_conc` to identify `_sms` Deriv Variable to apply to. +An optional variable `_w` may be defined that overrides `:vertical_movement` to define spatially-variable +vertical motion. + +# Parameters +$(PARS) +""" +Base.@kwdef mutable struct ReactionSinkFloat{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParBool("transportfloor", true, + description="true to provide oceanfloor flux, false to recycle flux into lowest ocean cell"), + ) + + var_rootnames::Vector{String} = String[] + ":vertical_movement attribute" + var_vertical_movement::Vector{Float64} = Float64[] +end + +PB.register_methods!(rj::ReactionSinkFloat) = nothing + +function PB.register_dynamic_methods!(rj::ReactionSinkFloat) + + vars = [ + PB.VarDep("zupper", "m", "depth of upper surface of box (m) 0 is surface, -100 is depth of 100 m"), + PB.VarDep("zlower", "m", "depth of lower surface of box (m)"), + PB.VarDep("Abox", "m^2", "horizontal area of box"), + # NB: we access Afloor via ocean subdomain which will provide ocean -> oceanfloor indices mapping + PB.VarDep("oceanfloor.ocean.Afloor", "m^2", "horizontal area of seafloor at base of box"; attributes=(:check_length=>false,)), + ] + + rj.var_rootnames = _find_sinkfloat_rootnames(rj.domain) + + vars_conc = [PB.VarDep(rn*"_conc", "", "") for rn in rj.var_rootnames] + vars_sms = [PB.VarContrib(rn*"_sms", "", "") for rn in rj.var_rootnames] + vars_w = [PB.VarDep("("*rn*"_w)", "", "") for rn in rj.var_rootnames] + PB.setfrozen!(rj.pars.transportfloor) + vars_fluxOceanfloor = if rj.pars.transportfloor[] + [PB.VarContrib("fluxOceanfloor.sinkflux_$(rn)", "", ""; attributes=(:check_length=>false,)) for rn in rj.var_rootnames] + else + [] + end + + PB.add_method_setup!( + rj, + setup_sink_float, + ( + PB.VarList_tuple(vars_conc), + PB.VarList_tuple(vars_w), + ), + ) + + PB.add_method_do!( + rj, + do_sink_float, + ( + PB.VarList_namedtuple(vars), + PB.VarList_tuple(vars_conc), + PB.VarList_tuple(vars_sms), + PB.VarList_tuple(vars_w), + isempty(vars_fluxOceanfloor) ? + PB.VarList_tuple_nothing(length(vars_conc)) : + PB.VarList_tuple(vars_fluxOceanfloor), + ), + ) + + return nothing +end + +"Find all variables with attribute :vertical_movement != 0.0, and check name of form _conc" +function _find_sinkfloat_rootnames(domain::PB.Domain) + + filter_conc(v) = PB.get_attribute(v, :vertical_movement, 0.0) != 0.0 + domvars_conc_sinkfloat = PB.get_variables(domain, filter_conc) + + rootnames = String[] + for v in domvars_conc_sinkfloat + length(v.name) >= 5 && v.name[end-4:end] == "_conc" || + error("find_sinkfloat_rootnames: Variable $(PB.fullname(v)) "* + "has :vertical_movement attribute but is not named _conc") + push!(rootnames, v.name[1:end-5]) + end + + return rootnames +end + +function setup_sink_float( + m::PB.ReactionMethod, + ( + vars_conc, + vars_w, + ), + cellrange::PB.AbstractCellRange, + attribute_name +) + attribute_name == :setup || return + + rj = m.reaction + # rvars_ are VariableReactions corresponding to data arrays vars_ + (rvars_conc, rvars_w) = PB.get_variables_tuple(m) + + # check for misconfiguration (:vertical_movement changed from 0.0 to non-zero after model creation) + current_rootnames = _find_sinkfloat_rootnames(rj.domain) + new_rootnames = setdiff(current_rootnames, rj.var_rootnames) + isempty(new_rootnames) || + error("setup_sink_float $(PB.fullname(rj)): Variables $(new_rootnames.*"_conc") have been updated "* + "from zero to non-zero :vertical_movement since model creation "* + "(fix: set :vertical_movement attribute to a dummy but non-zero value in the .yaml config file") + + # read :vertical_movement attribute + empty!(rj.var_vertical_movement) + io = IOBuffer() + println(io, "setup_sink_float: $(PB.fullname(rj)):") + for (var_w, rvar_w, rvar_conc) in PB.IteratorUtils.zipstrict(vars_w, rvars_w, rvars_conc) + # NB: get :vertical_movement from the Variable we link to, not our local VarDep + w_val = PB.get_domvar_attribute(rvar_conc, :vertical_movement) + isa(w_val, Real) || + error("setup_sink_float $(PB.fullname(rj)): Variable $(PB.fullname(rvar_conc.linkvar)) :vertical_movement attribute $w_val is not a number") + push!(rj.var_vertical_movement, w_val) # not used if w_var linked + if !isnothing(var_w) + println(io, " add $(rvar_conc.linkvar.name) using $(rvar_w.linkvar.name) (m d-1)") + else + println(io, " add $(rvar_conc.linkvar.name) using :vertical_movement $w_val (m d-1)") + end + end + @info String(take!(io)) + + return nothing +end + +function do_sink_float( + m::PB.ReactionMethod, + ( + vars, + vars_conc, + vars_sms, + vars_w, + vars_fluxOceanfloor, + ), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + + PB.IteratorUtils.foreach_longtuple_p( + do_sink_kernel, + vars_conc, vars_sms, vars_w, rj.var_vertical_movement, vars_fluxOceanfloor, + (rj, vars, cellrange, deltat) + ) + +end + +"calculate vertical advection for a single variable" +function do_sink_kernel( + ac_conc, + ac_sms, + ac_w, + ac_vertical_movement, + ac_floor, + (rj, vars, cellrange, deltat) +) + + (data_Afloor, oceanfloor_indices) = vars.Afloor + + @inbounds for (icol, colindices) in cellrange.columns + + for i in colindices + w = isnothing(ac_w) ? ac_vertical_movement : ac_w[i] + + # convert m d-1 to m yr-1 + w *= PB.Constants.k_daypyr + + # throttle rate for numerical stability + if deltat > 0.0 + w = sign(w)*min(abs(w), (vars.zupper[i]-vars.zlower[i])/deltat) + end + + if w > 0 && i > 1 + # upward flux into box above + flux = ac_conc[i]*w*vars.Abox[i] + ac_sms[i] -= flux + ac_sms[i-1] += flux + elseif w < 0 + # downward flux into box below and oceanfloor + flux = ac_conc[i]*(-w)*vars.Abox[i] + ac_sms[i] -= flux + + if ismissing(oceanfloor_indices[i]) + # no oceanfloor under this box + floor_frac = 0.0 + else + # calculate areas and partition into oceanfloor flux and sinking flux + floor_idx = oceanfloor_indices[i]::Int + floor_frac = data_Afloor[floor_idx]/vars.Abox[i] + if isnothing(ac_floor) + ac_sms[i] += floor_frac*flux # add back oceanfloor flux to current cell + else + ac_floor[floor_idx] += floor_frac*flux + end + end + + if i == last(colindices) + abs(1.0-floor_frac) < 1e-6 || @warn "deepest ocean cell icol $icol not covered by oceanfloor" + else + # transfer flux to cell below + ac_sms[i+1] += (1.0-floor_frac)*flux + end + end + end + end + + return nothing +end + +end # module From 7742a5bea67083422c0c71a775cb06727b0fea26 Mon Sep 17 00:00:00 2001 From: Daines Date: Fri, 21 Apr 2023 13:28:37 +0100 Subject: [PATCH 2/2] Add 3 box, MITgcm examples Examples added from PALEOdev.jl v0.21.7 --- Project.toml | 16 +- README.md | 2 +- docs/src/PALEOocean Reactions.md | 28 +- .../PTBClarkson2014/README.md | 9 + docs/src/collated_examples/mitgcm/README.md | 39 + .../src/collated_examples/ocean3box/README.md | 31 + docs/src/index.md | 2 +- docs/src/paleo_references.bib | 136 +++- .../PTBClarkson2014/PALEO_examples_PTB3box.jl | 16 +- .../PALEO_examples_PTB3box_cfg.yaml | 0 examples/PTBClarkson2014/README.md | 9 + .../PTBClarkson2014/config_PTB3box_expts.jl | 10 +- .../PTBClarkson2014/runtests.jl | 11 +- examples/Project.toml | 1 + examples/atmreservoirreaction.jl | 112 +++ examples/mitgcm/Insolation.jl | 141 ++++ examples/mitgcm/MITgcm_2deg8_COPDOM.yaml | 710 ++++++++++++++++++ examples/mitgcm/MITgcm_2deg8_PO4MMbase.jl | 105 +++ examples/mitgcm/MITgcm_2deg8_PO4MMcarbSCH4.jl | 102 +++ examples/mitgcm/MITgcm_2deg8_abiotic.jl | 99 +++ examples/mitgcm/MITgcm_2deg8_abiotic.yaml | 157 ++++ examples/mitgcm/MITgcm_2deg8_test.jl | 65 ++ examples/mitgcm/MITgcm_ECCO_COPDOM.yaml | 643 ++++++++++++++++ examples/mitgcm/MITgcm_ECCO_abiotic.jl | 93 +++ examples/mitgcm/MITgcm_ECCO_abiotic_test.jl | 80 ++ examples/mitgcm/README.md | 39 + examples/mitgcm/config_mitgcm_expts.jl | 64 ++ examples/mitgcm/plot_mitgcm.jl | 137 ++++ examples/mitgcm/runtests.jl | 60 ++ examples/ocean3box/PALEO_examples_oaonly.jl | 23 +- .../PALEO_examples_oaonly_abiotic.jl | 28 +- .../ocean3box/PALEO_examples_oaopencarb.jl | 23 +- .../PALEO_examples_ocean3box_cfg.yaml | 25 +- examples/ocean3box/README.md | 29 +- examples/ocean3box/config_ocean3box_expts.jl | 38 +- examples/ocean3box/plot_ocean_3box.jl | 42 -- examples/ocean3box/runtests.jl | 53 +- .../PALEO_examples_transport_advect.jl | 10 +- .../PALEO_examples_transport_diffuse.jl | 10 +- src/PALEOocean.jl | 1 + src/ocean/VerticalTransport.jl | 2 +- src/oceanfloor/Burial.jl | 565 ++++++++++++++ src/oceanfloor/Oceanfloor.jl | 8 + src/oceansurface/AirSeaExchange.jl | 2 +- test/configocean3box.yaml | 444 +++++++++++ test/configoceanTMM.yaml | 109 +++ test/runocean3boxtests.jl | 348 +++++++++ test/runoceanMITgcmtests.jl | 109 +++ test/runtests.jl | 19 +- 49 files changed, 4602 insertions(+), 203 deletions(-) create mode 100644 docs/src/collated_examples/PTBClarkson2014/README.md create mode 100644 docs/src/collated_examples/mitgcm/README.md create mode 100644 docs/src/collated_examples/ocean3box/README.md rename examples/{ocean3box => }/PTBClarkson2014/PALEO_examples_PTB3box.jl (84%) rename examples/{ocean3box => }/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml (100%) create mode 100644 examples/PTBClarkson2014/README.md rename examples/{ocean3box => }/PTBClarkson2014/config_PTB3box_expts.jl (96%) rename examples/{ocean3box => }/PTBClarkson2014/runtests.jl (85%) create mode 100644 examples/atmreservoirreaction.jl create mode 100644 examples/mitgcm/Insolation.jl create mode 100644 examples/mitgcm/MITgcm_2deg8_COPDOM.yaml create mode 100644 examples/mitgcm/MITgcm_2deg8_PO4MMbase.jl create mode 100644 examples/mitgcm/MITgcm_2deg8_PO4MMcarbSCH4.jl create mode 100644 examples/mitgcm/MITgcm_2deg8_abiotic.jl create mode 100644 examples/mitgcm/MITgcm_2deg8_abiotic.yaml create mode 100644 examples/mitgcm/MITgcm_2deg8_test.jl create mode 100644 examples/mitgcm/MITgcm_ECCO_COPDOM.yaml create mode 100644 examples/mitgcm/MITgcm_ECCO_abiotic.jl create mode 100644 examples/mitgcm/MITgcm_ECCO_abiotic_test.jl create mode 100644 examples/mitgcm/README.md create mode 100644 examples/mitgcm/config_mitgcm_expts.jl create mode 100644 examples/mitgcm/plot_mitgcm.jl create mode 100644 examples/mitgcm/runtests.jl create mode 100644 src/oceanfloor/Burial.jl create mode 100644 src/oceanfloor/Oceanfloor.jl create mode 100644 test/configocean3box.yaml create mode 100644 test/configoceanTMM.yaml create mode 100644 test/runocean3boxtests.jl create mode 100644 test/runoceanMITgcmtests.jl diff --git a/Project.toml b/Project.toml index a422f6d..b84a388 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PALEOocean" uuid = "41de04b1-2efd-44ae-92ae-39d71a4fd99b" authors = ["sd336 "] -version = "0.3.0" +version = "0.4.0" [deps] Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" @@ -15,26 +15,38 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" SIMD = "fdea26ae-647d-5447-a871-4b548cad5224" SnoopPrecompile = "66db9d55-30c0-4569-8b51-7e840670fc0c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" TestEnv = "1e6cf692-eddd-4d53-88a5-2d735e33781b" [compat] +DataFrames = "1.0" +DiffEqBase = "6.0" Documenter = "0.27" Interpolations = "0.13, 0.14" MAT = "0.10.4" PALEOaqchem = "0.3.1" PALEOboxes = "0.20.4, 0.21" +PALEOcopse = "0.4.5" PALEOmodel = "0.15.8" +Plots = "1.38" Preferences = "1.3" SIMD = "3.4" SnoopPrecompile = "1.0" +SpecialFunctions = "1.0, 2.0" +Sundials = "4.0" TestEnv = "1.0" julia = "1.6" [extras] +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +PALEOcopse = "4a6ed817-0e58-48c6-8452-9e9afc8cb508" PALEOmodel = "bf7b4fbe-ccb1-42c5-83c2-e6e9378b660c" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Documenter", "Logging", "PALEOmodel", "Test"] +test = ["DataFrames", "DiffEqBase", "Documenter", "Logging", "PALEOcopse", "PALEOmodel", "Plots", "Sundials", "Test"] diff --git a/README.md b/README.md index dfb10d7..ab9903e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Ocean components for the PALEO biogeochemical model. -**NB: work-in-progress - this repo contains an initial minimal example only to test infrastructure.** +**NB: work-in-progress - this repo contains initial minimal examples only to test infrastructure.** ## Installation and running a minimal example diff --git a/docs/src/PALEOocean Reactions.md b/docs/src/PALEOocean Reactions.md index 7d69d18..c51717e 100644 --- a/docs/src/PALEOocean Reactions.md +++ b/docs/src/PALEOocean Reactions.md @@ -1,6 +1,6 @@ # PALEOocean Reactions -## Ocean geometry and transport +## Ocean geometry and circulation transport ```@meta CurrentModule = PALEOocean.Ocean ``` @@ -12,25 +12,25 @@ OceanTransport6box.ReactionOceanTransport6box OceanTransportTMM.ReactionOceanTransportTMM ``` -### Vertical Transport +## Vertical Transport ```@docs +VerticalTransport.ReactionLightColumn VerticalTransport.ReactionExportDirect VerticalTransport.ReactionExportDirectColumn VerticalTransport.ReactionSinkFloat ``` -### Production +## Biological Production ```@docs BioProd.ReactionBioProdPrest BioProd.ReactionBioProdMMPop ``` -## Ocean surface +## Ocean surface air-sea flux ```@meta CurrentModule = PALEOocean.Oceansurface ``` -### Air-sea flux ```@docs AirSeaExchange.ReactionAirSea AirSeaExchange.ReactionAirSeaO2 @@ -38,3 +38,21 @@ AirSeaExchange.ReactionAirSeaCO2 AirSeaExchange.ReactionAirSeaCH4 AirSeaExchange.ReactionAirSeaFixedSolubility ``` + +## Ocean floor burial + +```@meta +CurrentModule = PALEOocean.Oceanfloor +``` + + +### Carbonate burial +```@docs +Burial.ReactionShelfCarb +Burial.ReactionBurialEffCarb +``` + +### Organic carbon and phosphorus burial +```@docs +Burial.ReactionBurialEffCorgP +``` \ No newline at end of file diff --git a/docs/src/collated_examples/PTBClarkson2014/README.md b/docs/src/collated_examples/PTBClarkson2014/README.md new file mode 100644 index 0000000..4c8fb25 --- /dev/null +++ b/docs/src/collated_examples/PTBClarkson2014/README.md @@ -0,0 +1,9 @@ +# PTB land-atmosphere-ocean model (Clarkson 2015) + +3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model, coupled to the COPSE land surface and sediment/crust. + +Combination of forcings are as used in [Clarkson2015](@cite). See paper SI for model description. + +**NB: this is an untested PALEO configuration intended to reproduce the Matlab code from the original paper.** + + julia> include("PALEO_examples_PTB3box.jl") \ No newline at end of file diff --git a/docs/src/collated_examples/mitgcm/README.md b/docs/src/collated_examples/mitgcm/README.md new file mode 100644 index 0000000..5b0913a --- /dev/null +++ b/docs/src/collated_examples/mitgcm/README.md @@ -0,0 +1,39 @@ +# MITgcm transport matrix ocean-only examples + +GCM ocean transport test cases using transport matrices in format defined by [Khatiwala2007](@cite) + +Examples require a download of Samar Khatiwala's TMM files (for MITgcm, UVic models) as described in https://github.com/samarkhatiwala/tmm where TM files are from http://kelvin.earth.ox.ac.uk/spk/Research/TMM/TransportMatrixConfigs/. The TMMDir key in `examples\LocalPreferences.toml` should then be set to the folder location on the local machine. + +Default configurations are set to run for ~10 model yr, single core, single fixed timestep. See comments in .jl files to change run time and enable threading and split-timestep solver (with fast timestep for biogeochemistry and vertical transport). + +Approximate model CPU times below are for a single laptop core (a CPU i5-6300U from ~2015). + +## 2.8 degree O2 only abiotic + +Minimal test case for ocean transport and air-sea exchange. + +Tracers: atmosphere O2, ocean O2 + + julia> include("MITgcm_2deg8_abiotic.jl") + +Wallclock time: 10.3 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) + +## 2.8 degree P, O2 + +Minimal test case for biotic ocean. + +Tracers: atmosphere O2, ocean O2, P, DOP + + julia> include("MITgcm_2deg8_PO4MMbase.jl") + +Wallclock time: 10.0 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) + +## 2.8 degree P, O2, S, DIC/TAlk + +Minimal test case for a biotic ocean with carbonate chemistry, SO4/H2S and CH4. + +Tracers: atmosphere O2, CO2(x2), ocean O2, P, DOC(x2), H2S(x2), SO4(x2), CH4(x2), DIC(x2), TAlk (13 ocean tracers) + + julia> include("MITgcm_2deg8_PO4MMcarbSCH4.jl") + +Wallclock time: 27.2 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) \ No newline at end of file diff --git a/docs/src/collated_examples/ocean3box/README.md b/docs/src/collated_examples/ocean3box/README.md new file mode 100644 index 0000000..21932b4 --- /dev/null +++ b/docs/src/collated_examples/ocean3box/README.md @@ -0,0 +1,31 @@ +# 3 Box Ocean Examples + +These examples demonstrate the 3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model, +standalone and coupled to the COPSE land surface and sediment/crust (as used in [Clarkson2015](@cite)). + +## Abiotic CO2/DIC only atmosphere-ocean + + julia> include("PALEO_examples_oaonly_abiotic.jl") + +Abiotic atmosphere-ocean with atmosphere CO2, ocean DIC, TAlk. Test case cf Sarmiento & Toggweiler (2007) book, Fig 10.4, p436-7 + +Commented-out options in file to set k_piston to show effect of default/fast/slow air-sea exchange rates + +## Biotic atmosphere-ocean (no weathering or burial) + + julia> include("PALEO_examples_oaonly.jl") + +Biotic atmosphere-ocean with atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk (no weathering or burial). + +Use in conjunction with expt='killbio' (disables production at t=0 yr) to +demonstrate effect of biological pump. + +## Open atmosphere-ocean with silicate/carbonate weathering and burial + + julia> include("PALEO_examples_oaopencarb.jl") + +Biotic atmosphere-ocean with atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk + +Open atm-ocean carbonate system, with carbonate/silicate weathering input, degassing input, and carbonate burial output + +Closed ocean organic carbon, sulphur systems (no burial) diff --git a/docs/src/index.md b/docs/src/index.md index cf94fb3..db1df89 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,6 +1,6 @@ # PALEOocean.jl documentation -## Installation and running the model +## Installation and running the examples ### Installation diff --git a/docs/src/paleo_references.bib b/docs/src/paleo_references.bib index 474ad5b..9f350e8 100644 --- a/docs/src/paleo_references.bib +++ b/docs/src/paleo_references.bib @@ -1,28 +1,114 @@ -@article{Bergman2004, -author = {Bergman, N. M. and Lenton, Timothy M and Watson, Andrew J}, -doi = {10.2475/ajs.304.5.397}, -issn = {0002-9599}, -journal = {American Journal of Science}, -month = {may}, -number = {5}, -pages = {397--437}, -title = {{COPSE: A new model of biogeochemical cycling over Phanerozoic time}}, -url = {http://www.ajsonline.org/cgi/doi/10.2475/ajs.304.5.397}, -volume = {304}, -year = {2004} -} - - -@article{Lenton2018, -author = {Lenton, Timothy M. and Daines, Stuart J and Mills, Benjamin J.W.}, -doi = {10.1016/j.earscirev.2017.12.004}, -journal = {Earth-Science Reviews}, +@article{Caldeira1993, +author = {Caldeira, Ken and Rampino, Michael R}, +doi = {10.1029/93PA01163}, +issn = {08838305}, +journal = {Paleoceanography}, +month = {aug}, +number = {4}, +pages = {515--525}, +title = {{Aftermath of the end-Cretaceous mass extinction: Possible biogeochemical stabilization of the carbon cycle and climate}}, +url = {http://doi.wiley.com/10.1029/93PA01163}, +volume = {8}, +year = {1993} +} + +@article{Canfield2006, +author = {Canfield, Donald E}, +doi = {10.1016/j.gca.2006.07.023}, +journal = {Geochimica et Cosmochimica Acta}, +month = {dec}, +number = {23}, +pages = {5753--5765}, +title = {{Models of oxic respiration, denitrification and sulfate reduction in zones of coastal upwelling}}, +url = {http://linkinghub.elsevier.com/retrieve/pii/S0016703706019120}, +volume = {70}, +year = {2006} +} + +@article{Clarkson2015, +author = {Clarkson, M. O. and Kasemann, S. A. and Wood, R. A. and Lenton, T. M. and Daines, Stuart J and Richoz, S. and Ohnemueller, F. and Meixner, A. and Poulton, Simon W and Tipper, E. T.}, +doi = {10.1126/science.aaa0193}, +journal = {Science}, +month = {apr}, +number = {6231}, +pages = {229--232}, +title = {{Ocean acidification and the Permo-Triassic mass extinction}}, +url = {https://www.sciencemag.org/lookup/doi/10.1126/science.aaa0193}, +volume = {348}, +year = {2015} +} + +@article{Daines2016, +author = {Daines, Stuart J and Lenton, Timothy M}, +doi = {10.1016/j.epsl.2015.11.021}, +journal = {Earth and Planetary Science Letters}, +month = {jan}, +pages = {42--51}, +title = {{The effect of widespread early aerobic marine ecosystems on methane cycling and the Great Oxidation}}, +url = {http://linkinghub.elsevier.com/retrieve/pii/S0012821X15007256}, +volume = {434}, +year = {2016} +} + +@article{Hotinski2000, +author = {Hotinski, Roberta M and Kump, Lee R and Najjar, Raymond G}, +doi = {10.1029/1999PA000408}, +journal = {Paleoceanography}, +number = {3}, +pages = {267}, +title = {{Opening Pandora's Box: The impact of open system modeling on interpretations of anoxia}}, +url = {http://www.agu.org/pubs/crossref/2000/1999PA000408.shtml}, +volume = {15}, +year = {2000} +} + +@article{Khatiwala2007, +author = {Khatiwala, Samar}, +doi = {10.1029/2007GB002923}, +journal = {Global Biogeochemical Cycles}, +month = {jul}, +number = {3}, +pages = {1--14}, +title = {{A computational framework for simulation of biogeochemical tracers in the ocean}}, +url = {http://www.agu.org/pubs/crossref/2007/2007GB002923.shtml}, +volume = {21}, +year = {2007} +} + +@article{Sarmiento1984, +author = {Sarmiento, Jorge L and Toggweiler, J. R.}, +doi = {10.1038/308621a0}, +journal = {Nature}, +month = {apr}, +number = {5960}, +pages = {621--624}, +title = {{A new model for the role of the oceans in determining atmospheric P CO2}}, +url = {http://www.nature.com/articles/308621a0}, +volume = {308}, +year = {1984} +} + + +@incollection{Toggweiler1985, +author = {Toggweiler, J. R. and Sarmiento, Jorge L}, +booktitle = {The Carbon cycle and atmospheric CO2: Natural variations Archean to present.}, +doi = {10.1029/GM032p0163}, month = {mar}, -pages = {1--28}, -publisher = {Elsevier B.V}, -title = {{COPSE reloaded: An improved model of biogeochemical cycling over Phanerozoic time}}, -url = {http://linkinghub.elsevier.com/retrieve/pii/S0012825217304117 https://linkinghub.elsevier.com/retrieve/pii/S0012825217304117}, -volume = {178}, -year = {2018} +pages = {163--184}, +publisher = {Wiley}, +title = {{Glacial to Interglacial Changes in Atmospheric Carbon Dioxide: The Critical Role of Ocean Surface Water in High Latitudes}}, +url = {http://doi.wiley.com/10.1029/GM032p0163}, +volume = {1}, +year = {1985} } + +@inproceedings{Watson1995, +author = {Watson, Andrew J}, +booktitle = {Upwelling in the ocean: Modern processes and ancient records}, +editor = {Summerhayes, CP}, +pages = {321--336}, +publisher = {Wiley}, +title = {{Are upwelling zones sources or sinks of CO2?}}, +year = {1995} +} diff --git a/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl b/examples/PTBClarkson2014/PALEO_examples_PTB3box.jl similarity index 84% rename from examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl rename to examples/PTBClarkson2014/PALEO_examples_PTB3box.jl index e13f7f4..b6d7085 100644 --- a/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box.jl +++ b/examples/PTBClarkson2014/PALEO_examples_PTB3box.jl @@ -14,7 +14,7 @@ import PALEOcopse global_logger(ConsoleLogger(stderr,Logging.Info)) include("config_PTB3box_expts.jl") -include("../plot_ocean_3box.jl") +include("../ocean3box/plot_ocean_3box.jl") # model = config_PTB3box_expts("Co2HOmLWCpp", ["baseline"]); tspan=(-260e6,-240e6) # tspan=(-10e6,10e6) # model = config_PTB3box_expts("Co2LOmHWC4pp", ["baseline"]); tspan=(-260e6,-240e6) # tspan=(-10e6,10e6) @@ -34,14 +34,14 @@ PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , no println("initial_state", initial_state) println("initial_deriv", initial_deriv) -run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) # With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling # (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) # Solve as DAE with sparse Jacobian PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -65,14 +65,14 @@ gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (xlim=(-252.15e6, -251.80e6), xflip=true, legend_background_color=nothing, )) -plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "S", "P"], pager=pager) +plot_totals(paleorun.output; species=["C", "TAlk", "TAlkerror", "S", "P"], pager=pager) plot_ocean_tracers( - run.output; + paleorun.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "P_conc", "SO4_delta", "H2S_delta", "OmegaAR", "DIC_delta", "pHtot", "BOH4_delta"], pager=pager ) -plot_oaonly_abiotic(run.output; pager=pager) -plot_PTB3box(run.output; pager=pager) -plot_carb_open(run.output; pager=pager) +plot_oaonly_abiotic(paleorun.output; pager=pager) +plot_PTB3box(paleorun.output; pager=pager) +plot_carb_open(paleorun.output; pager=pager) pager(:newpage) # flush output diff --git a/examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml b/examples/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml similarity index 100% rename from examples/ocean3box/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml rename to examples/PTBClarkson2014/PALEO_examples_PTB3box_cfg.yaml diff --git a/examples/PTBClarkson2014/README.md b/examples/PTBClarkson2014/README.md new file mode 100644 index 0000000..4c8fb25 --- /dev/null +++ b/examples/PTBClarkson2014/README.md @@ -0,0 +1,9 @@ +# PTB land-atmosphere-ocean model (Clarkson 2015) + +3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model, coupled to the COPSE land surface and sediment/crust. + +Combination of forcings are as used in [Clarkson2015](@cite). See paper SI for model description. + +**NB: this is an untested PALEO configuration intended to reproduce the Matlab code from the original paper.** + + julia> include("PALEO_examples_PTB3box.jl") \ No newline at end of file diff --git a/examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl b/examples/PTBClarkson2014/config_PTB3box_expts.jl similarity index 96% rename from examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl rename to examples/PTBClarkson2014/config_PTB3box_expts.jl index 94643f6..55cc17b 100644 --- a/examples/ocean3box/PTBClarkson2014/config_PTB3box_expts.jl +++ b/examples/PTBClarkson2014/config_PTB3box_expts.jl @@ -1,15 +1,9 @@ +include("../atmreservoirreaction.jl") # temporary solution to make ReactionReservoirAtm available "test cases and examples for 3 box ocean" function config_PTB3box_expts(baseconfig, expts) - if baseconfig=="oaopencarb" - # Open atmosphere-ocean with silicate carbonate weathering input and carbonate burial - - model = PB.create_model_from_config( - joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base" - ) - - elseif baseconfig=="Co2HOmLWCpp" + if baseconfig=="Co2HOmLWCpp" # Clarkson (2014) CO2Hi model = PB.create_model_from_config( diff --git a/examples/ocean3box/PTBClarkson2014/runtests.jl b/examples/PTBClarkson2014/runtests.jl similarity index 85% rename from examples/ocean3box/PTBClarkson2014/runtests.jl rename to examples/PTBClarkson2014/runtests.jl index f2c2576..7becac6 100644 --- a/examples/ocean3box/PTBClarkson2014/runtests.jl +++ b/examples/PTBClarkson2014/runtests.jl @@ -6,7 +6,8 @@ import DataFrames import PALEOboxes as PB -import PALEOreactions +import PALEOcopse +import PALEOocean import PALEOmodel @@ -25,10 +26,10 @@ skipped_testsets = [ tspan=(-260e6, -251.88e6) # stop just after second C pulse # (-260e6,-240e6) initial_state, modeldata = PALEOmodel.initialize!(model) - run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) sol = PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -45,7 +46,7 @@ skipped_testsets = [ ] for (domname, varname, propertyname, rtol) in conschecks startval, endval = PB.get_property( - PB.get_data(run.output, domname*"."*varname), + PB.get_data(paleorun.output, domname*"."*varname), propertyname=propertyname )[[1, end]] println(" check $domname.$varname $startval $endval $rtol") @@ -60,7 +61,7 @@ skipped_testsets = [ ("ocean", "P_total", 6.8448e15, 1e-3), ] for (domname, varname, checkval, rtol) in checkvals - outputval = PB.get_data(run.output, domname*"."*varname)[end] + outputval = PB.get_data(paleorun.output, domname*"."*varname)[end] println(" check $domname.$varname $outputval $checkval $rtol") @test isapprox(outputval, checkval, rtol=rtol) end diff --git a/examples/Project.toml b/examples/Project.toml index fbfc9a3..dcd9124 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -13,6 +13,7 @@ PALEOocean = "41de04b1-2efd-44ae-92ae-39d71a4fd99b" PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" diff --git a/examples/atmreservoirreaction.jl b/examples/atmreservoirreaction.jl new file mode 100644 index 0000000..c92c4c0 --- /dev/null +++ b/examples/atmreservoirreaction.jl @@ -0,0 +1,112 @@ +""" + temporary solution to make ReactionReservoirAtm available to PALEOocean examples + +TODO move to PALEOboxes and remove from PALEOdev +""" +module AtmReservoirTODO + +import PALEOboxes as PB +using PALEOboxes.DocStrings + +""" + ReactionReservoirAtm + +A single scalar atmosphere reservoir (state variable). + +Creates a state Variable `R` (units mol, with attribute `vfunction=VF_StateExplicit`) and +(if parameter `const=false`) corresponding source-sink flux `R_sms` (units mol yr-1, with attribute `vfunction=VF_Deriv`). + +Two associated Variables are also created: +- `pRatm`, partial pressure in units of bar or atm. This is calculated by dividing `R` (mol) by + parameter `moles1atm` (which has a default value for global modern Earth atmosphere). +- `pRPAL`, value normalized eg to a present-day modern Earth value. Calculated by dividing `R` (mol) by attribute `R:norm_value`, + which should be set in the .yaml file to a representative value. + +The local name prefix `R` should then be renamed using `variable_links:` in the configuration file. + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionReservoirAtm{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParType(PB.AbstractData, "field_data", PB.ScalarData, + allowed_values=PB.IsotopeTypes, + description="disable / enable isotopes and specify isotope type"), + PB.ParBool("const", false, + description="true to provide constant value with no _sms Variable"), + PB.ParDouble("moles1atm", PB.Constants.k_moles1atm, units="mol", + description="moles for a pressure of 1 Earth atm. Default value $(PB.Constants.k_moles1atm) for global modern Earth atmosphere") + ) + + norm_value::Float64 = NaN +end + +function PB.register_methods!(rj::ReactionReservoirAtm) + + @info "register_methods! ReactionReservoirAtm $(PB.fullname(rj)) field_data=$(rj.pars.field_data[])" + PB.setfrozen!(rj.pars.field_data) + + if rj.pars.const[] + R = PB.VarPropScalar( "R", "mol", "scalar constant reservoir", attributes=(:field_data=>rj.pars.field_data[],)) + R_sms = PB.VarTarget( "R_sms", "mol yr-1", "constant reservoir source-sinks", attributes=(:field_data=>rj.pars.field_data[],)) + else + R = PB.VarStateExplicitScalar("R", "mol", "scalar reservoir", attributes=(:field_data=>rj.pars.field_data[],)) + R_sms = PB.VarDerivScalar( "R_sms", "mol yr-1", "scalar reservoir source-sinks", attributes=(:field_data=>rj.pars.field_data[],)) + end + PB.setfrozen!(rj.pars.const) + + # sms variable not used by us, but must appear in a method to be linked and created + PB.add_method_do_nothing!(rj, [R_sms]) + + vars = [ + R, + PB.VarPropScalar( "pRnorm", "", "scalar atmosphere reservoir normalized eg to present-day value"), + PB.VarPropScalar( "pRatm", "atm", "scalar atmosphere reservoir partial pressure"), + ] + + if rj.pars.field_data[] <: PB.AbstractIsotopeScalar + push!(vars, + PB.VarPropScalar( "R_delta", "per mil", "scalar atmosphere reservoir isotope delta")) + end + + # callback function to store Variable norm during setup + function setup_callback(m, attribute_value, v, vdata) + v.localname == "R" || error("setup_callback unexpected Variable $(PB.fullname(v))") + if attribute_value == :norm_value + m.reaction.norm_value = PB.value_ad(PB.get_total(vdata[])) + end + return nothing + end + # set filterfn to force setup even if R is constant, not a state Variable + PB.add_method_setup_initialvalue_vars_default!(rj, [R], filterfn = v->true, setup_callback=setup_callback) + + PB.add_method_do!( + rj, + do_reservoir_atm, + (PB.VarList_namedtuple(vars), ), + ) + + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +function do_reservoir_atm(m::PB.AbstractReactionMethod, pars, (vars, ), cr::PB.AbstractCellRange, deltat) + rj = m.reaction + + vars.pRnorm[] = PB.get_total(vars.R[])/rj.norm_value + vars.pRatm[] = PB.get_total(vars.R[])/pars.moles1atm[] + + if hasfield(typeof(vars), :R_delta) + vars.R_delta[] = PB.get_delta(vars.R[]) + end + + return nothing +end + +end # module \ No newline at end of file diff --git a/examples/mitgcm/Insolation.jl b/examples/mitgcm/Insolation.jl new file mode 100644 index 0000000..6d5079f --- /dev/null +++ b/examples/mitgcm/Insolation.jl @@ -0,0 +1,141 @@ +module Insolation + + +import PALEOboxes as PB +using PALEOboxes.DocStrings + + +""" + ReactionForceInsolation + +Calculate time and latitude dependent daily mean modern Earth surface solar insolation. + +Daily mean photosynthetically-active surface insolation + + `insolation` = TOA flux * (1 - `albedo`) * `parfrac` + +See [`insolMITgcmDIC`](@ref) for details. + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionForceInsolation{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParDouble("albedo", 0.6, + description="mean planetary albedo"), + PB.ParDouble("parfrac", 1.0, + description="fraction of radiation that is photosynthetically active"), + PB.ParDoubleVec("latitude", Float64[], units="degrees N", + description="if non-empty, override grid latitude and set explicitly for each surface cell"), + ) +end + +function PB.register_methods!(rj::ReactionForceInsolation) + + vars = [ + PB.VarDepScalar("global.tforce", "yr", "historical time at which to apply forcings, present = 0 yr"), + PB.VarProp("insolation", "W m-2", "daily mean surface insolation"), + ] + + PB.add_method_do!( + rj, + do_force_insolation, + (PB.VarList_namedtuple(vars),), + p=rj.domain.grid, # add as context so fully typed + ) +end + + +function PB.check_configuration(rj::ReactionForceInsolation, model::PB.Model) + configok = true + if !isempty(rj.pars.latitude) && !isnothing(rj.domain.grid) + if rj.domain.grid.ncells != length(rj.pars.latitude) + @warn "check_configuration $(PB.fullname(rj)) length(latitude) parameter $(length(rj.pars.latitude)) != grid.ncells $(grid.ncells)" + configok = false + end + end + return configok +end + +function do_force_insolation(m::PB.ReactionMethod, pars, (vars, ), cellrange::PB.AbstractCellRange, deltat) + + grid = m.p + + tforce = PB.value_ad(vars.tforce[]) + + @inbounds for i in cellrange.indices + if isempty(pars.latitude) + lat = PB.Grids.get_lat(grid, i) + else + lat = pars.latitude[i] + end + vars.insolation[i] = insolMITgcmDIC(tforce, lat, albedo=pars.albedo[], parfrac=pars.parfrac[]) + end + + return nothing +end + +""" + insolMITgcmDIC(Timeyr,latdeg; albedo=0.6, solar=1360.0, parfrac=1.0) -> sfac + +MITgcm DIC package insol function directly translated from fortran. +Similar to [Brock1981](@cite). + +NB: there are three normalization constants here: `solar`, `albedo`, `parfrac` to define +top-of-atmosphere flux (from astronomical formulae) -> a crude approx to ground level flux (taking into account clouds etc) -> photosynthetic PAR flux + + C !DESCRIPTION: + C find light as function of date and latitude + C based on paltridge and parson + +# Arguments: +- `Timeyr`: yr, model time, NB: year assumed to start in winter +- `latdeg`: deg, latitudes +- `albedo`: planetary albedo (ie correct for top-of-atmosphere to ground-level, clouds etc) +- `solar`: W m-2 solar constant +- `parfrac`: photosynthetically active fraction + +# Returns: +- `sfac`: daily average photosynthetically active solar radiation just below surface +""" +function insolMITgcmDIC(Timeyr,latdeg; albedo=0.6, solar=1360.0, parfrac=1.0) + + # C find day (****NOTE for year starting in winter*****) + dayfrac= Timeyr - floor(Timeyr) # fraction of year + yday = 2*π*dayfrac # convert to radians + delta = (0.006918 + -(0.399912 *cos(yday)) # cosine zenith angle + +(0.070257 *sin(yday)) # (paltridge+platt) + -(0.006758 *cos(2*yday)) + +(0.000907 *sin(2*yday)) + -(0.002697 *cos(3*yday)) + +(0.001480 *sin(3*yday)) ) + + # C latitude in radians + lat=latdeg*2*π/360 + + sun1 = -sin(delta)/cos(delta) * sin(lat)/cos(lat) + sun1 = max(sun1,-0.999) + sun1 = min(sun1, 0.999 ) + dayhrs = abs(acos(sun1)) + cosz = ( sin(delta)*sin(lat)+ # average zenith angle + (cos(delta)*cos(lat).*sin(dayhrs)./dayhrs) ) + cosz = max(cosz, 5e-3) + frac = dayhrs/π # fraction of daylight in day + # C daily average photosynthetically active solar radiation just below surface + fluxi = solar*(1-albedo)*cosz*frac*parfrac + + # C convert to sfac + sfac = max(1e-5,fluxi) + + return sfac +end + + + +end \ No newline at end of file diff --git a/examples/mitgcm/MITgcm_2deg8_COPDOM.yaml b/examples/mitgcm/MITgcm_2deg8_COPDOM.yaml new file mode 100644 index 0000000..1c9f426 --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_COPDOM.yaml @@ -0,0 +1,710 @@ +# MITgcm 2.8deg P, O atmosphere-ocean test configuration +PO4MMbase: + parameters: + CIsotope: ScalarData + SIsotope: ScalarData + domains: + global: + # scalar domain + + reactions: + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2_total, -138.0*ocean.DOP_total] # DOP O2eq = 106.0 + 2 * 16.0 + variable_links: + sum: total_O2 + + total_P: + class: ReactionSum + parameters: + vars_to_add: [ocean.P_total, ocean.DOP_total] + variable_links: + sum: total_P + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2"] + + fluxOceanfloor: + reactions: + particulatefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] # fluxlist_BioParticulate + + solutefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["P", "O2"] #, "DIC::CIsotope", "TAlk", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] # fluxlist_Solute + + atm: + + + reactions: + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + reservoir_P: + class: ReactionReservoirTotal + operatorID: [1, 2] + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration m-3 (1027 kg m-3 * 2.15e-6 mol/kg-sw) + + reservoir_O2: + class: ReactionReservoirTotal + operatorID: [1, 2] + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 0.2054 # concentration m-3 (1027 kg m-3 * 200e-6 mol/kg-sw) + + reservoir_DOP: + class: ReactionReservoirTotal + operatorID: [1, 2] + variable_links: + R*: DOP* + variable_attributes: + R:initial_value: 0.0e-3 # concentration m-3 + + + transportMITgcm: + class: ReactionOceanTransportTMM + operatorID: [1, 2] + parameters: + base_path: $TMMDir$/MITgcm_2.8deg + TMfpsize: 64 + pack_chunk_width: 4 + + light: + class: ReactionLightColumn + parameters: + background_opacity: 0.02 + + bioprod: + class: ReactionBioProdMMPop + operatorID: [2] + parameters: + depthlimit: -119.0 # first two layers only (top of layer 3 is -120.0m) + + rCorgPO4: 106.0 + rNPO4: 16.0 + rCcarbCorg: 0.25 + rCcarbCorg_fixed: true + + nuDOM: 0.67 + + k_poptype: Constant + k_uPO4: 2.0e-3 # mol m-3 yr-1 + + k_nuttype: PO4MM + k_KPO4: 0.5e-3 # mol m-3 0.5 uM + + k_lightlim: MM + k_Ic: 30.0 + k_Irel: 1.0 + + variable_links: + partprod_*: export_* + domprod_P: DOP_sms + + dopdecay: + class: ReactionParticleDecay + operatorID: [2] + parameters: + decay_timescale: 0.5 # yr + variable_links: + Particle*: DOP* + decayflux: DOP_decay + dopdecaycomponents: + class: ReactionFluxToComponents + operatorID: [2] + parameters: + outputflux_prefix: remin_ + outputflux_names: ["Corg", "N", "P"] + outputflux_stoich: [106.0, 16.0, 1.0] # must match bioprod stoich + variable_links: + inputflux: DOP_decay + + biopump: + class: ReactionExportDirectColumn + operatorID: [2] + parameters: + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] + transportfloor: true + exportfunction: Martin + martin_rovera: 0.858 + martin_depthmin: 120.0 # base of second layer + + reminocean: + class: ReactionReminO2 + operatorID: [2] + parameters: + + variable_links: + soluteflux_*: "*_sms" + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_insol + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston_fixed: false + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + # output_CO2: ocean.oceansurface.DIC_sms + oceanfloor: + reactions: + reminoceanfloor: + class: ReactionReminO2 + operatorID: [2] + parameters: + + variable_links: + remin*: particulateflux* + soluteflux_*: fluxOceanfloor.soluteflux_* + + transfer_particulatefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + variable_links: + + transfer_solutefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + +############################################################ +# MITgcm 2.8deg P, O with Cinorg, S, CH4 +############################################################ +PO4MMcarbSCH4: + parameters: + CIsotope: IsotopeLinear # ScalarData + SIsotope: IsotopeLinear + + domains: + global: + # scalar domain + + reactions: + total_O2eq: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2_total, -2*ocean.CH4_total, -1*ocean.DOC_total, -2*ocean.H2S_total] + component_to_add: 1 # we just want the first component (total) from isotope variables + variable_links: + sum: total_O2eq + + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC_total, ocean.DOC_total, ocean.CH4_total] + variable_links: + sum: total_C + + total_S: + class: ReactionSum + parameters: + vars_to_add: [ocean.SO4_total, ocean.H2S_total] + variable_links: + sum: total_S + + total_P: + class: ReactionSum + parameters: + vars_to_add: [ocean.P_total, 0.009434*ocean.DOC_total] + component_to_add: 1 # we just want the first component (total) from isotope variables + variable_links: + sum: total_P + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2", "CO2::CIsotope"] + + fluxOceanfloor: + reactions: + particulatefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] # fluxlist_BioParticulate + + solutefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["P", "O2", "DIC::CIsotope", "TAlk", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] # fluxlist_Solute + + atm: + + + reactions: + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration m-3 (1027 kg m-3 * 2.15e-6 mol/kg-sw) + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 0.2054 # concentration m-3 (1027 kg m-3 * 200e-6 mol/kg-sw) + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2291.74e-3 # 2231.486e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_DOC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DOC* + variable_attributes: + R:initial_value: 1.0e-6 # concentration m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2426.8e-3 # 2363e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_SO4: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: SO4* + variable_attributes: + R:initial_value: 28756.0e-3 # concentration mol m-3 ~ 28e-3 mol/kg * 1027 kg m-3 + R:initial_delta: 1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_H2S: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + limit_delta_conc: 1e-6 # mol m-3 to limit delta **EXPERIMENTAL** + variable_links: + R*: H2S* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_CH4: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + limit_delta_conc: 1e-6 # mol m-3 to limit delta **EXPERIMENTAL** + variable_links: + R*: CH4* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega", "H2S"] + defaultconcs: ["TF", "TB", "Ca"] + solve_pH: solve # solve for Hfree iteratively + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + simd_width: FP32P8 # FP64P4 + pHtol: 1.2e-5 # 100*eps(Float32) + variable_links: + TCi_conc: DIC_conc + TS_conc: SO4_conc + CO2: CO2_conc + pCO2: pCO2 + OmegaCA: OmegaCA + TH2S_conc: H2S_conc + + transportMITgcm: + class: ReactionOceanTransportTMM + parameters: + base_path: $TMMDir$/MITgcm_2.8deg + TMfpsize: 64 # 32 + pack_chunk_width: 4 # 8 + + light: + class: ReactionLightColumn + parameters: + background_opacity: 0.02 + + const_cisotopes: + class: ReactionScalarConst + parameters: + constnames: ["D_mccb_DIC", "D_B_mccb_mocb"] + variable_attributes: + D_mccb_DIC:initial_value: 0.0 + D_B_mccb_mocb:initial_value: 25.0 + + bioprod: + class: ReactionBioProdMMPop + parameters: + depthlimit: -60.0 # surface cells only + + rCorgPO4: 106.0 + rNPO4: 16.0 + # rCcarbCorg: 0.25 + rCcarbCorg_fixed: false + k_r0: 0.0485 + k_eta: 0.7440 + + nuDOM: 0.66 + + k_poptype: Constant + k_uPO4: 3.0e-3 # mol m-3 yr-1 3 uM yr-1 + + k_nuttype: PO4MM + k_KPO4: 0.5e-3 # mol m-3 0.5 uM + + k_lightlim: MM + k_Ic: 30.0 + k_Irel: 1.0 + + variable_links: + partprod_*: export_* + domprod_Corg: DOC_sms + + docdecay: + class: ReactionParticleDecay + parameters: + decay_timescale: 0.5 # yr + field_data: external%CIsotope + variable_links: + Particle*: DOC* + decayflux: DOC_decay + docdecaycomponents: + class: ReactionFluxToComponents + parameters: + outputflux_prefix: remin_ + outputflux_names: ["Corg::Isotope", "N", "P"] + outputflux_stoich: [1.0, 0.150094, 0.009434] # must match bioprod stoich + field_data: external%CIsotope + variable_links: + inputflux: DOC_decay + + biopumporg: + class: ReactionExportDirectColumn + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope"] + transportfloor: true + exportfunction: Martin + martin_rovera: 0.858 + martin_depthmin: 120.0 # base of second layer + + biopumpcarb: + class: ReactionExportDirectColumn + + parameters: + fluxlist: ["Ccarb::CIsotope"] + transportfloor: true + exportfunction: SumExp + input_frac: [0.55, 0.45] # 2-G model of Ridgwell & Hargreaves (2007) + sumexp_scale: [1890.5, 1e6] + + reminocean: + class: ReactionReminO2_SO4_CH4 + + parameters: + SO4reminlimit: 1000.0e-3 + + variable_links: + soluteflux_*: "*_sms" + + redox_H2S_O2: + class: ReactionRedoxH2S_O2 + + parameters: + R_H2S_O2: 3.65e2 # 3.65e3 # (mol m-3) yr-1 + + redox_CH4_O2: + class: ReactionRedoxCH4_O2 + + parameters: + R_CH4_O2: 1.0e2 # 10.0e3 # (mol m-3) yr-1 + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_insol + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston_fixed: false + + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston_fixed: false + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: + reminoceanfloor: + class: ReactionReminO2_SO4_CH4 + + parameters: + SO4reminlimit: 1000.0e-3 + + variable_links: + remin*: particulateflux* + O2_conc: ocean.oceanfloor.O2_conc + SO4_*: ocean.oceanfloor.SO4_* # conc and delta + + soluteflux_*: fluxOceanfloor.soluteflux_* + + transfer_particulatefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + variable_links: + + transfer_solutefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + diff --git a/examples/mitgcm/MITgcm_2deg8_PO4MMbase.jl b/examples/mitgcm/MITgcm_2deg8_PO4MMbase.jl new file mode 100644 index 0000000..e00a7f8 --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_PO4MMbase.jl @@ -0,0 +1,105 @@ +using Logging + +using Plots; plotlyjs(size=(750, 565)) + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") +include("plot_mitgcm.jl") + +use_threads = false +use_split = false +n_inner = 2 + +# model = config_mitgcm_expts("PO4MMbase", ""); toutputs = [0.0, 1.0, 10.0, 100.0, 995,0, 1000.0, 1999.5, 2000.0, 2999.5, 3000.0] #, 1000.0, 1000.5] + +model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_COPDOM.yaml"), "PO4MMbase" +) + +toutputs = [0.0, 0.25, 0.5, 0.75, 1.0, 10.0] +# toutputs = [0.0, 1.0, 10.0, 100.0, 995,0, 1000.0, 1999.5, 2000.0, 2999.5, 3000.0] #, 1000.0, 1000.5] + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep_imp = transportMITgcm.pars.Aimp_deltat[]/PB.Constants.k_secpyr + + +output_filename = "" +# output_filename = "MITgcm_PO4MMbase2deg8_3000yr_20210202" + +pickup_output = nothing +initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads, pickup_output=pickup_output) +pickup_output = nothing + +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +if !use_threads + if use_split + @info "using tstep_outer=$n_inner x $tstep_imp yr" + cellrange_outer = PB.create_default_cellrange(paleorun.model, operatorID=1) + cellrange_inner = PB.create_default_cellrange(paleorun.model, operatorID=2) + + @time PALEOmodel.ODEfixed.integrateSplitEuler( + paleorun, initial_state, modeldata, toutputs, tstep_imp*n_inner, n_inner, + cellrange_outer=cellrange_outer, + cellrange_inner=cellrange_inner + ) + else + @info "using tstep=$tstep_imp yr" + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs, tstep_imp) + end +else + # Threads.nthreads() == 4 || error("use_threads requires 4 threads, Threads.nthreads()=", Threads.nthreads()) + + # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:44, :, :), (45:72, :, :), (73:98, :, :), (99:128, :, :)] # 4 threads + + # cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, tiles) # vector of vectors, 1 per tile + + cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean") + + if use_split + @info "using tstep_outer=$n_inner x $tstep_imp yr" + cellranges_outer = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean", operatorID=1) + cellranges_inner = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean", operatorID=2) + @time PALEOmodel.ODEfixed.integrateSplitEulerthreads(paleorun, initial_state, modeldata, toutputs , tstep_imp*n_inner, n_inner, + cellranges_outer=cellranges_outer, cellranges_inner=cellranges_inner) + else + @info "using tstep=$tstep_imp yr" + @time PALEOmodel.ODEfixed.integrateEulerthreads(paleorun, initial_state, modeldata, cellranges, toutputs , tstep_imp) + end +end + + +isempty(output_filename) || PALEOmodel.OutputWriters.save_jld2(paleorun.output, output_filename) + + +show(PB.show_variables(paleorun.model), allrows=true) +println() + +############################ +# Plot +############################ + +# single plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# multiple plots per screen +gr(size=(1200, 900)) +pager = PALEOmodel.PlotPager((2,2), (legend_background_color=nothing, )) + +plot_forcings(paleorun.output, pager=pager) +pager(:newpage) +plot_abiotic_O2(paleorun.output, toutputs=toutputs, pager=pager) +pager(:newpage) +plot_PO4MMbase( + paleorun.output, + toutputs=toutputs, + tbioprod=[0.5, 1.0], + pager=pager, +) +pager(:newpage) diff --git a/examples/mitgcm/MITgcm_2deg8_PO4MMcarbSCH4.jl b/examples/mitgcm/MITgcm_2deg8_PO4MMcarbSCH4.jl new file mode 100644 index 0000000..4fac37d --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_PO4MMcarbSCH4.jl @@ -0,0 +1,102 @@ +using Logging + +using Plots; plotlyjs(size=(750, 565)) + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") +include("plot_mitgcm.jl") + +use_threads = false + +model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_COPDOM.yaml"), "PO4MMcarbSCH4" +) + +toutputs = [0, 1.0, 10.0] # , 100.0, 1000.0, 1999.5, 2000.0, 2999.5, 3000.0] + +# start with low oxygen to test marine sulphur system +PB.set_variable_attribute!(model, "atm", "O2", :initial_value, 0.1*3.71e19) +PB.set_variable_attribute!(model, "ocean", "O2", :initial_value, 0.1*0.2054) + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep = transportMITgcm.pars.Aimp_deltat[]/PB.Constants.k_secpyr +@info "using tstep=$tstep yr" + +# output_filename = "MITgcm_PO4MMcarbSCH42deg8FP64_3000yr_20210210" +output_filename = "" + +pickup_output = nothing +initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads, pickup_output=pickup_output) +pickup_output = nothing + +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +if !use_threads + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs , tstep) +else + # if Threads.nthreads() == 4 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:44, :, :), (45:72, :, :), (73:98, :, :), (99:128, :, :)] # 4 threads + # elseif Threads.nthreads() == 8 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:23, :, :), (24:44, :, :), (45:61, :, :), (62:72, :, :), (73:83, :, :), (84:98, :, :), (99:116, :, :) , (117:128, :, :)] # 8 threads + # elseif Threads.nthreads() == 16 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:12, :, :), (13:22, :, :), (23:32, :, :), (33:44, :, :), + # (45:55, :, :), (56:61, :, :), (62:67, :, :), (68:72, :, :), + # (73:77, :, :), (78:83, :, :), (84:90, :, :), (91:98, :, :), + # (99:109, :, :) , (110:116, :, :) , (117:121, :, :), (122:128, :, :),] # 16 threads + + # else + # error("no config for nthreads=$(Threads.nthreads())") + # end + + # cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, tiles) # vector of vectors, 1 per tile + + cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean") + + @time PALEOmodel.ODEfixed.integrateEulerthreads(paleorun, initial_state, modeldata, cellranges, toutputs , tstep) +end + +isempty(output_filename) || PALEOmodel.OutputWriters.save_jld2(paleorun.output, output_filename) + +show(PB.show_variables(paleorun.model), allrows=true) +println() + +############################ +# Plot +############################ + +# single plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# multiple plots per screen +gr(size=(1200, 900)) +pager = PALEOmodel.PlotPager((2,2), (legend_background_color=nothing, )) + +plot_forcings(paleorun.output, pager=pager) +pager(:newpage) +plot_PO4MMbase( + paleorun.output, + toutputs=toutputs, + tbioprod=[0.5, 1.0], + pager=pager, +) +pager(:newpage) +plot_tracers( + paleorun.output, + tracers=["O2_conc", "SO4_conc", "H2S_conc", "CH4_conc", "SO4_delta", "H2S_delta", "CH4_delta", "TAlk_conc", "DIC_conc", "DIC_delta"], + toutputs=[1e12], + pager=pager +) +pager(:newpage) +plot_carbSCH4(paleorun.output, pager=pager) +pager(:newpage) + + + diff --git a/examples/mitgcm/MITgcm_2deg8_abiotic.jl b/examples/mitgcm/MITgcm_2deg8_abiotic.jl new file mode 100644 index 0000000..029705f --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_abiotic.jl @@ -0,0 +1,99 @@ +using Logging + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") +include("plot_mitgcm.jl") + +use_threads = false + + +model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_abiotic.yaml"), "abiotic_O2" +) + +toutputs_relative = [0, 0.25, 0.5, 0.75, 1.0] #, 10.0] + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep = transportMITgcm.pars.Aimp_deltat[]/PB.Constants.k_secpyr + +@info "using tstep=$tstep yr" + +num_segments = 1 +outfile_root = "" +# num_segments = 20 +# outfile_root = "MITgcm_PO4MMbase2deg8_100yr_20210201" + +toutputs = [] + +for iseg in 1:num_segments + if iseg > 1 + !isempty(outfile_root) || error("outfile_root is empty") + pickup_filename = build_outfilename(outfile_root, iseg-1) + pickup_output = PALEOmodel.OutputWriters.load_jld2!(PALEOmodel.OutputWriters.OutputMemory(), pickup_filename) + + tstart = PB.get_data(pickup_output, "ocean.tmodel")[end] + + else + pickup_output = nothing + tstart = 0.0 + end + + global initial_state + global modeldata + global toutputs + + initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads, pickup_output=pickup_output) + pickup_output = nothing + + toutputs = toutputs_relative .+ tstart + + global paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + if !use_threads + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs , tstep) + else + # Threads.nthreads() == 4 || error("use_threads requires 4 threads, Threads.nthreads()=", Threads.nthreads()) + + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:44, :, :), (45:72, :, :), (73:98, :, :), (99:128, :, :)] # 4 threads + + # cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, tiles) # vector of vectors, 1 per tile + + cellranges = PB.Grids.get_tiled_cellranges(run.model, Threads.nthreads(), "ocean") + + @time PALEOmodel.ODEfixed.integrateEulerthreads(paleorun, initial_state, modeldata, cellranges, toutputs , tstep) + end + + if !isempty(outfile_root) + output_filename = build_outfilename(outfile_root, iseg) + PALEOmodel.OutputWriters.save_jld2(paleorun.output, output_filename) + end +end + +show(PB.show_variables(paleorun.model), allrows=true) +println() + +############################ +# Plot +############################ + +# single plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# multiple plots per screen +gr(size=(1200, 900)) +pager = PALEOmodel.PlotPager((2,2), (legend_background_color=nothing, )) + +plot_forcings(paleorun.output, pager=pager) +pager(:newpage) +plot_abiotic_O2(paleorun.output, toutputs=toutputs, pager=pager) +pager(:newpage) + + diff --git a/examples/mitgcm/MITgcm_2deg8_abiotic.yaml b/examples/mitgcm/MITgcm_2deg8_abiotic.yaml new file mode 100644 index 0000000..6779b69 --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_abiotic.yaml @@ -0,0 +1,157 @@ +# MITgcm 2.8deg O2-only atmosphere-ocean test configuration +abiotic_O2: + parameters: + CIsotope: ScalarData + domains: + global: + # scalar domain + + reactions: + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2_total] + variable_links: + sum: total_O2 + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2"] + + + atm: + + + reactions: + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + reservoir_tracer: + class: ReactionReservoirTotal + variable_links: + R*: Tracer* + variable_attributes: + R:initial_value: 1.0 # concentration m-3 + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 0.2054 # concentration m-3 (1027 kg m-3 * 200e-6 mol/kg-sw) + + + + transportMITgcm: + class: ReactionOceanTransportTMM + parameters: + base_path: $TMMDir$/MITgcm_2.8deg + TMfpsize: 64 + pack_chunk_width: 0 # 4 + + light: + class: ReactionLightColumn + parameters: + background_opacity: 0.04 + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_insol + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston_fixed: false + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + # output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: + + diff --git a/examples/mitgcm/MITgcm_2deg8_test.jl b/examples/mitgcm/MITgcm_2deg8_test.jl new file mode 100644 index 0000000..b0b980f --- /dev/null +++ b/examples/mitgcm/MITgcm_2deg8_test.jl @@ -0,0 +1,65 @@ +using Logging + +using Plots; plotlyjs(size=(750, 565)) + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") + +use_threads = true + +# model = config_mitgcm_expts("abiotic_O2", ""); toutputs = [0, 0.25, 0.5, 0.75, 1.0] #, 10.0] + +# model = config_mitgcm_expts("PO4MMbase", ""); toutputs = [0, 0.25, 0.5, 0.75, 1.0] #, 10.0, 99.5, 100.0] #, 1000.0, 1000.5] + +model = config_mitgcm_expts("PO4MMcarbSCH4", ""); toutputs = [0, 1.0] # , 10.0, 100.0, 1000.0, 1999.5, 2000.0, 2999.5, 3000.0] +# start with low oxygen to test marine sulphur system +PB.set_variable_attribute!(model, "atm", "O2", :initial_value, 0.1*3.71e19) +PB.set_variable_attribute!(model, "ocean", "O2", :initial_value, 0.1*0.2054) + + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep = transportMITgcm.par_Aimp_deltat[]/PB.Constants.k_secpyr + +@info "using tstep=$tstep yr" + + +initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads) + +run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +if !use_threads + @time PALEOmodel.ODEfixed.integrateEuler(run, initial_state, modeldata, toutputs , tstep) +else + #= + if Threads.nthreads() == 4 + # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + tiles = [(1:44, :, :), (45:72, :, :), (73:98, :, :), (99:128, :, :)] # 4 threads + elseif Threads.nthreads() == 8 + # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + tiles = [(1:23, :, :), (24:44, :, :), (45:61, :, :), (62:72, :, :), (73:83, :, :), (84:98, :, :), (99:116, :, :) , (117:128, :, :)] # 8 threads + elseif Threads.nthreads() == 16 + # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + tiles = [(1:12, :, :), (13:22, :, :), (23:32, :, :), (33:44, :, :), + (45:55, :, :), (56:61, :, :), (62:67, :, :), (68:72, :, :), + (73:77, :, :), (78:83, :, :), (84:90, :, :), (91:98, :, :), + (99:109, :, :) , (110:116, :, :) , (117:121, :, :), (122:128, :, :),] # 16 threads + + else + error("no config for nthreads=$(Threads.nthreads())") + end + + cellranges = PB.Grids.get_tiled_cellranges(run.model, tiles) # vector of vectors, 1 per tile +=# + cellranges = PB.Grids.get_tiled_cellranges(run.model, Threads.nthreads(), "ocean") + + @time PALEOmodel.ODEfixed.integrateEulerthreads(run, initial_state, modeldata, cellranges, toutputs , tstep) +end + + +show(PB.show_variables(run.model), allrows=true) +println() + diff --git a/examples/mitgcm/MITgcm_ECCO_COPDOM.yaml b/examples/mitgcm/MITgcm_ECCO_COPDOM.yaml new file mode 100644 index 0000000..8c00747 --- /dev/null +++ b/examples/mitgcm/MITgcm_ECCO_COPDOM.yaml @@ -0,0 +1,643 @@ +######################################################### +# MITgcm ECCO P, O atmosphere-ocean test configuration +####################################################### +PO4MMbase: + parameters: + CIsotope: ScalarData + SIsotope: ScalarData + domains: + global: + # scalar domain + + reactions: + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2_total] + variable_links: + sum: total_O2 + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2"] + + fluxOceanfloor: + reactions: + particulatefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] # fluxlist_BioParticulate + + solutefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["P", "O2"] #, "DIC::CIsotope", "TAlk", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] # fluxlist_Solute + + atm: + + + reactions: + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration m-3 (1027 kg m-3 * 2.15e-6 mol/kg-sw) + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 0.2054 # concentration m-3 (1027 kg m-3 * 200e-6 mol/kg-sw) + + + + transportMITgcm: + class: ReactionOceanTransportTMM + parameters: + base_path: $TMMDir$/MITgcm_ECCO + Aimp_deltat: 43200 # s 12 x 3600 + TMfpsize: 32 + pack_chunk_width: 4 # SIMD transport + + + light: + class: ReactionLightColumn + parameters: + background_opacity: 0.02 + + bioprod: + class: ReactionBioProdMMPop + parameters: + depthlimit: -119.0 # equivalent of top two layers in 2.8 deg grid (top of layer 3 in 2.8deg grid is -120.0m) + + rCorgPO4: 106.0 + rNPO4: 16.0 + rCcarbCorg: 0.25 + rCcarbCorg_fixed: true + + nuDOM: 0.66 + + k_poptype: Constant + k_uPO4: 3.0e-3 # mol m-3 yr-1 3 uM yr-1 + + k_nuttype: PO4MM + k_KPO4: 0.5e-3 # mol m-3 0.5 uM + + k_lightlim: MM + k_Ic: 30.0 + k_Irel: 1.0 + + variable_links: + partprod_*: export_* + domprod_*: remin_* # no explicit DOM pool, so reroute directly to remin + + biopump: + class: ReactionExportDirectColumn + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] + transportfloor: true + exportfunction: Martin + martin_rovera: 0.858 + martin_depthmin: 120.0 # base of second layer + + reminocean: + class: ReactionReminO2 + + parameters: + + variable_links: + soluteflux_*: "*_sms" + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_insol + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston_fixed: false + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + # output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: + reminoceanfloor: + class: ReactionReminO2 + + parameters: + + variable_links: + remin*: particulateflux* + soluteflux_*: fluxOceanfloor.soluteflux_* + + transfer_particulatefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + variable_links: + + transfer_solutefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + +######################################### +# MITgcm ECCO P, O with Cinorg, S, CH4 +########################################## +PO4MMcarbSCH4: + parameters: + CIsotope: IsotopeLinear # ScalarData + SIsotope: IsotopeLinear + + domains: + global: + # scalar domain + + reactions: + total_O2eq: + class: ReactionSum + parameters: + vars_to_add: [atm.O2, ocean.O2_total, -2*ocean.CH4_total, -2*ocean.H2S_total] + component_to_add: 1 # we just want the first component (total) from isotope variables + variable_links: + sum: total_O2eq + + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC_total, ocean.CH4_total] + variable_links: + sum: total_C + + total_S: + class: ReactionSum + parameters: + vars_to_add: [ocean.SO4_total, ocean.H2S_total] + variable_links: + sum: total_S + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2", "CO2::CIsotope"] + + fluxOceanfloor: + reactions: + particulatefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: particulateflux_ + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] # fluxlist_BioParticulate + + solutefluxtarget: + class: ReactionFluxTarget + operatorID: [1, 2] + parameters: + flux_totals: true + target_prefix: soluteflux_ + fluxlist: ["P", "O2", "DIC::CIsotope", "TAlk", "SO4::SIsotope", "H2S::SIsotope", "CH4::CIsotope"] # fluxlist_Solute + + atm: + + + reactions: + reservoir_O2: + class: ReactionReservoirAtm + + variable_links: + R*: O2* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration m-3 (1027 kg m-3 * 2.15e-6 mol/kg-sw) + + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 0.2054 # concentration m-3 (1027 kg m-3 * 200e-6 mol/kg-sw) + + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2291.74e-3 # 2231.486e-6 mol kg-1 * 1027 kg m-3 + R:initial_delta: -1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2426.8e-3 # 2363e-6 mol kg-1 * 1027 kg m-3 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_SO4: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + variable_links: + R*: SO4* + variable_attributes: + R:initial_value: 28756.0e-3 # concentration mol m-3 ~ 28e-3 mol/kg * 1027 kg m-3 + R:initial_delta: 1.0 + R:norm_value: 1000.0e-3 # for scaling only + + reservoir_H2S: + class: ReactionReservoirTotal + parameters: + field_data: external%SIsotope + limit_delta_conc: 1e-6 # mol m-3 to limit delta **EXPERIMENTAL** + variable_links: + R*: H2S* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + reservoir_CH4: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + limit_delta_conc: 1e-6 # mol m-3 to limit delta **EXPERIMENTAL** + variable_links: + R*: CH4* + variable_attributes: + R:initial_value: 1e-6 # concentration mol m-3 ~ 1e-9 mol/kg * 1027 kg m-3 + R:initial_delta: 0.0 + R:norm_value: 1.0e-3 # for scaling only + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega", "H2S"] + defaultconcs: ["TF", "TB", "Ca"] + solve_pH: solve # solve for Hfree iteratively + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + simd_width: FP32P8 # FP64P4 + pHtol: 1.2e-5 # 100*eps(Float32) + variable_links: + TCi_conc: DIC_conc + TS_conc: SO4_conc + CO2: CO2_conc + pCO2: pCO2 + OmegaCA: OmegaCA + TH2S_conc: H2S_conc + + transportMITgcm: + class: ReactionOceanTransportTMM + parameters: + base_path: $TMMDir$/MITgcm_ECCO + + light: + class: ReactionLightColumn + parameters: + background_opacity: 0.02 + + const_cisotopes: + class: ReactionScalarConst + parameters: + constnames: ["D_mccb_DIC", "D_B_mccb_mocb"] + variable_attributes: + D_mccb_DIC:initial_value: 0.0 + D_B_mccb_mocb:initial_value: 25.0 + + bioprod: + class: ReactionBioProdMMPop + parameters: + depthlimit: -60.0 # surface cells only + + rCorgPO4: 106.0 + rNPO4: 16.0 + # rCcarbCorg: 0.25 + rCcarbCorg_fixed: false + k_r0: 0.0485 + k_eta: 0.7440 + + nuDOM: 0.66 + + k_poptype: Constant + k_uPO4: 3.0e-3 # mol m-3 yr-1 3 uM yr-1 + + k_nuttype: PO4MM + k_KPO4: 0.5e-3 # mol m-3 0.5 uM + + k_lightlim: MM + k_Ic: 30.0 + k_Irel: 1.0 + + variable_links: + partprod_*: export_* + domprod_*: remin_* # no explicit DOM pool, so reroute directly to remin + + biopumporg: + class: ReactionExportDirectColumn + + parameters: + fluxlist: ["P", "N", "Corg::CIsotope"] + transportfloor: true + exportfunction: Martin + martin_rovera: 0.858 + martin_depthmin: 120.0 # base of second layer + + biopumpcarb: + class: ReactionExportDirectColumn + + parameters: + fluxlist: ["Ccarb::CIsotope"] + transportfloor: true + exportfunction: SumExp + input_frac: [0.55, 0.45] # 2-G model of Ridgwell & Hargreaves (2007) + sumexp_scale: [1890.5, 1e6] + + reminocean: + class: ReactionReminO2_SO4_CH4 + + parameters: + SO4reminlimit: 1000.0e-3 + + variable_links: + soluteflux_*: "*_sms" + + redox_H2S_O2: + class: ReactionRedoxH2S_O2 + + parameters: + R_H2S_O2: 3.65e2 # 3.65e3 # (mol m-3) yr-1 + + redox_CH4_O2: + class: ReactionRedoxCH4_O2 + + parameters: + R_CH4_O2: 1.0e2 # 10.0e3 # (mol m-3) yr-1 + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_insol + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_ECCO/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston_fixed: false + + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston_fixed: false + moistair: false # no sat H2O correction + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + transfer_fluxAtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: + reminoceanfloor: + class: ReactionReminO2_SO4_CH4 + + parameters: + SO4reminlimit: 1000.0e-3 + + variable_links: + remin*: particulateflux* + O2_conc: ocean.oceanfloor.O2_conc + SO4_*: ocean.oceanfloor.SO4_* # conc and delta + + soluteflux_*: fluxOceanfloor.soluteflux_* + + transfer_particulatefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.particulateflux_$fluxname$ + output_fluxes: particulateflux_$fluxname$ + variable_links: + + transfer_solutefluxOceanfloor: + class: ReactionFluxTransfer + operatorID: [1, 2] + parameters: + transfer_matrix: Identity + input_fluxes: fluxOceanfloor.soluteflux_$fluxname$ + output_fluxes: ocean.oceanfloor.$fluxname$_sms + variable_links: + + diff --git a/examples/mitgcm/MITgcm_ECCO_abiotic.jl b/examples/mitgcm/MITgcm_ECCO_abiotic.jl new file mode 100644 index 0000000..73270df --- /dev/null +++ b/examples/mitgcm/MITgcm_ECCO_abiotic.jl @@ -0,0 +1,93 @@ +using Logging + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") +include("plot_mitgcm.jl") + +use_threads = true +do_benchmarks = false + +model = config_mitgcm_expts("PO4MMbaseECCO", ""); toutputs_relative = [0.0, 1.0, 10.0, 100.0] + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep = transportMITgcm.par_Aimp_deltat[]/PB.Constants.k_secpyr +transportMITgcm = nothing + +@info "using tstep=$tstep yr" + +num_segments = 20 +outfile_root = "MITgcm_PO4MMbaseECCO_100yr_20210130" + +toutputs=[] +for iseg in 1:num_segments +# for iseg in 13:num_segments + if iseg > 1 + pickup_filename = build_outfilename(outfile_root, iseg-1) + pickup_output = PALEOmodel.OutputWriters.load_jld2!(PALEOmodel.OutputWriters.OutputMemory(), pickup_filename) + tstart = PB.get_data(pickup_output, "ocean.tmodel")[end] + else + pickup_output = nothing + tstart = 0.0 + end + initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads, pickup_output=pickup_output) + pickup_output = nothing + + toutputs = toutputs_relative .+ tstart + + global paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + if !use_threads + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs , tstep) + else + # Threads.nthreads() == 4 || error("use_threads requires 4 threads, Threads.nthreads()=", Threads.nthreads()) + + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # # tiles = [(1:44, :, :), (45:72, :, :), (73:98, :, :), (99:128, :, :)] # 4 threads + # tiles = [(1:124, :, :), (125:202, :, :), (203:275, :, :), (276:360, :, :)] # 4 threads + + # cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, tiles) # vector of vectors, 1 per tile + + cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean") + + @time PALEOmodel.ODEfixed.integrateEulerthreads(paleorun, initial_state, modeldata, cellranges, toutputs , tstep) + end + + output_filename = build_outfilename(outfile_root, iseg) + PALEOmodel.OutputWriters.save_jld2(paleorun.output, output_filename) +end + + +show(PB.show_variables(paleorun.model), allrows=true) +println() + +############################ +# Plot +############################ + +# single plots +# plotlyjs(size=(750, 565)) +# pager = PALEOmodel.DefaultPlotPager() + +# multiple plots per screen +gr(size=(1200, 900)) +pager = PALEOmodel.PlotPager((2,2), (legend_background_color=nothing, )) + +plot_forcings(paleorun.output, pager=pager, lonidx=200) +pager(:newpage) +plot_abiotic_O2(paleorun.output, toutputs=toutputs, pager=pager, lonidx1=200, lonidx2=340) +pager(:newpage) +plot_PO4MMbase( + paleorun.output, + toutputs=toutputs, + tbioprod=[0.5, 1.0], + pager=pager, +) +pager(:newpage) + + diff --git a/examples/mitgcm/MITgcm_ECCO_abiotic_test.jl b/examples/mitgcm/MITgcm_ECCO_abiotic_test.jl new file mode 100644 index 0000000..db5ecdc --- /dev/null +++ b/examples/mitgcm/MITgcm_ECCO_abiotic_test.jl @@ -0,0 +1,80 @@ +using Logging + +using Plots + +import PALEOboxes as PB +import PALEOmodel +import PALEOocean + +global_logger(ConsoleLogger(stderr,Logging.Info)) +include("config_mitgcm_expts.jl") +include("plot_mitgcm.jl") + +use_threads = true +do_benchmarks = false + +model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_ECCO_COPDOM.yaml"), "PO4MMbase" +) + +toutputs = [0.0, 1.0] # , 10.0, 100.0] + +transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") +tstep = transportMITgcm.par_Aimp_deltat[]/PB.Constants.k_secpyr +transportMITgcm = nothing + +@info "using tstep=$tstep yr" + +output_filename = "" + +pickup_output = nothing +initial_state, modeldata = PALEOmodel.initialize!(model, threadsafe=use_threads, pickup_output=pickup_output) +pickup_output = nothing + +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + +if !use_threads + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs , tstep) +else + # if Threads.nthreads() == 4 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:124, :, :), (125:202, :, :), (203:275, :, :), (276:360, :, :)] # 4 threads + # elseif Threads.nthreads() == 8 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:62, :, :), (63:120, :, :), + # (121:167, :, :), (168:200, :, :), + # (201:232, :, :), (233:275, :, :), + # (276:325, :, :) , (326:360, :, :)] # 8 threads + # elseif Threads.nthreads() == 16 + # # indices are slightly uneven to equalize the number of active (mostly ocean) cells per thread + # tiles = [(1:31, :, :), (32:62, :, :), (63:88, :, :), (89:120, :, :), + # (121:148, :, :), (149:167, :, :), (168:184, :, :), (185:199, :, :), + # (200:215, :, :), (216:230, :, :), (231:250, :, :), (251:274, :, :), + # (275:304, :, :), (305:325, :, :), (326:340, :, :), (341:360, :, :),] # 16 threads + + # else + # error("no config for nthreads=$(Threads.nthreads())") + # end + + # cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, tiles) # vector of vectors, 1 per tile + + cellranges = PB.Grids.get_tiled_cellranges(paleorun.model, Threads.nthreads(), "ocean") + + for (t,tc) in enumerate(cellranges) + numcells = 0 + for c in tc + numcells += length(c.indices) + end + println("thread $t numcells $numcells") + end + @time PALEOmodel.ODEfixed.integrateEulerthreads(paleorun, initial_state, modeldata, cellranges, toutputs , tstep) +end + +isempty(output_filename) || PALEOmodel.OutputWriters.save_jld2(paleorun.output, output_filename) + + +show(PB.show_variables(paleorun.model), allrows=true) +println() + + + diff --git a/examples/mitgcm/README.md b/examples/mitgcm/README.md new file mode 100644 index 0000000..5b0913a --- /dev/null +++ b/examples/mitgcm/README.md @@ -0,0 +1,39 @@ +# MITgcm transport matrix ocean-only examples + +GCM ocean transport test cases using transport matrices in format defined by [Khatiwala2007](@cite) + +Examples require a download of Samar Khatiwala's TMM files (for MITgcm, UVic models) as described in https://github.com/samarkhatiwala/tmm where TM files are from http://kelvin.earth.ox.ac.uk/spk/Research/TMM/TransportMatrixConfigs/. The TMMDir key in `examples\LocalPreferences.toml` should then be set to the folder location on the local machine. + +Default configurations are set to run for ~10 model yr, single core, single fixed timestep. See comments in .jl files to change run time and enable threading and split-timestep solver (with fast timestep for biogeochemistry and vertical transport). + +Approximate model CPU times below are for a single laptop core (a CPU i5-6300U from ~2015). + +## 2.8 degree O2 only abiotic + +Minimal test case for ocean transport and air-sea exchange. + +Tracers: atmosphere O2, ocean O2 + + julia> include("MITgcm_2deg8_abiotic.jl") + +Wallclock time: 10.3 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) + +## 2.8 degree P, O2 + +Minimal test case for biotic ocean. + +Tracers: atmosphere O2, ocean O2, P, DOP + + julia> include("MITgcm_2deg8_PO4MMbase.jl") + +Wallclock time: 10.0 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) + +## 2.8 degree P, O2, S, DIC/TAlk + +Minimal test case for a biotic ocean with carbonate chemistry, SO4/H2S and CH4. + +Tracers: atmosphere O2, CO2(x2), ocean O2, P, DOC(x2), H2S(x2), SO4(x2), CH4(x2), DIC(x2), TAlk (13 ocean tracers) + + julia> include("MITgcm_2deg8_PO4MMcarbSCH4.jl") + +Wallclock time: 27.2 s core-1 (model yr)-1 with default timestep = 86400 s (1 day) \ No newline at end of file diff --git a/examples/mitgcm/config_mitgcm_expts.jl b/examples/mitgcm/config_mitgcm_expts.jl new file mode 100644 index 0000000..3c4bd32 --- /dev/null +++ b/examples/mitgcm/config_mitgcm_expts.jl @@ -0,0 +1,64 @@ + +import PALEOboxes as PB + +import PALEOocean +import PALEOmodel +using Printf + +include("Insolation.jl") +include("../atmreservoirreaction.jl") # temporary solution to make ReactionReservoirAtm available + + +function config_mitgcm_expts(baseconfig, expt, extrapars=Dict()) + + + if baseconfig == "abiotic_O2" + + model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_abiotic.yaml"), "abiotic_O2", modelpars=extrapars) + + elseif baseconfig == "PO4MMbase" + + model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_COPDOM.yaml"), "PO4MMbase", modelpars=extrapars) + + elseif baseconfig == "PO4MMbaseECCO" + + model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_ECCO_COPDOM.yaml"), "PO4MMbase", modelpars=extrapars) + + elseif baseconfig == "PO4MMcarbSCH4" + model = PB.create_model_from_config( + joinpath(@__DIR__, "MITgcm_2deg8_COPDOM.yaml"), "PO4MMcarbSCH4", modelpars=extrapars) + + else + error("unrecognized baseconfig='$(baseconfig)'") + end + + return model +end + + + + +function build_outfilename(outfileroot, segment_number) + return outfileroot*@sprintf("_%04i", segment_number) +end + +function concatenate_segments(outfileroot, segment_range) + segment_range = collect(segment_range) + + output = PALEOmodel.OutputWriters.load_jld2!(PALEOmodel.OutputWriters.OutputMemory(), + build_outfilename(outfileroot, first(segment_range))) + for i in 2:length(segment_range) + segment_number = segment_range[i] + segout = PALEOmodel.OutputWriters.load_jld2!(PALEOmodel.OutputWriters.OutputMemory(), + build_outfilename(outfileroot, segment_number)) + + append!(output, segout) + + end + + return output +end + diff --git a/examples/mitgcm/plot_mitgcm.jl b/examples/mitgcm/plot_mitgcm.jl new file mode 100644 index 0000000..b404fb9 --- /dev/null +++ b/examples/mitgcm/plot_mitgcm.jl @@ -0,0 +1,137 @@ + +function plot_forcings( + output; + pager=PALEOmodel.DefaultPlotPager(), + lonidx=71, # 2.8 Pac 200 ECCO +) + + for t in [0.0, 0.5] + pager( + heatmap(output, "oceansurface.wind_speed", (tmodel=t,), swap_xy=true), + heatmap(output, "oceansurface.open_area_fraction", (tmodel=t,), swap_xy=true), + ) + end + for t in [0.0, 0.5] + pager(heatmap(output, "oceansurface.surface_insol", (tmodel=t,), swap_xy=true)) + end + + pager( + heatmap(output, "ocean.insol", (tmodel=0.0, i=lonidx), swap_xy=true), + heatmap(output, "ocean.insol", (tmodel=0.0, i=lonidx), swap_xy=true, ylim=(-500.0, 0)), + ) + + for t in [0.0, 0.5] + pager( + heatmap(output, "ocean.temp", (tmodel=t, k=1), swap_xy=true), + ) + end + + return nothing +end + +function plot_abiotic_O2( + output; + toutputs=[0.0, 1.0, 10.0, 100.0, 1000.0], + pager=PALEOmodel.DefaultPlotPager(), + lonidx1=71, # 2.8 Pac 200 ECCO + lonidx2=121, # 2.8 Atl 340 ECCO +) + for t in toutputs + pager( + heatmap(output, "ocean.O2_conc", (tmodel=t, i=lonidx1), swap_xy=true), + heatmap(output, "ocean.O2_conc", (tmodel=t, i=lonidx2), swap_xy=true), + ) + end + + pager( + plot(title="O2 air-sea flux", output, ["fluxAtmtoOceansurface.flux_total_O2"], ylabel="flux (mol yr-1)",), + plot(title="O2 totals", output, ["global.total_O2", "atm.O2", "ocean.O2_total"], ylabel="total (mol)",), + plot(title="O2 total", output, ["global.total_O2"], ylabel="total (mol)",), + ) + return nothing +end + +function plot_PO4MMbase( + output; + toutputs=[0.0, 1.0, 10.0, 100.0, 1000.0], + tbioprod=[1999.5, 2000.0], + pager=PALEOmodel.DefaultPlotPager(), +) + + for t in toutputs + pager( + heatmap(output, "ocean.P_conc", (tmodel=t, k=1), swap_xy=true,), + ) + end + + if PB.has_variable(output, "ocean.DOP_conc") + pager( + heatmap(output, "ocean.DOP_conc", (tmodel=toutputs[end], k=1), swap_xy=true,), + plot(title="P total", output, ["global.total_P", "ocean.P_total", "ocean.DOP_total"], ylabel="total (mol)",), + ) + end + + pager( + plot(title="P total", output, ["global.total_P"], ylabel="total (mol)",), + ) + + for t in tbioprod + pager( + heatmap(output, "ocean.bioprod/Prod_Corg", (tmodel=t, k=1), swap_xy=true,), + heatmap(output, "ocean.bioprod/Prod_Corg", (tmodel=t, k=2), swap_xy=true,), + ) + end + + pager(plot(title="Marine production", output, ["ocean.Prod_Ccarb_total", "ocean.Prod_Corg_total"], + ylabel="Production (mol C yr-1)",)) + + return nothing +end + + +function plot_carbSCH4( + output; + pager=PALEOmodel.DefaultPlotPager(), +) + + pager( + plot(title="O2 totals", output, ["atm.O2", "ocean.O2_total"], ylabel="total (mol)",), + plot(title="O2eq total", output, ["global.total_O2eq"], ylabel="total (mol)",), + plot(title="P total", output, ["global.total_P", "ocean.P_total"], ylabel="total (mol)",), + plot(title="C total", output, ["global.total_C"], ylabel="total (mol)",), + plot(title="C total moldelta", output, ["global.total_C.v_moldelta"], ylabel="mol S * per mil",), + plot(title="S totals", output, ["global.total_S", "ocean.SO4_total", "ocean.H2S_total"], ylabel="total (mol)",), + plot(title="S total", output, ["global.total_S"], ylabel="total (mol)",), + plot(title="S total moldelta", output, ["global.total_S.v_moldelta"], ylabel="mol S * per mil",), + plot(title="TAlk total", output, ["ocean.TAlk_total"], ylabel="total (mol)",), + + plot(title="Atmospheric d13CO2", output, ["atm.CO2_delta"], ylabel="d13C (per mil)",), + plot(title="Atmospheric pCO2", output, ["atm.pCO2atm"], ylabel="pCO2 (atm)",), + plot(title="Atmospheric pO2", output, ["atm.pO2PAL"], ylabel="pO2 (PAL)",), + ) + + return nothing +end + +function plot_tracers( + output; + tracers=["SO4_conc", "H2S_conc", "CH4_conc", "SO4_delta", "H2S_delta", "CH4_delta", "TAlk_conc", "DIC_conc", "DIC_delta"], + toutputs=[1e12], + pager=PALEOmodel.DefaultPlotPager(), + lonidx1=71, # 2.8 Pac 200 ECCO + lonidx2=121, # 2.8 Atl 340 ECCO +) + + for tr in tracers + for t in toutputs + pager( + heatmap(output, "ocean.$tr", (tmodel=t, i=lonidx1), swap_xy=true), + heatmap(output, "ocean.$tr", (tmodel=t, i=lonidx2), swap_xy=true), + heatmap(output, "ocean.$tr", (tmodel=t, k=1), swap_xy=true), + :skip, + ) + end + end + + return nothing +end diff --git a/examples/mitgcm/runtests.jl b/examples/mitgcm/runtests.jl new file mode 100644 index 0000000..0134875 --- /dev/null +++ b/examples/mitgcm/runtests.jl @@ -0,0 +1,60 @@ +using Test +using Logging +import DataFrames + +import PALEOboxes as PB + +import PALEOocean +import PALEOmodel + + +@testset "MITgcm examples" begin + +skipped_testsets = [ + # "PO4MMbase", +] + + +!("PO4MMbase" in skipped_testsets) && @testset "PO4MMbase" begin + + include("config_mitgcm_expts.jl") + + model = config_mitgcm_expts("PO4MMbase", "") + toutputs = [0.0, 0.25, 0.5, 0.75, 1.0] + + transportMITgcm = PB.get_reaction(model, "ocean", "transportMITgcm") + tstep_imp = transportMITgcm.pars.Aimp_deltat[]/PB.Constants.k_secpyr + + initial_state, modeldata = PALEOmodel.initialize!(model) + + run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + + @info "using tstep=$tstep_imp yr" + @time PALEOmodel.ODEfixed.integrateEuler(run, initial_state, modeldata, toutputs, tstep_imp) + + println("conservation checks:") + conschecks = [ + ("global", "total_O2", 1e-6), + ("global", "total_P", 1e-6), + ] + for (domname, varname, rtol) in conschecks + startval, endval = PB.get_data(run.output, domname*"."*varname)[[1, end]] + println(" check $domname.$varname $startval $endval $rtol") + @test isapprox(startval, endval, rtol=rtol) + end + + println("check values at end of run:") + checkvals = [ + ("ocean", "Prod_Corg_total", 3.6975e15, 1e-4), + ] + for (domname, varname, checkval, rtol) in checkvals + outputval = PB.get_data(run.output, domname*"."*varname)[end] + println(" check $domname.$varname $outputval $checkval $rtol") + @test isapprox(outputval, checkval, rtol=rtol) + end + +end + + + +end diff --git a/examples/ocean3box/PALEO_examples_oaonly.jl b/examples/ocean3box/PALEO_examples_oaonly.jl index cbe482e..c85f96b 100644 --- a/examples/ocean3box/PALEO_examples_oaonly.jl +++ b/examples/ocean3box/PALEO_examples_oaonly.jl @@ -15,8 +15,15 @@ global_logger(ConsoleLogger(stderr, Logging.Info)) include("config_ocean3box_expts.jl") include("plot_ocean_3box.jl") -# model = config_ocean3box_expts("oaonly", ["baseline"]); tspan=(-1e4,1e4) -model = config_ocean3box_expts("oaonly", ["killbio", "lowO2", "lowSO4"]); tspan=(-1e4,1e4) +# Ocean-atmosphere only (no weathering or burial) +# Use in conjunction with expt='killbio' (disables production at t=0 yr) to +# demonstrate effect of biological pump. + +model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_base") + +# config_ocean3box_expts(model, ["baseline"]); tspan=(-1e4,1e4) +config_ocean3box_expts(model, ["killbio", "lowO2", "lowSO4"]); tspan=(-1e4,1e4) initial_state, modeldata = PALEOmodel.initialize!(model) statevar_norm = PALEOmodel.get_statevar_norm(modeldata.solver_view_all) @@ -28,14 +35,14 @@ initial_deriv = similar(initial_state) PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) println("initial_deriv", initial_deriv) -run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) # With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling # (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) # Solve as DAE with sparse Jacobian PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -44,7 +51,7 @@ PALEOmodel.ODE.integrateDAEForwardDiff( ) # Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) -# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# sol = PALEOmodel.ODE.integrateForwardDiff(paleorun, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) # solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) @@ -60,11 +67,11 @@ PALEOmodel.ODE.integrateDAEForwardDiff( gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) -plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) +plot_totals(paleorun.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) plot_ocean_tracers( - run.output; + paleorun.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "CH4_conc", "P_conc", "SO4_delta", "H2S_delta", "CH4_delta"], pager=pager ) -plot_oaonly_abiotic(run.output; pager=pager) +plot_oaonly_abiotic(paleorun.output; pager=pager) pager(:newpage) # flush output diff --git a/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl b/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl index c35191d..7abdeb3 100644 --- a/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl +++ b/examples/ocean3box/PALEO_examples_oaonly_abiotic.jl @@ -17,9 +17,17 @@ global_logger(ConsoleLogger(stderr, Logging.Info)) include("config_ocean3box_expts.jl") include("plot_ocean_3box.jl") -model = config_ocean3box_expts("oaonly_abiotic", ["baseline"]); tspan=(0,1e4) -# model = config_ocean3box_expts("oaonly_abiotic", ["fastexchange"]); tspan=(0,1e4) -# model = config_ocean3box_expts("oaonly_abiotic", ["slowexchange"]); tspan=(0,1e4) +# Ocean-atmosphere only, test case cf Sarmiento & Toggweiler (2007) book, Fig 10.4, p436-7 +# This tests the effect of air-sea exchange rate for an abiotic model. + +# set k_piston below to show effect of default/fast/slow air-sea exchange rates + +model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_abiotic_base") + +config_ocean3box_expts(model, ["baseline"]); tspan=(0,1e4) +# config_ocean3box_expts(model, ["fastexchange"]); tspan=(0,1e4) +# config_ocean3box_expts(model, ["slowexchange"]); tspan=(0,1e4) initial_state, modeldata = PALEOmodel.initialize!(model) @@ -32,14 +40,14 @@ initial_deriv = similar(initial_state) PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) println("initial_deriv", initial_deriv) -run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) # With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling # (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) # Solve as DAE with sparse Jacobian PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -48,7 +56,7 @@ PALEOmodel.ODE.integrateDAEForwardDiff( ) # Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) -# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# sol = PALEOmodel.ODE.integrateForwardDiff(paleorun, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) # solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) @@ -64,9 +72,9 @@ PALEOmodel.ODE.integrateDAEForwardDiff( gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) -plot_totals(run.output; species=["C", "TAlk", "TAlkerror"], pager=pager) -plot_ocean_tracers(run.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot"], pager=pager) -plot_oaonly_abiotic(run.output; pager=pager) +plot_totals(paleorun.output; species=["C", "TAlk", "TAlkerror"], pager=pager) +plot_ocean_tracers(paleorun.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot"], pager=pager) +plot_oaonly_abiotic(paleorun.output; pager=pager) pager(:newpage) # flush output ##################################### @@ -74,5 +82,5 @@ pager(:newpage) # flush output ######################################## # Solve as DAE without Jacobian -# PALEOmodel.ODE.integrateDAE(run, initial_state, modeldata, tspan, alg=IDA(), +# PALEOmodel.ODE.integrateDAE(paleorun, initial_state, modeldata, tspan, alg=IDA(), # solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), save_start=false)) diff --git a/examples/ocean3box/PALEO_examples_oaopencarb.jl b/examples/ocean3box/PALEO_examples_oaopencarb.jl index 05ac61d..7b22041 100644 --- a/examples/ocean3box/PALEO_examples_oaopencarb.jl +++ b/examples/ocean3box/PALEO_examples_oaopencarb.jl @@ -16,8 +16,11 @@ global_logger(ConsoleLogger(stderr, Logging.Info)) include("config_ocean3box_expts.jl") include("plot_ocean_3box.jl") +# Open atmosphere-ocean with silicate carbonate weathering input and carbonate burial +model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base") -model = config_ocean3box_expts("oaopencarb", ["killbio", "lowO2"]); tspan=(-10e6, 10e6) # tspan=(-10e6,1000.0) # +config_ocean3box_expts(model, ["killbio", "lowO2"]); tspan=(-1e6, 1e6) # tspan=(-10e6, 10e6) initial_state, modeldata = PALEOmodel.initialize!(model) statevar_norm = PALEOmodel.get_statevar_norm(modeldata.solver_view_all) @@ -29,23 +32,23 @@ initial_deriv = similar(initial_state) PALEOmodel.SolverFunctions.ModelODE(modeldata)(initial_deriv, initial_state , nothing, 0.0) println("initial_deriv", initial_deriv) -run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) # With `killbio` H2S goes to zero, so this provides a test case for solvers `abstol` handling # (without this option, solver will fail or take excessive steps as it attempts to solve H2S for noise) -# Solve as DAE with sparse Jacobian +# Solve as DAE with (sparse) Jacobian PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( - abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), + abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), # required to handle H2S -> 0.0 save_start=false ) ) # Solve as ODE with Jacobian (OK if no carbonate chem or global temperature) -# sol = PALEOmodel.ODE.integrateForwardDiff(run, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) +# sol = PALEOmodel.ODE.integrateForwardDiff(paleorun, initial_state, modeldata, tspan, alg=CVODE_BDF(linear_solver=:KLU)) # solvekwargs=(abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all),)) @@ -62,13 +65,13 @@ gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) -plot_totals(run.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) +plot_totals(paleorun.output; species=["C", "TAlk", "TAlkerror", "O2", "S", "P"], pager=pager) plot_ocean_tracers( - run.output; + paleorun.output; tracers=["TAlk_conc", "DIC_conc", "temp", "pHtot", "O2_conc", "SO4_conc", "H2S_conc", "P_conc", "SO4_delta", "H2S_delta", "pHtot", "OmegaAR"], pager=pager ) -plot_oaonly_abiotic(run.output; pager=pager) -plot_carb_open(run.output; pager=pager) +plot_oaonly_abiotic(paleorun.output; pager=pager) +plot_carb_open(paleorun.output; pager=pager) pager(:newpage) # flush output diff --git a/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml b/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml index 9444494..5b5ed77 100644 --- a/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml +++ b/examples/ocean3box/PALEO_examples_ocean3box_cfg.yaml @@ -1,6 +1,6 @@ - +# Abiotic atmosphere-ocean with atmospheric CO2, ocean DIC and TAlk ocean3box_oaonly_abiotic_base: parameters: CIsotope: IsotopeLinear @@ -115,7 +115,7 @@ ocean3box_oaonly_abiotic_base: oceanfloor: reactions: - +# Biotic atmosphere-ocean with atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk ocean3box_oaonly_base: parameters: CIsotope: IsotopeLinear @@ -373,8 +373,9 @@ ocean3box_oaonly_base: oceanfloor: reactions: -# Open atm-ocean carbonate system, with weathering input and burial output -# Closed organic carbon, ocean sulphur systems (no burial) +# Atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk +# Open atm-ocean carbonate system, with carbonate/silicate weathering input, degassing input, and carbonate burial output +# Closed ocean organic carbon, sulphur systems (no burial) ocean3box_oaopencarb_base: parameters: CIsotope: IsotopeLinear @@ -442,6 +443,14 @@ ocean3box_oaopencarb_base: # scalar domain reactions: + # set forcings to steady-state at time tforce_constant + tforce_constant: + class: ReactionScalarConst + parameters: + constnames: ["tforce_constant"] + variable_attributes: + tforce_constant%initial_value: 0.0 + force_enable_bioprod: class: ReactionForceInterp parameters: @@ -452,12 +461,18 @@ ocean3box_oaopencarb_base: force_solar: class: ReactionForce_CK_Solar + variable_links: + tforce: tforce_constant force_UDWE: - class: ReactionForce_UDWEbergman2004 + class: ReactionForce_UDWEbergman2004 + variable_links: + tforce: tforce_constant force_CPlandrel: class: ReactionForce_CPlandrelbergman2004 + variable_links: + tforce: tforce_constant temp_CK_1992: class: ReactionGlobalTemperatureCK1992 diff --git a/examples/ocean3box/README.md b/examples/ocean3box/README.md index 00e19e1..21932b4 100644 --- a/examples/ocean3box/README.md +++ b/examples/ocean3box/README.md @@ -1,8 +1,31 @@ # 3 Box Ocean Examples - julia> cd("PALEOocean/examples/ocean3box") - julia> include("PALEO_examples_ocean3box.jl") - These examples demonstrate the 3-box [Sarmiento1984](@cite), [Toggweiler1985](@cite) ocean model, standalone and coupled to the COPSE land surface and sediment/crust (as used in [Clarkson2015](@cite)). +## Abiotic CO2/DIC only atmosphere-ocean + + julia> include("PALEO_examples_oaonly_abiotic.jl") + +Abiotic atmosphere-ocean with atmosphere CO2, ocean DIC, TAlk. Test case cf Sarmiento & Toggweiler (2007) book, Fig 10.4, p436-7 + +Commented-out options in file to set k_piston to show effect of default/fast/slow air-sea exchange rates + +## Biotic atmosphere-ocean (no weathering or burial) + + julia> include("PALEO_examples_oaonly.jl") + +Biotic atmosphere-ocean with atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk (no weathering or burial). + +Use in conjunction with expt='killbio' (disables production at t=0 yr) to +demonstrate effect of biological pump. + +## Open atmosphere-ocean with silicate/carbonate weathering and burial + + julia> include("PALEO_examples_oaopencarb.jl") + +Biotic atmosphere-ocean with atmosphere O2, CO2, ocean P, O2, SO4/H2S, CH4, DIC, TAlk + +Open atm-ocean carbonate system, with carbonate/silicate weathering input, degassing input, and carbonate burial output + +Closed ocean organic carbon, sulphur systems (no burial) diff --git a/examples/ocean3box/config_ocean3box_expts.jl b/examples/ocean3box/config_ocean3box_expts.jl index b993365..c51fc91 100644 --- a/examples/ocean3box/config_ocean3box_expts.jl +++ b/examples/ocean3box/config_ocean3box_expts.jl @@ -1,39 +1,7 @@ +include("../atmreservoirreaction.jl") # temporary solution to make ReactionReservoirAtm available "test cases and examples for 3 box ocean" -function config_ocean3box_expts(baseconfig, expts) - - if baseconfig == "oaonly_abiotic" - # Ocean-atmosphere only, test case cf Sarmiento & Toggweiler (2007) book, Fig 10.4, p436-7 - # This tests the effect of air-sea exchange rate for an abiotic model. - - # set k_piston below to show effect of default/fast/slow air-sea exchange rates - - model = PB.create_model_from_config( - joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_abiotic_base") - - elseif baseconfig == "oaonly" - # Ocean-atmosphere only (no weathering or burial) - # Use in conjunction with expt='killbio' (disables production at t=0 yr) to - # demonstrate effect of biological pump. - - model = PB.create_model_from_config( - joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_base") - - elseif baseconfig=="oaopencarb" - # Open atmosphere-ocean with silicate carbonate weathering input and carbonate burial - - model = PB.create_model_from_config( - joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base") - - elseif baseconfig=="oaopencorg" - # Semi-open atmosphere-ocean with organic carbon and sulphur burial - - model = PB.create_model_from_config( - joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencorg_base") - - else - error("unrecognized baseconfig='$(baseconfig)'") - end +function config_ocean3box_expts(model, expts) ########################### # configure expt @@ -65,5 +33,5 @@ function config_ocean3box_expts(baseconfig, expts) end end - return model + return nothing end diff --git a/examples/ocean3box/plot_ocean_3box.jl b/examples/ocean3box/plot_ocean_3box.jl index 3b68faa..6fb274d 100644 --- a/examples/ocean3box/plot_ocean_3box.jl +++ b/examples/ocean3box/plot_ocean_3box.jl @@ -94,45 +94,3 @@ function plot_corg_open( return nothing end -function plot_copse_totals( - output; - pager=PALEOmodel.DefaultPlotPager() -) - pager(plot(title="Carbon", output, ["global.total_C", "sedcrust.C", "sedcrust.G", "atm.CO2", "ocean.DIC_total", "ocean.CH4_total"], ylabel="mol C",)), - pager(plot(title="Total carbon", output, ["global.total_C"], ylabel="mol C",)) - pager(plot(title="Total carbon moldelta", output, ["global.total_C.v_moldelta"], ylabel="mol C * per mil",)) - pager(plot(title="Sulphur", output, ["global.total_S", "ocean.SO4_total", "ocean.H2S_total", "sedcrust.PYR", "sedcrust.GYP"], ylabel="mol S",)) - pager(plot(title="Total sulphur", output, ["global.total_S"], ylabel="mol S",)) - pager(plot(title="Total sulphur moldelta",output, ["global.total_S.v_moldelta"], ylabel="mol S * per mil",)) - pager(plot(title="Total redox", output, ["global.total_redox"], ylabel="mol O2 equiv",)) - - pager(:newpage) - - return nothing -end - -function plot_copse_reloaded( - output; - pager=PALEOmodel.DefaultPlotPager() -) - - pager(plot(title="Physical forcings", output, "global.".*["DEGASS", "UPLIFT", "PG"], ylim=(0, 2.0), ylabel="normalized forcing",)) - pager(plot(title="Land area forcings", output, ["land.BA_AREA", "land.GRAN_AREA", "global.ORGEVAP_AREA"], ylim=(0, 2.5), ylabel="normalized forcing",)) - pager(plot(title="Basalt area forcings", output, ["global.CFB_area", "land.oib_area"], ylabel="area (km^2)",)) - pager(plot(title="Evolutionary forcings", output, "global.".*["EVO", "W", "Bforcing", "PELCALC", "CPland_relative", "F_EPSILON", "COAL"], ylabel="normalized forcing",)) - - pager(plot(title="Silicate weathering", output, ["land.silw", "land.granw", "land.basw", "oceanfloor.sfw_total"], ylabel="flux (mol/yr)",)) - - pager((plot(title="Sulphur isotopes", output, ["sedcrust.PYR_delta", "sedcrust.GYP_delta"]); - plot!(output, ["ocean.SO4_delta", "ocean.H2S_delta"], (cell=[:s, :h, :d], ), ylabel="delta 34S (per mil)",))) - pager((plot(title="Carbon isotopes", output, ["sedcrust.G_delta", "sedcrust.C_delta", "atm.CO2_delta"]); - plot!(output, ["ocean.DIC_delta", "ocean.CH4_delta"], (cell=[:s, :h, :d], ), ylabel="per mil",))) - - pager(plot(title="Sr sed reservoir", output, ["sedcrust.Sr_sed"], ylabel="Sr_sed (mol)",)) - pager(plot(title="Sr ocean reservoir", output, ["ocean.Sr"], ylabel="Sr (mol)",)) - pager(plot(title="Sr fluxes", output, ["fluxRtoOcean.flux_Sr", "fluxOceanBurial.flux_total_Sr", "fluxOceanfloor.soluteflux_total_Sr" ], ylabel="Sr flux (mol yr-1)",)) - pager((plot(title="Sr isotopes", output, ["sedcrust.Sr_mantle_delta", "sedcrust.Sr_new_ig_delta", "sedcrust.Sr_old_ig_delta", "sedcrust.Sr_sed_delta"]); - plot!(output, "ocean.Sr_delta", (cell=1, ), ylabel="87Sr",))) - - return nothing -end diff --git a/examples/ocean3box/runtests.jl b/examples/ocean3box/runtests.jl index 650f87f..8b67e94 100644 --- a/examples/ocean3box/runtests.jl +++ b/examples/ocean3box/runtests.jl @@ -1,6 +1,6 @@ using Test using Logging -using DiffEqBase +# using DiffEqBase using Sundials import DataFrames @@ -10,6 +10,7 @@ import PALEOocean import PALEOcopse import PALEOmodel +include("config_ocean3box_expts.jl") @testset "ocean3box examples" begin @@ -21,15 +22,16 @@ skipped_testsets = [ !("oaonly_abiotic" in skipped_testsets) && @testset "oaonly_abiotic" begin - include("config_ocean3box_expts.jl") + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_abiotic_base") - model = config_ocean3box_expts("oaonly_abiotic", ["baseline"]); tspan=(0,1e4) + config_ocean3box_expts(model, ["baseline"]); tspan=(0,1e4) initial_state, modeldata = PALEOmodel.initialize!(model) - run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) sol = PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -45,7 +47,7 @@ skipped_testsets = [ ] for (domname, varname, propertyname, rtol) in conschecks startval, endval = PB.get_property( - PB.get_data(run.output, domname*"."*varname), + PB.get_data(paleorun.output, domname*"."*varname), propertyname=propertyname )[[1, end]] println(" check $domname.$varname $startval $endval $rtol") @@ -59,7 +61,7 @@ skipped_testsets = [ ("atm", "CO2_delta", -10.161, 1e-4), ] for (domname, varname, checkval, rtol) in checkvals - outputval = PB.get_data(run.output, domname*"."*varname)[end] + outputval = PB.get_data(paleorun.output, domname*"."*varname)[end] println(" check $domname.$varname $outputval $checkval $rtol") @test isapprox(outputval, checkval, rtol=rtol) end @@ -68,15 +70,16 @@ end !("oaonly" in skipped_testsets) && @testset "oaonly" begin - include("config_ocean3box_expts.jl") + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaonly_base") - model = config_ocean3box_expts("oaonly", ["baseline"]); tspan=(0,1e4) + config_ocean3box_expts(model, ["baseline"]); tspan=(0,1e4) initial_state, modeldata = PALEOmodel.initialize!(model) - run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) sol = PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -95,7 +98,7 @@ end ] for (domname, varname, propertyname, rtol) in conschecks startval, endval = PB.get_property( - PB.get_data(run.output, domname*"."*varname), + PB.get_data(paleorun.output, domname*"."*varname), propertyname=propertyname )[[1, end]] println(" check $domname.$varname $startval $endval $rtol") @@ -110,7 +113,7 @@ end ("atm", "pO2atm", 0.2102, 1e-4), ] for (domname, varname, checkval, rtol) in checkvals - outputval = PB.get_data(run.output, domname*"."*varname)[end] + outputval = PB.get_data(paleorun.output, domname*"."*varname)[end] println(" check $domname.$varname $outputval $checkval $rtol") @test isapprox(outputval, checkval, rtol=rtol) end @@ -119,15 +122,16 @@ end !("oaopencarb" in skipped_testsets) && @testset "oaopencarb" begin - include("config_ocean3box_expts.jl") + model = PB.create_model_from_config( + joinpath(@__DIR__, "PALEO_examples_ocean3box_cfg.yaml"), "ocean3box_oaopencarb_base") - model = config_ocean3box_expts("oaopencarb", ["killbio", "lowO2"]); tspan=(-10e6,10e6) + config_ocean3box_expts(model, ["killbio", "lowO2"]); tspan=(-10e6,10e6) initial_state, modeldata = PALEOmodel.initialize!(model) - run = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) + paleorun = PALEOmodel.Run(model=model, output=PALEOmodel.OutputWriters.OutputMemory()) sol = PALEOmodel.ODE.integrateDAEForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, alg=IDA(linear_solver=:KLU), solvekwargs=( abstol=1e-6*PALEOmodel.get_statevar_norm(modeldata.solver_view_all), @@ -143,7 +147,7 @@ end ] for (domname, varname, propertyname, rtol) in conschecks startval, endval = PB.get_property( - PB.get_data(run.output, domname*"."*varname), + PB.get_data(paleorun.output, domname*"."*varname), propertyname=propertyname )[[1, end]] println(" check $domname.$varname $startval $endval $rtol") @@ -152,12 +156,17 @@ end println("check values at end of run:") checkvals = [ - ("ocean", "pHtot", [8.32404, 8.40819, 8.3423], 1e-4 ), - ("atm", "pCO2atm", 0.000209573, 1e-4), - ("atm", "CO2_delta", -8.51631 , 1e-4), + # time dependent COPSE forcing + # ("ocean", "pHtot", [8.32404, 8.40819, 8.3423], 1e-4 ), + # ("atm", "pCO2atm", 0.000209573, 1e-4), + # ("atm", "CO2_delta", -8.51631 , 1e-4), + # constant forcing at t=0.0 + ("atm", "pCO2atm", 0.000214884, 1e-4), + ("ocean", "pHtot", [8.31981, 8.40179, 8.33596], 1e-4), + ("atm", "CO2_delta", -8.52482 , 1e-4), ] for (domname, varname, checkval, rtol) in checkvals - outputval = PB.get_data(run.output, domname*"."*varname)[end] + outputval = PB.get_data(paleorun.output, domname*"."*varname)[end] println(" check $domname.$varname $outputval $checkval $rtol") @test isapprox(outputval, checkval, rtol=rtol) end diff --git a/examples/transport_examples/PALEO_examples_transport_advect.jl b/examples/transport_examples/PALEO_examples_transport_advect.jl index 86b042e..0c9161a 100644 --- a/examples/transport_examples/PALEO_examples_transport_advect.jl +++ b/examples/transport_examples/PALEO_examples_transport_advect.jl @@ -34,10 +34,10 @@ initial_state = PALEOmodel.get_statevar(modeldata.solver_view_all) ############################ tspan=(0.0, 5e3) -run = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) PALEOmodel.ODE.integrateForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, solvekwargs=(reltol=1e-5,), ) @@ -55,14 +55,14 @@ gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) # total -pager(plot(title="Total T", run.output, ["ocean.T_total"]; ylabel="T (mol)",)) +pager(plot(title="Total T", paleorun.output, ["ocean.T_total"]; ylabel="T (mol)",)) # line plots at specified times columns = [:a, :b] tcol = collect(0.0:100.0:1000) # times at which to plot columns for col in columns pager( - plot(title="Ocean T_conc column :$col", run.output, "ocean.T_conc", ( tmodel=tcol, column=col); + plot(title="Ocean T_conc column :$col", paleorun.output, "ocean.T_conc", ( tmodel=tcol, column=col); swap_xy=true, labelattribute=:filter_records) ) end @@ -72,7 +72,7 @@ pager(:newpage) pager=PALEOmodel.PlotPager((2, 1), (legend_background_color=nothing, )) for col in columns pager( - heatmap(title="Ocean T_conc column :$col", run.output, "ocean.T_conc", (column=col,); + heatmap(title="Ocean T_conc column :$col", paleorun.output, "ocean.T_conc", (column=col,); clims=(0.0, 0.5)) # transport is quite diffusive, so set scale for visibility that ignores initial high conc ) end diff --git a/examples/transport_examples/PALEO_examples_transport_diffuse.jl b/examples/transport_examples/PALEO_examples_transport_diffuse.jl index 2b68daf..26c830a 100644 --- a/examples/transport_examples/PALEO_examples_transport_diffuse.jl +++ b/examples/transport_examples/PALEO_examples_transport_diffuse.jl @@ -35,10 +35,10 @@ initial_state = PALEOmodel.get_statevar(modeldata.solver_view_all) ############################ tspan=(0.0, 5e3) -run = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) +paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) PALEOmodel.ODE.integrateForwardDiff( - run, initial_state, modeldata, tspan, + paleorun, initial_state, modeldata, tspan, solvekwargs=(reltol=1e-5,), ) @@ -56,14 +56,14 @@ gr(size=(1200, 900)) pager=PALEOmodel.PlotPager((2, 3), (legend_background_color=nothing, )) # total -pager(plot(title="Total T", run.output, ["ocean.T_total"]; ylabel="T (mol)",)) +pager(plot(title="Total T", paleorun.output, ["ocean.T_total"]; ylabel="T (mol)",)) # line plots at specified times columns = [:a, :b] tcol = collect(0.0:100.0:1000) # times at which to plot columns for col in columns pager( - plot(title="Ocean T_conc column :$col", run.output, "ocean.T_conc", ( tmodel=tcol, column=col); + plot(title="Ocean T_conc column :$col", paleorun.output, "ocean.T_conc", ( tmodel=tcol, column=col); swap_xy=true, xaxis=:log10, xlim=10 .^(-2.5, 0), labelattribute=:filter_records) ) end @@ -72,7 +72,7 @@ pager(:newpage) # heatmaps vs time pager=PALEOmodel.PlotPager((2, 1), (legend_background_color=nothing, )) for col in columns - d = PALEOmodel.get_array(run.output, "ocean.T_conc"; column=col) + d = PALEOmodel.get_array(paleorun.output, "ocean.T_conc"; column=col) d.values .= log10.(d.values) pager( heatmap(title="Ocean T_conc column :$col", d; clims=(-2.5, 0), xlim=(0, 1000)) # set scale for visibility diff --git a/src/PALEOocean.jl b/src/PALEOocean.jl index b4573d2..6ae2dc5 100644 --- a/src/PALEOocean.jl +++ b/src/PALEOocean.jl @@ -13,5 +13,6 @@ include("ocean/Ocean.jl") include("oceansurface/Oceansurface.jl") +include("oceanfloor/Oceanfloor.jl") end # module PALEOocean diff --git a/src/ocean/VerticalTransport.jl b/src/ocean/VerticalTransport.jl index 5c077de..3e8c4ed 100644 --- a/src/ocean/VerticalTransport.jl +++ b/src/ocean/VerticalTransport.jl @@ -14,7 +14,7 @@ using PALEOboxes.DocStrings Calculate light availability `insol` in ocean interior, given surface insolation `surface_insol`. -Includes: (i) a `background_opacity`; (ii) contributions from any Variables representing concentrations with non-initialize_to_zero +Includes: (i) a `background_opacity`; (ii) contributions from any Variables representing concentrations with non-zero `specific_light_extinction` attribute; (iii) any other opacity contributions added to the Target Variable `opacity`. # Parameters diff --git a/src/oceanfloor/Burial.jl b/src/oceanfloor/Burial.jl new file mode 100644 index 0000000..cbccbc0 --- /dev/null +++ b/src/oceanfloor/Burial.jl @@ -0,0 +1,565 @@ +module Burial + + +import PALEOboxes as PB +using PALEOboxes.DocStrings + +using SpecialFunctions +using Interpolations + +# import Infiltrator + +""" + ReactionShelfCarb + +Shallow-water carbonate burial controlled by carbonate saturation state (after [Caldeira1993](@cite)) + +Carbonate burial rate in cell `i` is `shelf_Ccarb[i]` = `carbsedshallow` * `shelfareanorm[i]` * '(OmegaAR[i]-1.0)^1.7` +where `carbsedshallow` (mol C yr-1) controls the global rate, and `shelfareanorm[i]` controls the spatial distribution +among ocean shelf cells. + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionShelfCarb{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParDouble("carbsedshallow", 1.4355e12, units="mol C yr-1", + description="total carbonate deposition rate"), + + PB.ParDoubleVec("shelfareanorm", Float64[], units="", + description="per box distribution of carbonate burial (length=Domain size, must sum to 1.0)"), + + PB.ParType(PB.AbstractData, "CIsotope", PB.ScalarData, + external=true, + allowed_values=PB.IsotopeTypes, + description="disable / enable carbon isotopes and specify isotope type"), + ) + +end + +function PB.register_methods!(rj::ReactionShelfCarb) + + CIsotopeType = rj.pars.CIsotope[] + PB.setfrozen!(rj.pars.CIsotope) + @info "register_methods! $(PB.fullname(rj)) CIsotopeType=$CIsotopeType" + + fluxOceanfloorSolute = PB.Fluxes.FluxContrib( + "fluxOceanfloor.soluteflux_", ["DIC::$CIsotopeType", "TAlk", "(Ca)"], + alloptional=false + ) + + fluxOceanBurial = PB.Fluxes.FluxContrib( + "fluxOceanBurial.flux_", ["Ccarb::$CIsotopeType"], + ) + + vars = [ + PB.VarDepScalar("(global.shelfarea_force)", "", + "optional forcing multiplier for carbsedshallow (defaults to 1.0)"), + + PB.VarDep("ocean.oceanfloor.OmegaAR", "", "aragonite saturation"), + + PB.VarProp("shelf_Ccarb", "mol yr-1", "shelf Ccarb burial", + attributes=(:field_data=>CIsotopeType, :initialize_to_zero=>true, :calc_total=>true)), + ] + if CIsotopeType <: PB.AbstractIsotopeScalar + push!(vars, + PB.VarDep("ocean.oceanfloor.DIC_delta", "per mil", "d13C DIC"), + PB.VarDepScalar("ocean.D_mccb_DIC", "per mil", + "d13C marine calcite burial relative to ocean DIC"), + ) + end + + PB.add_method_do!( + rj, + do_shelf_carb, + ( + PB.VarList_namedtuple_fields(fluxOceanfloorSolute), + PB.VarList_namedtuple_fields(fluxOceanBurial), + PB.VarList_namedtuple(vars) + ), + p = CIsotopeType, + ) + + PB.add_method_do_totals_default!(rj) + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +function PB.check_configuration(rj::ReactionShelfCarb, model::PB.Model) + configok = true + + PB.check_parameter_sum(rj.pars.shelfareanorm, PB.get_length(rj.domain)) || (configok = false) + + return configok +end + +function do_shelf_carb( + m::PB.ReactionMethod, + pars, + (fluxOceanfloorSolute, fluxOceanBurial, vars), + cellrange::PB.AbstractCellRange, + delta +) + CIsotopeType = m.p + + shelfarea_force = PB.get_if_available(vars.shelfarea_force, 1.0) + @inbounds for i in cellrange.indices + if pars.shelfareanorm[i] > 0.0 + ccrate_total = (shelfarea_force * pars.carbsedshallow[] * pars.shelfareanorm[i] + * max(vars.OmegaAR[i]-1.0, 0.0)^1.7) + ccrate = @PB.isotope_totaldelta(CIsotopeType, ccrate_total, vars.DIC_delta[i]+vars.D_mccb_DIC[]) + vars.shelf_Ccarb[i] = ccrate # NB: attribute :initialize_to_zero=true so we can skip cells with shelfareanorm=0 + + fluxOceanfloorSolute.DIC[i] -= ccrate + fluxOceanfloorSolute.TAlk[i] -= 2.0*PB.get_total(ccrate) + fluxOceanBurial.Ccarb[i] += ccrate + + PB.add_if_available(fluxOceanfloorSolute.Ca, i, -PB.get_total(ccrate)) + end + end + + return nothing +end + + +""" + ReactionBurialEffCarb + +Deep ocean carbonate burial as a carbonate-saturation-state-dependent fraction of carbonate flux. + +Parameterisation from [Caldeira1993](@cite), intended for use in ocean box models with a single deep ocean box. + +Fraction of input carbonate flux buried `flys` is a function of oceanfloor carbonate saturation state. + +Carbonate burial flux `flux_Ccarb` = `particulateflux_Ccarb` * `flys`, where Fraction of input carbonate flux +buried `flys` is a function of carbonate saturation state. + +Can be used with a spatially resolved model to provide a saturation-state dependent switch that allows burial of +oceanfloor carbonate flux only above the lysocline. + +Parameter `burial_eff_function` sets the functional form used for burial fraction `flys`: +- "Caldeira1993": flys = 0.5*(1.0+tanh(k0([CO3] -k1))) + (after [Caldeira1993](@cite), intended for use with the single deep ocean box in a 3-box ocean model) +- "OmegaCA": flys = 1 - 0.5 * erfc(m0*(Ω_CA - m1)) (burial efficiency a function of oceanfloor saturation state) + +# Parameters +$(PARS) + +# Methods and Variables for default Parameters +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionBurialEffCarb{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParString("burial_eff_function", "Caldeira1993", allowed_values=["Caldeira1993", "OmegaCA"], + description="functional form for burial efficiency"), + PB.ParDouble("k0", 26.0, units="m3/mol", + description="Caldeira1993: burial frac 'steepness' with CO3 concentration"), + PB.ParDouble("k1", 0.11, units="mol/m3", + description="Caldeira1993: CO3 concentration at burial frac 0.5"), + PB.ParDouble("m0", 10.0, units="", + description="OmegaCA: burial frac 'steepness' with OmegaCA"), + PB.ParDouble("m1", 1.0, units="", + description="OmegaCA: OmegaCA at burial frac 0.5"), + + PB.ParBoolVec("hascarbseddeep", Bool[], + description="per box flag to enable (length=Domain size)"), + + PB.ParType(PB.AbstractData, "CIsotope", PB.ScalarData, + external=true, + allowed_values=PB.IsotopeTypes, + description="disable / enable carbon isotopes and specify isotope type"), + ) + + +end + +function PB.register_methods!(rj::ReactionBurialEffCarb) + + CIsotopeType = rj.pars.CIsotope[] + PB.setfrozen!(rj.pars.CIsotope) + @info "register_methods! $(PB.fullname(rj)) CIsotopeType=$CIsotopeType" + + fluxOceanfloorSolute = PB.Fluxes.FluxContrib( + "fluxOceanfloor.soluteflux_", ["DIC::$CIsotopeType", "TAlk", "(Ca)"], + alloptional=false, + ) + + fluxOceanBurial = PB.Fluxes.FluxContrib( + "fluxOceanBurial.flux_", ["Ccarb::$CIsotopeType"], + ) + + vars = [ + PB.VarTarget("particulateflux_Ccarb", "mol yr-1", "input carbonate particulate flux", + attributes=(:field_data=>CIsotopeType,)), + + PB.VarProp("deep_Ccarb", "mol yr-1", "deep unresolved Ccarb burial", + attributes=(:field_data=>CIsotopeType, :calc_total=>true,)), + PB.VarProp("flys", "", "fraction of Ccarb export buried"), + ] + if CIsotopeType <: PB.AbstractIsotopeScalar + push!(vars, + PB.VarDep("ocean.oceanfloor.DIC_delta", "per mil", "d13C DIC"), + PB.VarDepScalar("ocean.D_mccb_DIC", "per mil", + "d13C marine calcite burial relative to ocean DIC"), + ) + end + + function burial_eff_caldeira1993(pars, vars, i) + return 0.5*(1.0 + tanh(pars.k0[]*(vars.CO3_conc[i] - pars.k1[]))) + end + + function burial_eff_omegaCA(pars, vars, i) + return 1.0 - 0.5*erfc(pars.m0[]*(vars.OmegaCA[i] - pars.m1[])) + end + + if rj.pars.burial_eff_function[] == "Caldeira1993" + # ScalarData as we only want total, not isotopes (if any) + push!(vars, PB.VarDep("ocean.oceanfloor.CO3_conc", "mol m-3", "CO3 concentration"; attributes=(:field_data=>PB.ScalarData,))) + burial_eff_fn = burial_eff_caldeira1993 + elseif rj.pars.burial_eff_function[] == "OmegaCA" + push!(vars, PB.VarDep("ocean.oceanfloor.OmegaCA", "", "calcite saturation")) + burial_eff_fn = burial_eff_omegaCA + else + error("unknown burial_eff_function $(rj.pars.burial_eff_function[])") + end + PB.setfrozen!(rj.pars.burial_eff_function) + + PB.add_method_do!( + rj, + do_burial_eff_carb, + ( + PB.VarList_namedtuple_fields(fluxOceanfloorSolute), + PB.VarList_namedtuple_fields(fluxOceanBurial), + PB.VarList_namedtuple(vars), + ), + p = (CIsotopeType, burial_eff_fn), + ) + + PB.add_method_do_totals_default!(rj) + PB.add_method_initialize_zero_vars_default!(rj) + +end + +function PB.check_configuration(rj::ReactionBurialEffCarb, model::PB.Model) + configok = true + + length(rj.pars.hascarbseddeep) == PB.get_length(rj.domain) || + (configok = false; @error "config error: length(carbseddeep) != Domain size") + + return configok +end + +function do_burial_eff_carb( + m::PB.ReactionMethod, + pars, + (fluxOceanfloorSolute, fluxOceanBurial, vars), + cellrange::PB.AbstractCellRange, + deltat +) + (CIsotopeType, burial_eff_fn) = m.p + + @inbounds for i in cellrange.indices + vars.flys[i] = burial_eff_fn(pars, vars, i) + if pars.hascarbseddeep[i] + ccrate = vars.flys[i] * vars.particulateflux_Ccarb[i] + else + ccrate = 0.0*vars.flys[i]*vars.particulateflux_Ccarb[i] + end + vars.deep_Ccarb[i] = ccrate + + # return all CaCO3 as solute Ca, CO3 except that which is buried + ccremin = vars.particulateflux_Ccarb[i] - ccrate + fluxOceanfloorSolute.DIC[i] += ccremin + fluxOceanfloorSolute.TAlk[i] += 2.0*PB.get_total(ccremin) + PB.add_if_available(fluxOceanfloorSolute.Ca, i, PB.get_total(ccremin)) + + fluxOceanBurial.Ccarb[i] += ccrate + end + + return nothing +end + +# define a template instance so we know the type +const LININTERP_TEMPLATE = Interpolations.LinearInterpolation( + [0.0, 1.0], + [0.0, 1.0], + extrapolation_bc=Interpolations.Flat(), +) + +""" + ReactionBurialEffCorgP + +Burial efficiency (fraction of `particulateflux` input) for Corg and P. + +Input organic matter flux is given by `particulateflux_Corg, N, P`. A fraction of `Corg` and `P` is buried +to output flux `fluxOceanBurial.flux_, Corg, P, Porg, PFe, Pauth`, where `P` is the total P burial flux and +`Porg, PFe, Pauth` are the three P burial mineral phases. The remainder of the input flux is +transferred to `reminflux_Corg, N, P`, where it would usually be linked to a `ReactionRemin` to be remineralized. + +The fraction of `Corg` buried is given by a burial efficiency function, with options set by +Parameter `burial_eff_function`: +- `Prescribed`: Corg burial in cell `i` = Corg particulateflux * `BECorgNorm`*`Parameter BECorg[i]` +- `Ozaki2011`: Corg burial in cell `i` = Corg particulateflux * `BECorgNorm`*`burialEffCorg_Ozaki2011(sedimentation_rate)` +- `ConstantBurialRate`: Corg burial in cell `i` = `BECorgNorm`*`Parameter BECorg[i]` (independent of Corg flux) + +It is also possible to set Parameter `FixedCorgBurialTotal`, in which case the ocean total Corg burial rate is fixed, +and the per-cell Corg burial fluxes calculated as above are then normalized to reach this global total rate. + +P burial efficiency is defined as P:Corg ratios for components Porg, PFe, Pauth by parameters `BPorgCorg`, `BPFeCorg`, `BPauthCorg`. +If these are Vectors of length > 1, they define interpolated functions of oceanfloor `[O2]` on a grid defined by parameter `BPO2`. +If they are all Vectors of length 1, they define fixed Corg:P ratios. + +# Parameters +$(PARS) + +# Methods and Variables +$(METHODS_DO) +""" +Base.@kwdef mutable struct ReactionBurialEffCorgP{P} <: PB.AbstractReaction + base::PB.ReactionBase + + pars::P = PB.ParametersTuple( + PB.ParDouble("BECorgNorm", 1.0, units="", + description="overall normalization factor for Corg burial (or total Corg burial for ConstantBurialRate)"), + PB.ParString("burial_eff_function", "Prescribed", + allowed_values=["Prescribed", "Ozaki2011", "ConstantBurialRate"], + description="Corg burial efficiency parameterisation (or ConstantBurialRate)"), + PB.ParDoubleVec("BECorg", Float64[], units="", + description="prescribed fraction seafloor Corg flux buried (or per-cell fraction of total Corg for ConstantBurialRate)"), + PB.ParDouble("FixedCorgBurialTotal", NaN, units="mol C yr-1", + description="if != NaN, fix total ocean Corg burial rate by renormalizing per-cell fluxes"), + + PB.ParDoubleVec("BPO2", [NaN], units="mol O2 m-3", + description="[O2] points for interpolated oxygen-dependent P:Corg (length 1 for O2-independent P:Corg)"), + PB.ParDoubleVec("BPorgCorg", [0.0], units="mol P (mol Corg)-1", + description="P:Corg for organic P burial fraction at each [O2] (Vector length 1 for O2-independent P:Corg)"), + PB.ParDoubleVec("BPFeCorg", [0.0], units="mol P (mol Corg)-1", + description="P:Corg for Fe-associated P burial fraction at each [O2] (Vector length 1 for O2-independent P:Corg)"), + PB.ParDoubleVec("BPauthCorg", [0.0], units="mol P (mol Corg)-1", + description="P:Corg for CFA-associated P burial fraction at each [O2] (Vector length 1 for O2-independent P:Corg)"), + + PB.ParType(PB.AbstractData, "CIsotope", PB.ScalarData, + external=true, + allowed_values=PB.IsotopeTypes, + description="disable / enable carbon isotopes and specify isotope type"), + ) + + fPorgCorg::typeof(LININTERP_TEMPLATE) = LININTERP_TEMPLATE + fPFeCorg::typeof(LININTERP_TEMPLATE) = LININTERP_TEMPLATE + fPauthCorg::typeof(LININTERP_TEMPLATE) = LININTERP_TEMPLATE +end + +function PB.register_methods!(rj::ReactionBurialEffCorgP) + + CIsotopeType = rj.pars.CIsotope[] + PB.setfrozen!(rj.pars.CIsotope) + @info "register_methods! $(PB.fullname(rj)) CIsotopeType=$CIsotopeType" + + particulateflux = PB.Fluxes.FluxTarget( + "particulateflux_", ["Corg::$CIsotopeType", "P", "N"], + description="input particulate flux", + ) + + reminflux = PB.Fluxes.FluxContrib( + "reminflux_", ["Corg::$CIsotopeType", "P", "N"], + description="output unburied particulate flux", + alloptional=false, + ) + + fluxOceanBurial = PB.Fluxes.FluxContrib( + "fluxOceanBurial.flux_", ["Corg::$CIsotopeType", "Porg", "PFe", "Pauth", "P"], + ) + + vars = PB.VariableReaction[ + PB.VarProp("burial_eff_Corg", "", "Corg burial efficiency"), + PB.VarDep("(sedimentation_rate)", "m yr-1", "sedimentation rate"), + PB.VarDep("(ocean.oceanfloor.O2_conc)", "mol m-3", "O2 concentration"), + ] + + # Corg burial efficiency functions + burial_eff_prescribed(pars, input_flux, vars, i) = pars.BECorgNorm[]*pars.BECorg[i] + + burial_eff_Ozaki2011(pars, input_flux, vars, i) = pars.BECorgNorm[]*burialEffCorg_Ozaki2011(vars.sedimentation_rate[i]) + + function burial_eff_forcedconstant(pars, input_flux, vars, i) + forceconstCorg = pars.BECorgNorm[]*rj.pars.BECorg[i] # mol Corg yr-1 constant forced burial rate + burial_eff = min(1.0, forceconstCorg/(PB.get_total(input_flux.Corg[i])+eps())) + return burial_eff + end + + if rj.pars.burial_eff_function[] == "Prescribed" + burial_eff_fn = burial_eff_prescribed + elseif rj.pars.burial_eff_function[] == "Ozaki2011" + burial_eff_fn = burial_eff_Ozaki2011 + elseif rj.pars.burial_eff_function[] == "ConstantBurialRate" + burial_eff_fn = burial_eff_forcedconstant + else + error("register_methods! $(PB.typename(rj)) $(PB.fullname(rj)) config error: "* + "unknown burial_eff_function $(rj.pars.burial_eff_function[])") + end + PB.setfrozen!(rj.pars.burial_eff_function) + + PB.add_method_setup!(rj, setup_burial_eff_CorgP, (),) + + PB.add_method_do!( + rj, + do_burial_eff_CorgP, + ( + PB.VarList_namedtuple_fields(particulateflux), + PB.VarList_namedtuple_fields(reminflux), + PB.VarList_namedtuple_fields(fluxOceanBurial), + PB.VarList_namedtuple(vars) + ), + p = burial_eff_fn, + ) + + # PB.add_method_do_totals_default!(rj) + PB.add_method_initialize_zero_vars_default!(rj) + + return nothing +end + +function setup_burial_eff_CorgP( + m::PB.ReactionMethod, + pars, + _, + cellrange::PB.AbstractCellRange, + attribute_name, +) + attribute_name == :setup || return + + rj = m.reaction + + if pars.burial_eff_function[] == "Prescribed" + length(pars.BECorg) == PB.get_length(rj.domain) || + error("setup_burial_eff_CorgP! $(PB.fullname(rj)) config error: length(BECorg) != Domain size") + end + + # Corg:P burial ratio functions + all(length(pars.BPO2) .== length.((pars.BPorgCorg, pars.BPauthCorg, pars.BPFeCorg))) || + error("setup_burial_eff_CorgP! $(PB.fullname(rj)) config error: "* + "lengths BPO2, BPorgCorg, BPauthCorg, BPFeCorg differ") + + if length(pars.BPO2) <= 1 + # oxygen-independent - duplicate values so that interpolation -> constant + O2_vals = [-1.0, 1.0] + BPorgCorg_vals = pars.BPorgCorg[[1, 1]] + BPFeCorg_vals = pars.BPFeCorg[[1, 1]] + BPauthCorg_vals = pars.BPauthCorg[[1, 1]] + else + O2_vals = pars.BPO2.v + BPorgCorg_vals = pars.BPorgCorg.v + BPFeCorg_vals = pars.BPFeCorg.v + BPauthCorg_vals = pars.BPauthCorg.v + end + + rj.fPorgCorg = Interpolations.LinearInterpolation( + O2_vals, BPorgCorg_vals, + extrapolation_bc=Interpolations.Flat(), + ) + rj.fPFeCorg = Interpolations.LinearInterpolation( + O2_vals, BPFeCorg_vals, + extrapolation_bc=Interpolations.Flat(), + ) + rj.fPauthCorg = Interpolations.LinearInterpolation( + O2_vals, BPauthCorg_vals, + extrapolation_bc=Interpolations.Flat(), + ) + + return nothing +end + +function do_burial_eff_CorgP( + m::PB.ReactionMethod, + pars, + (particulateflux, reminflux, fluxOceanBurial, vars), + cellrange::PB.AbstractCellRange, + deltat +) + rj = m.reaction + burial_eff_fn = m.p + + if length(pars.BPO2) > 1 + !isnothing(vars.O2_conc) || error("oxygen-dependent C:P burial but no O2_conc variable") + end + + # Calculate Corg burial efficiency first, to allow for optional normalization to pars.FixedCorgBurialTotal + # zero of appropriate type for type stability + burial_Corg_tot = zero(PB.get_total(particulateflux.Corg[1])*burial_eff_fn(pars, particulateflux, vars, 1)) + @inbounds for i in cellrange.indices + # Corg burial from burial efficiency + vars.burial_eff_Corg[i] = burial_eff_fn(pars, particulateflux, vars, i) + burial_Corg_tot += vars.burial_eff_Corg[i]*PB.get_total(particulateflux.Corg[i]) + end + # optionally renormalize burial rate to fixed total Corg burial + totnormfac = if isnan(pars.FixedCorgBurialTotal[]) + # 1.0 of appropriate type for type stability + one(pars.FixedCorgBurialTotal[]/burial_Corg_tot) + else + length(cellrange.indices) == PB.get_length(rj.domain) || + error("FixedCorgBurialTotal can't be used with tiled cellrange") + pars.FixedCorgBurialTotal[]/burial_Corg_tot + end + + # Calculate burial fluxes + @inbounds for i in cellrange.indices + burial_Corg = totnormfac*vars.burial_eff_Corg[i]*particulateflux.Corg[i] + + fluxOceanBurial.Corg[i] += burial_Corg + # @Infiltrator.infiltrate + reminflux.Corg[i] += particulateflux.Corg[i] - burial_Corg + + # no N burial + reminflux.N[i] += particulateflux.N[i] + + # P burial from burial efficiency + O2_conc = PB.get_if_available(vars.O2_conc, i, -1.0) # not needed if oxygen-independent + BPorgCorg, BPFeCorg, BPauthCorg = rj.fPorgCorg(O2_conc), rj.fPFeCorg(O2_conc), rj.fPauthCorg(O2_conc) + + burial_Porg, burial_PFe, burial_Pauth = (BPorgCorg, BPFeCorg, BPauthCorg).*PB.get_total(burial_Corg) + burial_P = burial_Porg + burial_PFe + burial_Pauth + + fluxOceanBurial.Porg[i] += burial_Porg + fluxOceanBurial.PFe[i] += burial_PFe + fluxOceanBurial.Pauth[i] += burial_Pauth + fluxOceanBurial.P[i] += burial_P + + reminflux.P[i] += particulateflux.P[i] - burial_P + if particulateflux.P[i] - burial_P < 0 + @warn "ReactionBurialEffCorgP burial_P exceeds particulateflux_P" + end + + end + + return nothing +end + +""" + burialEffCorg_Ozaki2011(sedimentation_rate) -> BECorg + +Organic carbon burial efficiency `BECorg` (fraction buried), [Ozaki2011](@cite) +as a function of `sedimentation_rate` (m/yr) +```jldoctest; setup = :(import PALEOocean) +julia> round(PALEOocean.Oceanfloor.Burial.burialEffCorg_Ozaki2011(2.369e-3), sigdigits=5) # 100m +0.26767 +julia> round(PALEOocean.Oceanfloor.Burial.burialEffCorg_Ozaki2011(3.4152e-4), sigdigits=5) # 1000m +0.12335 +``` +""" +function burialEffCorg_Ozaki2011(sedimentation_rate) + # eqn (2) burial efficiency + BECorg = (100.0*sedimentation_rate)^0.4/2.1 + + return BECorg +end + + +end diff --git a/src/oceanfloor/Oceanfloor.jl b/src/oceanfloor/Oceanfloor.jl new file mode 100644 index 0000000..df45307 --- /dev/null +++ b/src/oceanfloor/Oceanfloor.jl @@ -0,0 +1,8 @@ +module Oceanfloor + + +import PALEOboxes as PB + +include("Burial.jl") + +end \ No newline at end of file diff --git a/src/oceansurface/AirSeaExchange.jl b/src/oceansurface/AirSeaExchange.jl index ccaf7b1..f18d007 100644 --- a/src/oceansurface/AirSeaExchange.jl +++ b/src/oceansurface/AirSeaExchange.jl @@ -258,7 +258,7 @@ PB.create_reaction(::Type{ReactionAirSeaO2}, base::PB.ReactionBase) = create_ReactionAirSea(base, ScO2, solO2Wann92, false, nothing, "O2", "") """ - ReactionAirSeaO2 + ReactionAirSeaCO2 See [`ReactionAirSea`](@ref) diff --git a/test/configocean3box.yaml b/test/configocean3box.yaml new file mode 100644 index 0000000..fc783b3 --- /dev/null +++ b/test/configocean3box.yaml @@ -0,0 +1,444 @@ +test_airsea_O2: + parameters: + # CIsotope: ScalarData + domains: + global: + # scalar domain + + reactions: + total_O2: + class: ReactionSum + parameters: + vars_to_add: [atm.O, ocean.O2] + variable_links: + sum: total_O2 + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["O2"] + + atm: + + + reactions: + reservoir_O: + class: ReactionReservoirAtm + + variable_links: + R*: O* + pRatm: pO2atm + pRnorm: pO2PAL + variable_attributes: + R:norm_value: 3.7e19 # present-day atmospheric level + R:initial_value: 3.7e19 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + variable_links: + output_O2: O_sms # rename O2 -> O + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_scalar: + class: ReactionReservoirScalar + variable_links: + R*: Scalar* + variable_attributes: + R:initial_value: 1.0 # moles + + reservoir_tracer: + class: ReactionReservoirTotal + variable_links: + R*: Tracer* + variable_attributes: + R:initial_value: 1.0 # concentration m-3 + + reservoir_O2: + class: ReactionReservoir + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 1.0e-3 # concentration m-3 + + + oceansurface: + reactions: + airsea_O2: + class: ReactionAirSeaO2 + parameters: + piston: 3.0 # m d-1 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + # output_CO2: ocean.oceansurface.DIC_sms + + + oceanfloor: + reactions: + +test_bioprodPrest: + parameters: + CIsotope: ScalarData + domains: + global: + # scalar domain + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + + bioprod: + class: ReactionBioProdPrest + + parameters: + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + export: + class: ReactionExportDirect + + parameters: + fluxlist: ["P"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything into bottom cell + + variable_links: + remin_P: P_sms # link export P back to state variable sms for testing + + + oceansurface: + + + oceanfloor: + reactions: + + +test_reminPonly: + parameters: + CIsotope: ScalarData + domains: + global: + # scalar domain + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + + bioprod: + class: ReactionBioProdPrest + + parameters: + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + export: + class: ReactionExportDirect + + parameters: + fluxlist: ["P"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything into bottom cell + + variable_links: + # remin_P: # default is OK + + remin: + class: ReactionReminPonly + + variable_links: + soluteflux_*: "*_sms" + + oceansurface: + + + oceanfloor: + reactions: + +test_reminO2: + parameters: + CIsotope: IsotopeLinear + domains: + global: + + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_P: + class: ReactionReservoirTotal + variable_links: + R*: P* + variable_attributes: + R:initial_value: 2.208e-3 # concentration mol m-3 = 2.15e-6 mol/kg * 1027 kg m-3 + + reservoir_O2: + class: ReactionReservoirTotal + variable_links: + R*: O2* + variable_attributes: + R:initial_value: 300.0e-3 # concentration mol m-3 ~ 300e-6 mol/kg * 1027 kg m-3 + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2000.0e-3 + R:initial_delta: -1.0 + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2000.0e-3 + + const_cisotopes: + class: ReactionScalarConst + parameters: + constnames: ["D_mccb_DIC", "D_B_mccb_mocb"] + variable_attributes: + D_mccb_DIC:initial_value: 0.0 + D_B_mccb_mocb:initial_value: 25.0 + + bioprod: + class: ReactionBioProdPrest + + parameters: + + rCcarbCorg: 0.2 + # restore, frac, none + bioprod: [1, 2, 0] + bioprodval: [0.0, 0.18, .NaN] + variable_links: + prod_*: export_* + + export: + class: ReactionExportDirect + + parameters: + + fluxlist: ["P", "N", "Corg::CIsotope", "Ccarb::CIsotope"] + transportocean: [[0.0,0.0,0.0], + [0.0,0.0,0.0], + [1.0, 1.0, 1.0]] #dump everything into bottom cell + + variable_links: + # remin_P: # default is OK + + remin: + class: ReactionReminO2 + + parameters: + + variable_links: + soluteflux_*: "*_sms" + + oceansurface: + + + oceanfloor: + reactions: + +test_carbchem: + parameters: + solve_pH: solve + CIsotope: IsotopeLinear + domains: + global: + + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2000.0e-3 + R:initial_delta: -1.0 + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2000.0e-3 + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega"] + defaultconcs: ["TS", "TF", "TB", "Ca"] + solve_pH: external%solve_pH + outputs: ["pCO2", "xCO2dryinp", "CO3", "OmegaCA", "OmegaAR"] + variable_links: + TCi_conc: DIC_conc + + oceansurface: + + + oceanfloor: + reactions: + + + +test_airsea_CO2: + parameters: + CIsotope: IsotopeLinear + domains: + global: + # scalar domain + + reactions: + total_C: + class: ReactionSum + parameters: + vars_to_add: [atm.CO2, ocean.DIC] + variable_links: + sum: total_C + + fluxAtmtoOceansurface: + reactions: + fluxtarget: + class: ReactionFluxTarget + + parameters: + flux_totals: true + fluxlist: ["CO2::CIsotope"] + + atm: + + + reactions: + reservoir_CO2: + class: ReactionReservoirAtm + parameters: + field_data: external%CIsotope + variable_links: + R*: CO2* + pRatm: pCO2atm + pRnorm: pCO2PAL + variable_attributes: + R:norm_value: 4.956e16 # pre ind 280e-6 + R:initial_value: 4.956e16 # pre ind 280e-6 + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Distribute + transfer_multiplier: -1.0 + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: $fluxname$_sms + variable_links: + + ocean: + reactions: + transport3box: + class: ReactionOceanTransport3box + + reservoir_DIC: + class: ReactionReservoirTotal + parameters: + field_data: external%CIsotope + variable_links: + R*: DIC* + variable_attributes: + R:initial_value: 2000.0e-3 + R:initial_delta: -1.0 + + reservoir_TAlk: + class: ReactionReservoirTotal + variable_links: + R*: TAlk* + variable_attributes: + R:initial_value: 2000.0e-3 + + carbchem: + class: ReactionCO2SYS + parameters: + components: ["Ci", "B", "S", "F", "Omega"] + defaultconcs: ["TS", "TF", "TB", "Ca"] + solve_pH: constraint # Hfree as DAE variable + outputs: ["pCO2", "xCO2dryinp", "CO2", "CO3", "OmegaCA", "OmegaAR"] + variable_links: + TCi_conc: DIC_conc + CO2: CO2_conc + pCO2: pCO2 + OmegaAR: OmegaAR + Ca_conc: Ca_conc + + + oceansurface: + reactions: + airsea_CO2: + class: ReactionAirSeaCO2 + parameters: + + piston: 3.0 # m d-1 + + variable_links: + Xatm_delta: atm.CO2_delta + Xocean_delta: ocean.oceansurface.DIC_delta + + transfer_AtmtoOceansurface: + class: ReactionFluxTransfer + parameters: + transfer_matrix: Identity + input_fluxes: fluxAtmtoOceansurface.flux_$fluxname$ + output_fluxes: ocean.oceansurface.$fluxname$_sms + variable_links: + output_CO2: ocean.oceansurface.DIC_sms + + oceanfloor: + reactions: diff --git a/test/configoceanTMM.yaml b/test/configoceanTMM.yaml new file mode 100644 index 0000000..9ebc4e9 --- /dev/null +++ b/test/configoceanTMM.yaml @@ -0,0 +1,109 @@ +test_trspt_read: + parameters: + + domains: + global: + + + ocean: + reactions: + force_temperature: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Theta_gcm.mat + data_var: Tgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + constant_offset: 273.15 # convert C -> Kelvin + variable_links: + F: temp + + force_salinity: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/GCM/Salt_gcm.mat + data_var: Sgcm + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: sal + + reservoir_tracer: + class: ReactionReservoirTotal + variable_links: + R*: Tracer* + variable_attributes: + R:initial_value: 1.0 # concentration m-3 + + # reservoir_tracer2: + # class: ReactionReservoirTotal + # variable_links: + # R*: Tracer2* + # variable_attributes: + # R:initial_value: 1.0 # concentration m-3 + + # reservoir_tracer3: + # class: ReactionReservoirTotal + # variable_links: + # R*: Tracer3* + # variable_attributes: + # R:initial_value: 1.0 # concentration m-3 + + # reservoir_tracer4: + # class: ReactionReservoirTotal + # variable_links: + # R*: Tracer4* + # variable_attributes: + # R:initial_value: 1.0 # concentration m-3 + + + transportMITgcm: + class: ReactionOceanTransportTMM + parameters: + base_path: $TMMDir$/MITgcm_2.8deg + pack_chunk_width: 4 + + + oceansurface: + reactions: + force_par: + class: ReactionForceInsolation + + variable_links: + insolation: surface_downwelling_photosynthetic_radiative_flux + + force_wind_speed: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/wind_speed.mat + data_var: windspeed + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + variable_links: + F: wind_speed + + + force_open_area_fraction: + class: ReactionForceGrid + parameters: + matlab_file: $TMMDir$/MITgcm_2.8deg/BiogeochemData/ice_fraction.mat + data_var: Fice + time_var: "" + tidx_start: 1 + tidx_end: 12 + cycle_time: 1.0 + scale: -1.0 # convert sea ice fraction to open area (0 - 1) + constant_offset: 1.0 + variable_links: + F: open_area_fraction + + + oceanfloor: + reactions: + diff --git a/test/runocean3boxtests.jl b/test/runocean3boxtests.jl new file mode 100644 index 0000000..10637de --- /dev/null +++ b/test/runocean3boxtests.jl @@ -0,0 +1,348 @@ + +using Test +using Logging +using DiffEqBase +using Sundials +using Plots + +import PALEOboxes as PB +import PALEOocean +import PALEOmodel + +@testset "Ocean3box" begin + +include("../examples/atmreservoirreaction.jl") # temporary solution to make ReactionReservoirAtm available + +skipped_testsets = [ + # "Atm Ocean 3 box CO2", + # "Ocean 3 box remin O2", + # "Ocean 3 box remin Ponly", + # "Ocean 3 box bioprod P restore", + # "Ocean 3 box airsea O2", +] + +configfile = joinpath(@__DIR__, "configocean3box.yaml") + +!("Atm Ocean 3 box CO2" in skipped_testsets) && @testset "Atm Ocean 3 box CO2" begin + + model = PB.create_model_from_config(configfile, "test_airsea_CO2") + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + # DAE with ForwardDiff sparse Jacobian + + PALEOmodel.ODE.integrateDAEForwardDiff(paleorun, initial_state, modeldata, (0, 1e5), alg=IDA(linear_solver=:KLU)) + + + for domainname in ("global", "atm", "ocean") + println(domainname, " variables after integrate to steady-state:") + domain = PB.get_domain(model, domainname) + vars = PB.get_variables(domain) + for var in vars + println("\t", PB.fullname(var), " = ", PB.get_data(var, modeldata)) + end + end + + # plot concentration + display(plot(title="Total C", paleorun.output, ["global.total_C"], ylabel="C (mol)",)) + display(plot(title="Total C moldelta", paleorun.output, ["global.total_C.v_moldelta"], ylabel="C_moldelta (mol * delta)",)) + plot(title="Total C components", paleorun.output, ["global.total_C", "atm.CO2"], ylabel="C (mol)",) + display(plot!(paleorun.output, "ocean.DIC", (cell=[:s, :h, :d], ))) + plot(title="Air-sea CO2", paleorun.output, "fluxAtmtoOceansurface.flux_CO2", (cell=[:s, :h], ), ylabel="mol yr-1", xlim=(0, 1e3)) + display(plot!(paleorun.output, "fluxAtmtoOceansurface.flux_total_CO2")) + display(plot(title="pH (total)", paleorun.output, "ocean.pHtot", (cell=[:s, :h, :d], ), ylabel="pH (total)", xlim=(0, 1e3))) + plot(title="d13C", paleorun.output, "atm.CO2_delta",ylabel="per mil", xlim=(0, 1e3)) + display(plot(paleorun.output, "ocean.DIC_delta", (cell=[:s, :h, :d], ))) + + # test conservation + total_C = PB.get_property( + PB.get_data(paleorun.output, "global.total_C"), + propertyname=:v + ) + @test abs(total_C[end] - total_C[1])/total_C[1] < 1e-10 + total_C_moldelta = PB.get_property( + PB.get_data(paleorun.output, "global.total_C"), + propertyname=:v_moldelta + ) + @test abs(total_C_moldelta[end] - total_C_moldelta[1])/total_C_moldelta[1] < 1e-10 + + # test pCO2 + sigdigits = 10 + @test round.(PB.get_data(paleorun.output, "atm.pCO2atm")[end], sigdigits=sigdigits) == + round(0.000651920453771681, sigdigits=sigdigits) + +end + + +!("Ocean 3 box remin O2" in skipped_testsets) && @testset "Ocean 3 box remin O2" begin + + model = PB.create_model_from_config(configfile, "test_reminO2") + + ocean_domain = PB.get_domain(model, "ocean") + @test PB.get_length(ocean_domain) == 3 + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + ocean_modelcreated_vars_dict = Dict([(var.name, var) for var in PB.get_variables(ocean_domain, hostdep=false)]) + + + # Check model derivative + + PB.do_deriv(modeldata.dispatchlists_all) + + println("state, sms variables after check model derivative:") + for (state_var, sms_var) in PB.IteratorUtils.zipstrict( + PB.get_vars(modeldata.solver_view_all.stateexplicit), + PB.get_vars(modeldata.solver_view_all.stateexplicit_deriv) + ) + println(PB.fullname(state_var), " ", PB.get_data(state_var, modeldata)) + println(PB.fullname(sms_var), " ", PB.get_data(sms_var, modeldata)) + end + + println("ocean model created variables after check model derivative:") + for (name, var ) in ocean_modelcreated_vars_dict + println("\t", PB.fullname(var), " = ", PB.get_data(var, modeldata)) + end + + # check conservation + ocean_P_sms_data = PB.get_data(PB.get_variable(ocean_domain,"P_sms"), modeldata) + sum_P_sms = sum(ocean_P_sms_data) + println("check conservation: sum(P_sms)=",sum_P_sms) + @test abs(sum_P_sms) < 1e-16 + + ocean_O2_sms_data = PB.get_data(PB.get_variable(ocean_domain,"O2_sms"), modeldata) + sum_O2_sms = sum(ocean_O2_sms_data) + println("check conservation: sum(O2_sms)=",sum_O2_sms) + @test abs(sum_O2_sms) < 1e-16 + + # integrate to approx steady state + PALEOmodel.ODE.integrate(paleorun, initial_state, modeldata, (0, 1e5)) # first run includes JIT time + + # show(paleorun.output.domains["ocean"].data, allcols=true) + # println() + + # check conservation + P_total = PB.get_data(paleorun.output, "ocean.P_total") + @test abs(P_total[end] - P_total[1])/P_total[1] < 1e-10 + + O2_total = PB.get_data(paleorun.output, "ocean.O2_total") + @test abs(O2_total[end] - O2_total[1])/O2_total[1] < 1e-10 + + DIC_total = PB.get_property( + PB.get_data(paleorun.output, "ocean.DIC_total"), + propertyname=:v + ) + @test abs(DIC_total[end] - DIC_total[1])/DIC_total[1] < 1e-10 + DIC_total_moldelta = PB.get_property( + PB.get_data(paleorun.output, "ocean.DIC_total"), + propertyname=:v_moldelta + ) + @test abs(DIC_total_moldelta[end] - DIC_total_moldelta[1])/DIC_total_moldelta[1] < 1e-10 + + TAlk_total = PB.get_data(paleorun.output, "ocean.TAlk_total") + @test abs(TAlk_total[end] - TAlk_total[1])/TAlk_total[1] < 1e-10 +end + + + +!("Ocean 3 box remin Ponly" in skipped_testsets) && @testset "Ocean 3 box remin Ponly" begin + + model = PB.create_model_from_config(configfile, "test_reminPonly") + + + ocean_domain = PB.get_domain(model, "ocean") + @test PB.get_length(ocean_domain) == 3 + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + ocean_modelcreated_vars_dict = Dict([(var.name, var) for var in PB.get_variables(ocean_domain, hostdep=false)]) + + + # Check model derivative + + PB.do_deriv(modeldata.dispatchlists_all) + + println("state, sms variables after check model derivative:") + for (state_var, sms_var) in PB.IteratorUtils.zipstrict( + PB.get_vars(modeldata.solver_view_all.stateexplicit), + PB.get_vars(modeldata.solver_view_all.stateexplicit_deriv) + ) + println(PB.fullname(state_var), " ", PB.get_data(state_var, modeldata)) + println(PB.fullname(sms_var), " ", PB.get_data(sms_var, modeldata)) + end + + println("ocean model created variables after check model derivative:") + for (name, var ) in ocean_modelcreated_vars_dict + println("\t", PB.fullname(var), " = ", PB.get_data(var, modeldata)) + end + + # check conservation + ocean_P_sms_data = PB.get_data(PB.get_variable(ocean_domain,"P_sms"), modeldata) + sum_P_sms = sum(ocean_P_sms_data) + println("check conservation: sum(P_sms)=",sum_P_sms) + @test abs(sum_P_sms) < 1e-16 + + # integrate to approx steady state + PALEOmodel.ODE.integrate(paleorun, initial_state, modeldata, (0, 1e5)) # first run includes JIT time + + # show(paleorun.output["ocean"], allcols=true) + println() + + # check conservation + P_total = PB.get_data(paleorun.output, "ocean.P_total") + @test abs(P_total[end] - P_total[1])/P_total[1] < 1e-10 +end + + + +!("Ocean 3 box bioprod P restore" in skipped_testsets) && @testset "Ocean 3 box bioprod P restore" begin + + model = PB.create_model_from_config(configfile, "test_bioprodPrest") + + ocean_domain = PB.get_domain(model, "ocean") + @test PB.get_length(ocean_domain) == 3 + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + ocean_modelcreated_vars_dict = Dict([(var.name, var) for var in PB.get_variables(ocean_domain, hostdep=false)]) + + + # Check model derivative + + PB.do_deriv(modeldata.dispatchlists_all) + + println("state, sms variables after check model derivative:") + for (state_var, sms_var) in PB.IteratorUtils.zipstrict( + PB.get_vars(modeldata.solver_view_all.stateexplicit), + PB.get_vars(modeldata.solver_view_all.stateexplicit_deriv) + ) + println(PB.fullname(state_var), " ", PB.get_data(state_var, modeldata)) + println(PB.fullname(sms_var), " ", PB.get_data(sms_var, modeldata)) + end + + println("ocean model created variables after check model derivative:") + for (name, var ) in ocean_modelcreated_vars_dict + println("\t", PB.fullname(var), " = ", PB.get_data(var, modeldata)) + end + + # check conservation + ocean_P_sms_data = PB.get_data(PB.get_variable(ocean_domain,"P_sms"), modeldata) + sum_P_sms = sum(ocean_P_sms_data) + println("check conservation: sum(P_sms)=",sum_P_sms) + @test abs(sum_P_sms) < 1e-16 + + # integrate to approx steady state + PALEOmodel.ODE.integrate(paleorun, initial_state, modeldata, (0, 1e5)) # first run includes JIT time + + # show(paleorun.output["ocean"], allcols=true) + # println() + + # check conservation + P_total = PB.get_data(paleorun.output, "ocean.P_total") + @test abs(P_total[end] - P_total[1])/P_total[1] < 1e-10 +end + + + +!("Ocean 3 box airsea O2" in skipped_testsets) && @testset "Ocean 3 box airsea O2" begin + + model = PB.create_model_from_config(configfile, "test_airsea_O2") + + # Test OceanBase domain configuration + @test PB.get_num_domains(model) == 6 + + global_domain = PB.get_domain(model, "global") + @test PB.get_length(global_domain) == 1 + + ocean_domain = PB.get_domain(model, "ocean") + @test PB.get_length(ocean_domain) == 3 + + @test ocean_domain.grid.subdomains["oceansurface"].indices == [1, 2] + @test ocean_domain.grid.subdomains["oceanfloor"].indices == [1, 2, 3] + + oceansurface_domain = PB.get_domain(model, "oceansurface") + @test PB.get_length(oceansurface_domain) == 2 + + oceanfloor_domain = PB.get_domain(model, "oceanfloor") + @test PB.get_length(oceanfloor_domain) == 3 + + # test OceanBase variables + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + ocean_modelcreated_vars_dict = Dict([(var.name, var) for var in PB.get_variables(ocean_domain, hostdep=false)]) + + println("ocean model created variables after initialize!:") + for (name, var ) in ocean_modelcreated_vars_dict + println("\t", PB.fullname(var), " = ", PB.get_data(var, modeldata)) + end + + # test a constant variable + @test PB.get_data(ocean_modelcreated_vars_dict["zupper"], modeldata) == [0.0, 0.0, -100.0] + + + # bodge a test for ocean tracer with single non-zero cell + ocean_Tracer_data = PB.get_data(PB.get_variable(ocean_domain,"Tracer"), modeldata) + ocean_Tracer_data .= [1.0, 0.0, 0.0] + # update initial_state with our bodged values + initial_state = PALEOmodel.get_statevar(modeldata.solver_view_all) + + # Check model derivative + + PB.do_deriv(modeldata.dispatchlists_all) + + println("state, sms variables after check model derivative:") + for (state_var, sms_var) in PB.IteratorUtils.zipstrict( + PB.get_vars(modeldata.solver_view_all.stateexplicit), + PB.get_vars(modeldata.solver_view_all.stateexplicit_deriv) + ) + println(PB.fullname(state_var), " ", PB.get_data(state_var, modeldata)) + println(PB.fullname(sms_var), " ", PB.get_data(sms_var, modeldata)) + end + + # check conservation + ocean_Tracer_sms_data = PB.get_data(PB.get_variable(ocean_domain,"Tracer_sms"), modeldata) + sum_Tracer_sms = sum(ocean_Tracer_sms_data) + println("check conservation: sum(Tracer_sms)=",sum_Tracer_sms) + @test abs(sum_Tracer_sms) < 1e-16 + + # integrate to approx steady state + PALEOmodel.ODE.integrate(paleorun, initial_state, modeldata, (0, 1e5)) # first run includes JIT time + #show(paleorun.output["fluxAtmtoOceansurface"], allcols=true) + #println() + #show(paleorun.output["atm"], allcols=true) + #println() + #show(paleorun.output["ocean"], allcols=true) + #println() + + # check conservation + tracer_total = PB.get_data(paleorun.output, "ocean.Tracer_total") + @test abs(tracer_total[1] - 1.0) < 1e-16 + @test abs(tracer_total[end] - tracer_total[1]) < 1e-4 + + total_O2 = PB.get_data(paleorun.output, "global.total_O2") + @test abs(total_O2[end] - total_O2[1]) < 1e-7*total_O2[1] + + # plot concentration + display(plot(title="Tracer total", paleorun.output, ["ocean.Tracer_total"], ylabel="total (mol)",)) + display(plot(title="Tracer concentration", paleorun.output, "ocean.Tracer_conc", (cell=[:s, :h, :d], ), ylabel="conc (mol m-3)", xlim=(0, 1e3))) + display(plot(title="Total O2", paleorun.output, ["global.total_O2"], ylabel="O2 (mol)",)) + plot(title="Total O2 components", paleorun.output, ["global.total_O2", "atm.O"], ylabel="O2 (mol)",) + display(plot!(paleorun.output, "ocean.O2", (cell=[:s, :h, :d], ))) + plot(title="Air-sea O2", paleorun.output, "fluxAtmtoOceansurface.flux_O2", (cell=[:s, :h], ), ylabel="mol yr-1", xlim=(0, 1e3)) + display(plot!(paleorun.output, "fluxAtmtoOceansurface.flux_total_O2")) + + model = nothing +end + +end + diff --git a/test/runoceanMITgcmtests.jl b/test/runoceanMITgcmtests.jl new file mode 100644 index 0000000..9ba6a5c --- /dev/null +++ b/test/runoceanMITgcmtests.jl @@ -0,0 +1,109 @@ +using Test +using BenchmarkTools + +import PALEOboxes as PB + +import PALEOocean +import PALEOmodel + +using Plots + +@testset "MITgcm" begin + +skipped_testsets = [ + # "forcing", + # "transport" +] + +do_plots = false + +!("forcing" in skipped_testsets) && @testset "forcing" begin + + model = PB.create_model_from_config( + joinpath(@__DIR__, "configoceanTMM.yaml"), "test_trspt_read") + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + # integrate to approx steady state + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, (0, 0.25, 0.5, 0.75, 1.0), 1/365) + + show(PB.show_variables(model), allrows=true) + println() + + if do_plots + pager = PALEOmodel.PlotPager((2, 2)) + else + pager = PALEOmodel.NoPlotPager() + end + + for t in [0.0, 0.5] + pager( + heatmap(paleorun.output, "oceansurface.surface_downwelling_photosynthetic_radiative_flux", (tmodel=t,), swap_xy=true), + heatmap(paleorun.output, "oceansurface.open_area_fraction", (tmodel=t,), swap_xy=true), + ) + end + +end + +!("transport" in skipped_testsets) && @testset "transport" begin + + model = PB.create_model_from_config( + joinpath(@__DIR__, "configoceanTMM.yaml"), "test_trspt_read") + + initial_state, modeldata = PALEOmodel.initialize!(model) + + paleorun = PALEOmodel.Run(model=model, output = PALEOmodel.OutputWriters.OutputMemory()) + + # bodge a test for ocean tracer with single non-zero cell + ocean_domain = PB.get_domain(model, "ocean") + ocean_Tracer_data = PB.get_data(PB.get_variable(ocean_domain,"Tracer"), modeldata) + Tracer_total = sum(ocean_Tracer_data) + ocean_Tracer_data .= 0.0 + ocean_Tracer_data[1] = Tracer_total + # update initial_state with our bodged values + initial_state = PALEOmodel.get_statevar(modeldata.solver_view_all) + + # integrate to approx steady state + # toutputs = [0, 0.25, 0.5, 0.75, 1.0, 10.0, 100.0, 1000.0] + toutputs = [0, 0.25, 0.5, 0.75, 1.0] + @time PALEOmodel.ODEfixed.integrateEuler(paleorun, initial_state, modeldata, toutputs , 1/365) + + show(PB.show_variables(model), allrows=true) + println() + + # check conservation + tracer_total = PB.get_data(paleorun.output, "ocean.Tracer_total") + @test isapprox(tracer_total[1], Tracer_total, rtol=1e-16) + @test isapprox(tracer_total[end], tracer_total[1], rtol=1e-4) + + if do_plots + pager = PALEOmodel.PlotPager((2, 2)) + else + pager = PALEOmodel.NoPlotPager() + end + + for t in [0.0, 0.5] + pager( + heatmap(paleorun.output, "oceansurface.surface_downwelling_photosynthetic_radiative_flux", (tmodel=t,), swap_xy=true), + heatmap(paleorun.output, "oceansurface.open_area_fraction", (tmodel=t,), swap_xy=true), + heatmap(paleorun.output, "ocean.temp", (tmodel=t, k=1), swap_xy=true), + ) + end + + for t in [0.0, 1.0] + pager( + heatmap(paleorun.output, "ocean.Tracer_conc", (tmodel=t, k=1), swap_xy=true), + ) + end + + pager( + plot(title="Tracer total", paleorun.output, ["ocean.Tracer_total"], ylabel="total (mol)",) + ) + +end + + + +end # testset diff --git a/test/runtests.jl b/test/runtests.jl index aedccf2..8878622 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,11 +3,28 @@ using Test using Documenter +ENV["GKSwstype"] = "100" # to run Plots.jl GR on a headless system @testset "PALEOocean all" begin +@testset "PALEOocean test" begin + include("runocean3boxtests.jl") + + # include("runoceanMITgcmtests.jl") +end + +@testset "PALEOocean/examples" begin + include("../examples/transport_examples/runtests.jl") +include("../examples/ocean3box/runtests.jl") + +include("../examples/PTBClarkson2014/runtests.jl") + +end + doctest(PALEOocean; manual=false) -end \ No newline at end of file +end + +delete!(ENV, "GKSwstype"); # undo workaround for Plots.jl on a headless system \ No newline at end of file