From 3ccf195b18bdfceddc5e8dd5b645b837cf2b0f5a Mon Sep 17 00:00:00 2001 From: Julia Sloan Date: Tue, 19 May 2026 17:10:24 -0700 Subject: [PATCH] expose diagnostics frequency and reduction for each component --- NEWS.md | 13 ++++ .../cmip_oceananigans_climaseaice.yml | 10 ++- .../cmip_oceananigans_climaseaice_bucket.yml | 10 ++- config/longrun_configs/cmip_diagedmf_land.yml | 16 +++-- config/longrun_configs/cmip_edonly_bucket.yml | 15 +++-- config/longrun_configs/cmip_edonly_land.yml | 15 ++++- config/longrun_configs/cmip_progedmf_land.yml | 16 +++-- docs/src/input.md | 9 +++ .../climaland_bucket.jl | 7 +- .../climaland_integrated.jl | 7 +- src/Input.jl | 61 ++++++++++++++++- src/SimCoordinator.jl | 12 +++- src/SimOutput/diagnostics.jl | 66 +++++++++++++++---- test/input_tests.jl | 14 ++++ test/sim_output_tests.jl | 8 +++ 15 files changed, 239 insertions(+), 40 deletions(-) diff --git a/NEWS.md b/NEWS.md index 76fb6e7f27..a6f1a62e27 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,19 @@ ClimaCoupler.jl Release Notes `main` ------- +#### Configurable land and coupler diagnostics output frequency and reduction +Adds new config options to control the period and reduction type of land and +coupler diagnostics, matching the existing options for ocean and sea-ice: +- `land_diagnostics_period` (default `"monthly"`) and + `land_diagnostics_reduction` (default `"average"`) +- `coupler_diagnostics_period` (default `nothing`, falls back to the + auto-derived `get_diag_period`) and `coupler_diagnostics_reduction` + (default `"average"`) + +Also fixes the calls to `ClimaLand.default_diagnostics` in the ClimaLand +extension to pass `outdir` positionally, so the dispatched method actually +produces land diagnostics instead of falling through to the no-op fallback. + #### Update SST, SIC at monthly frequency PR[#1926](https://github.com/CliMA/ClimaCoupler.jl/pull/1926) For prescribed ocean and sea ice models, read in SST and SIC data monthly instead of at the model timestep. These data have diff --git a/config/ci_configs/cmip_oceananigans_climaseaice.yml b/config/ci_configs/cmip_oceananigans_climaseaice.yml index 52c95f9951..47fbabb216 100644 --- a/config/ci_configs/cmip_oceananigans_climaseaice.yml +++ b/config/ci_configs/cmip_oceananigans_climaseaice.yml @@ -1,6 +1,8 @@ FLOAT_TYPE: "Float32" albedo_model: "CouplerAlbedo" atmos_config_file: "config/atmos_configs/climaatmos_edonly.yml" +coupler_diagnostics_period: "1hours" +coupler_diagnostics_reduction: "instantaneous" coupler_toml: ["toml/amip_edonly.toml"] dt_atmos: "120secs" # 2 minutes dt_cpl: "360secs" # 6 minutes @@ -11,18 +13,24 @@ dz_bottom: 100.0 energy_check: false h_elem: 8 ice_model: "clima_seaice" +land_diagnostics_period: "1hours" +land_diagnostics_reduction: "instantaneous" land_model: "integrated" land_spun_up_ic: false mode_name: "cmip" netcdf_output_at_levels: true +ocean_diagnostic_interval: "1hours" +ocean_diagnostic_mode: "instantaneous" ocean_model: "oceananigans" output_default_diagnostics: true radiation_reset_rng_seed: true rayleigh_sponge: true +seaice_diagnostic_interval: "1hours" +seaice_diagnostic_mode: "instantaneous" simple_ocean: true start_date: "20100101" surface_setup: "PrescribedSurface" -t_end: "12hours" +t_end: "3hours" topo_smoothing: true topography: "Earth" viscous_sponge: true diff --git a/config/ci_configs/cmip_oceananigans_climaseaice_bucket.yml b/config/ci_configs/cmip_oceananigans_climaseaice_bucket.yml index cdee969527..efa4ff92f1 100644 --- a/config/ci_configs/cmip_oceananigans_climaseaice_bucket.yml +++ b/config/ci_configs/cmip_oceananigans_climaseaice_bucket.yml @@ -2,6 +2,8 @@ FLOAT_TYPE: "Float32" albedo_model: "CouplerAlbedo" atmos_config_file: "config/atmos_configs/climaatmos_edonly.yml" bucket_albedo_type: "map_temporal" +coupler_diagnostics_period: "1hours" +coupler_diagnostics_reduction: "instantaneous" coupler_toml: ["toml/amip_edonly.toml"] dt_atmos: "120secs" # 2 minutes dt_cpl: "360secs" # 6 minutes @@ -12,16 +14,22 @@ dz_bottom: 100.0 energy_check: false h_elem: 8 ice_model: "clima_seaice" +land_diagnostics_period: "1hours" +land_diagnostics_reduction: "instantaneous" mode_name: "cmip" netcdf_output_at_levels: true +ocean_diagnostic_interval: "1hours" +ocean_diagnostic_mode: "instantaneous" ocean_model: "oceananigans" output_default_diagnostics: true radiation_reset_rng_seed: true rayleigh_sponge: true +seaice_diagnostic_interval: "1hours" +seaice_diagnostic_mode: "instantaneous" simple_ocean: true start_date: "20100101" surface_setup: "PrescribedSurface" -t_end: "12hours" +t_end: "3hours" topo_smoothing: true topography: "Earth" viscous_sponge: true diff --git a/config/longrun_configs/cmip_diagedmf_land.yml b/config/longrun_configs/cmip_diagedmf_land.yml index c92a505001..07e2667f8a 100644 --- a/config/longrun_configs/cmip_diagedmf_land.yml +++ b/config/longrun_configs/cmip_diagedmf_land.yml @@ -2,6 +2,8 @@ FLOAT_TYPE: "Float64" albedo_model: "CouplerAlbedo" atmos_config_file: "config/atmos_configs/climaatmos_diagedmf.yml" checkpoint_dt: "1months" +coupler_diagnostics_period: "1days" +coupler_diagnostics_reduction: "instantaneous" coupler_toml: ["toml/amip_diagedmf.toml"] dt_atmos: "120secs" # 2 minutes dt_cloud_fraction: "1hours" @@ -13,18 +15,24 @@ dt_seaice: "360secs" # 6 minutes energy_check: false extra_atmos_diagnostics: - short_name: [hur, hus, ta] - period: 1months - reduction_time: average + period: 1days + reduction_time: inst pressure_coordinates: true - compute_every: 6hours + compute_every: 1days ice_model: "clima_seaice" insolation: "timevarying" +land_diagnostics_period: "1days" +land_diagnostics_reduction: "instantaneous" land_model: "integrated" land_spun_up_ic: false mode_name: "cmip" ocean_model: "oceananigans" -output_default_diagnostics: true +ocean_diagnostic_interval: "1days" +ocean_diagnostic_mode: "instantaneous" +output_default_diagnostics: false rmse_check: true +seaice_diagnostic_interval: "1days" +seaice_diagnostic_mode: "instantaneous" start_date: "20100101" surface_setup: "PrescribedSurface" t_end: "186days" diff --git a/config/longrun_configs/cmip_edonly_bucket.yml b/config/longrun_configs/cmip_edonly_bucket.yml index 5d84e516f4..7ea02154f6 100644 --- a/config/longrun_configs/cmip_edonly_bucket.yml +++ b/config/longrun_configs/cmip_edonly_bucket.yml @@ -2,6 +2,8 @@ FLOAT_TYPE: "Float64" atmos_config_file: "config/atmos_configs/climaatmos_edonly.yml" bucket_albedo_type: "map_temporal" checkpoint_dt: "1months" +coupler_diagnostics_period: "1days" +coupler_diagnostics_reduction: "instantaneous" dt_atmos: "120secs" # 2 minutes dt_cloud_fraction: "1hours" dt_cpl: "360secs" # 6 minutes @@ -12,12 +14,15 @@ dt_seaice: "360secs" # 6 minutes energy_check: false ice_model: "clima_seaice" insolation: "timevarying" +land_diagnostics_period: "1days" +land_diagnostics_reduction: "instantaneous" mode_name: "cmip" ocean_model: "oceananigans" ocean_diagnostic_interval: "1days" -ocean_diagnostic_mode: "averaged" +ocean_diagnostic_mode: "instantaneous" +output_default_diagnostics: false seaice_diagnostic_interval: "1days" -seaice_diagnostic_mode: "averaged" +seaice_diagnostic_mode: "instantaneous" rmse_check: true start_date: "20100101" surface_setup: "PrescribedSurface" @@ -26,7 +31,7 @@ topo_smoothing: true topography: "Earth" extra_atmos_diagnostics: - short_name: [hur, hus, ta] - period: 1months - reduction_time: average + period: 1days + reduction_time: inst pressure_coordinates: true - compute_every: 6hours + compute_every: 1days diff --git a/config/longrun_configs/cmip_edonly_land.yml b/config/longrun_configs/cmip_edonly_land.yml index 5022f62a39..9c3da3189f 100644 --- a/config/longrun_configs/cmip_edonly_land.yml +++ b/config/longrun_configs/cmip_edonly_land.yml @@ -1,6 +1,8 @@ FLOAT_TYPE: "Float64" atmos_config_file: "config/atmos_configs/climaatmos_edonly.yml" checkpoint_dt: "1months" +coupler_diagnostics_period: "1days" +coupler_diagnostics_reduction: "instantaneous" dt_atmos: "120secs" # 2 minutes dt_cloud_fraction: "1hours" dt_cpl: "360secs" # 6 minutes @@ -11,17 +13,24 @@ dt_seaice: "360secs" # 6 minutes energy_check: false extra_atmos_diagnostics: - short_name: [hur, hus, ta] - period: 1months - reduction_time: average + period: 1days + reduction_time: inst pressure_coordinates: true - compute_every: 6hours + compute_every: 1days ice_model: "clima_seaice" insolation: "timevarying" +land_diagnostics_period: "1days" +land_diagnostics_reduction: "instantaneous" land_model: "integrated" land_spun_up_ic: false mode_name: "cmip" ocean_model: "oceananigans" +ocean_diagnostic_interval: "1days" +ocean_diagnostic_mode: "instantaneous" +output_default_diagnostics: false rmse_check: true +seaice_diagnostic_interval: "1days" +seaice_diagnostic_mode: "instantaneous" start_date: "20100101" surface_setup: "PrescribedSurface" t_end: "366days" diff --git a/config/longrun_configs/cmip_progedmf_land.yml b/config/longrun_configs/cmip_progedmf_land.yml index 9a20d95be2..32e25196fd 100644 --- a/config/longrun_configs/cmip_progedmf_land.yml +++ b/config/longrun_configs/cmip_progedmf_land.yml @@ -2,6 +2,8 @@ FLOAT_TYPE: "Float64" albedo_model: "CouplerAlbedo" atmos_config_file: "config/atmos_configs/climaatmos_progedmf.yml" checkpoint_dt: "1months" +coupler_diagnostics_period: "1days" +coupler_diagnostics_reduction: "instantaneous" coupler_toml: ["toml/amip_progedmf.toml"] dt_atmos: "120secs" # 2 minutes dt_cloud_fraction: "1hours" @@ -13,18 +15,24 @@ dt_seaice: "360secs" # 6 minutes energy_check: false extra_atmos_diagnostics: - short_name: [hur, hus, ta] - period: 1months - reduction_time: average + period: 1days + reduction_time: inst pressure_coordinates: true - compute_every: 6hours + compute_every: 1days ice_model: "clima_seaice" insolation: "timevarying" +land_diagnostics_period: "1days" +land_diagnostics_reduction: "instantaneous" land_model: "integrated" land_spun_up_ic: false mode_name: "cmip" ocean_model: "oceananigans" -output_default_diagnostics: true +ocean_diagnostic_interval: "1days" +ocean_diagnostic_mode: "instantaneous" +output_default_diagnostics: false rmse_check: true +seaice_diagnostic_interval: "1days" +seaice_diagnostic_mode: "instantaneous" start_date: "20100101" surface_setup: "PrescribedSurface" t_end: "186days" diff --git a/docs/src/input.md b/docs/src/input.md index a8781b3237..6ea1d780a3 100644 --- a/docs/src/input.md +++ b/docs/src/input.md @@ -28,6 +28,11 @@ multiple configuration files can be used together: - **Atmos config file** (`--atmos_config_file`): Optional ClimaAtmos-specific configuration - **TOML parameter files** (`--coupler_toml`): One or more TOML files containing model parameters +Sometimes, the coupler config file includes options that are not defined in ClimaCoupler.jl, +but affect only the atmosphere model. For example `output_default_diagnostics` controls +only the atmosphere diagnostics, despite its generic name. The full set of options used by +the atmosphere model can be found in the [ClimaAtmos.jl docs](https://clima.github.io/ClimaAtmos.jl/stable/config/). + When multiple config files are specified, values in the coupler config file will take precedence over those in the atmosphere config file. This is explained in more detail in the [Precendence of Config Files and CLI Arguments](#precendence-of-config-files-and-cli-arguments) @@ -135,6 +140,8 @@ specific timesteps should be specified, rather than only `dt`. | Argument | Type | Default | Valid Options | Description | |----------|------|---------|---------------|-------------| | `--use_coupler_diagnostics` | Bool | `true` | `true`, `false` | Whether to compute and output coupler diagnostics | +| `--coupler_diagnostics_period` | String | `nothing` | `"Nsecs"`, `"Nmins"`, `"Nhours"`, `"Ndays"`, `"Nmonths"` | Time interval between coupler diagnostic outputs. If unset, the period is auto-derived from the simulation duration. | +| `--coupler_diagnostics_reduction` | String | `"average"` | `average`, `instantaneous`, `max`, `min` | Reduction mode for coupler diagnostic outputs | | `--coupler_output_dir` | String | `"output"` | Any valid directory path | Directory to save output files | @@ -164,6 +171,8 @@ specific timesteps should be specified, rather than only `dt`. | `--land_model` | String | `"bucket"` | `bucket`, `integrated` | Land model to use | | `--land_temperature_anomaly` | String | `"aquaplanet"` | `amip`, `aquaplanet`, `nothing` | Type of temperature anomaly for land model | | `--use_land_diagnostics` | Bool | `true` | `true`, `false` | Whether to compute and output land model diagnostics | +| `--land_diagnostics_period` | String | `"1months"` | `"30mins"`, `"1hours"`, `"1days"`, `"10days"`, `"1months"` | Time interval between land diagnostic outputs. ClimaLand's diagnostics API only accepts a fixed set of periods, so the values listed here are the only supported options. | +| `--land_diagnostics_reduction` | String | `"average"` | `average`, `instantaneous`, `max`, `min` | Reduction type for land diagnostic outputs | | `--land_spun_up_ic` | Bool | `false` | `true`, `false` | Whether to use integrated land initial conditions from spun up state | | `--lai_source` | String | `"modis_monthly"` | `modis_monthly`, `modis_monthly_climatology` | Source for leaf area index data. `modis_monthly` uses full MODIS monthly data, `modis_monthly_climatology` uses MODIS monthly climatology with periodic calendar | | `--bucket_albedo_type` | String | `"map_static"` | `map_static`, `function`, `map_temporal`, `era5` | Access bucket surface albedo information from data file. Use `era5` for ERA5-derived processed albedo files (requires `era5_initial_condition_dir`) | diff --git a/ext/ClimaCouplerClimaLandExt/climaland_bucket.jl b/ext/ClimaCouplerClimaLandExt/climaland_bucket.jl index 58547f6a36..5d9afb1204 100644 --- a/ext/ClimaCouplerClimaLandExt/climaland_bucket.jl +++ b/ext/ClimaCouplerClimaLandExt/climaland_bucket.jl @@ -49,6 +49,8 @@ function BucketSimulation( atmos_h, land_temperature_anomaly::String = "amip", use_land_diagnostics::Bool = true, + land_diagnostics_period::Symbol = :monthly, + land_diagnostics_reduction::Symbol = :average, albedo_type::String = "map_static", bucket_initial_condition::String = "", era5_albedo_file_path::Union{Nothing, String} = nothing, @@ -185,8 +187,11 @@ function BucketSimulation( diagnostics = CL.default_diagnostics( model, start_date, + output_dir; output_writer = output_writer, - reduction_period = :monthly, + reduction_period = land_diagnostics_period, + reduction_type = land_diagnostics_reduction, + dt = float(dt), ) else diagnostics = nothing diff --git a/ext/ClimaCouplerClimaLandExt/climaland_integrated.jl b/ext/ClimaCouplerClimaLandExt/climaland_integrated.jl index 5fe4ef100c..bb708ed78c 100644 --- a/ext/ClimaCouplerClimaLandExt/climaland_integrated.jl +++ b/ext/ClimaCouplerClimaLandExt/climaland_integrated.jl @@ -77,6 +77,8 @@ function ClimaLandSimulation( atmos_h, land_temperature_anomaly::String = "amip", use_land_diagnostics::Bool = true, + land_diagnostics_period::Symbol = :monthly, + land_diagnostics_reduction::Symbol = :average, coupled_param_dict = CP.create_toml_dict(FT), land_ic_path::Union{Nothing, String} = nothing, lai_source::String = "modis_monthly", @@ -186,9 +188,12 @@ function ClimaLandSimulation( diagnostics = CL.default_diagnostics( model, start_date, + output_dir; output_writer = output_writer, output_vars = :short, - reduction_period = :monthly, + reduction_period = land_diagnostics_period, + reduction_type = land_diagnostics_reduction, + dt = float(dt), ) else output_writer = nothing diff --git a/src/Input.jl b/src/Input.jl index 8bcf01b937..615a9a0ef8 100644 --- a/src/Input.jl +++ b/src/Input.jl @@ -16,6 +16,7 @@ import ClimaCore as CC import ClimaCoupler import ..Checkpointer import ..Interfacer +import ..TimeManager import ..Utilities export argparse_settings, @@ -167,6 +168,14 @@ function argparse_settings() help = "Boolean flag indicating whether to compute and output coupler diagnostics [`true` (default), `false`]" arg_type = Bool default = true + "--coupler_diagnostics_period" + help = "Time interval between coupler diagnostic outputs. If not set, the period is derived from the simulation duration. [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Nmonths\"]" + arg_type = String + default = nothing + "--coupler_diagnostics_reduction" + help = "Reduction mode for coupler diagnostic outputs. [`average` (default), `instantaneous`, `max`, `min`]" + arg_type = String + default = "average" # Physical simulation information "--evolving_ocean" help = "Boolean flag indicating whether to use a dynamic slab ocean model, as opposed to constant surface temperatures [`true` (default), `false`]" @@ -224,6 +233,14 @@ function argparse_settings() help = "Boolean flag indicating whether to compute and output land model diagnostics [`true` (default), `false`]" arg_type = Bool default = true + "--land_diagnostics_period" + help = "Time interval between land diagnostic outputs. ClimaLand only supports a fixed set of periods: [`1months` (default), `10days`, `1days`, `1hours`, `30mins`]" + arg_type = String + default = "1months" + "--land_diagnostics_reduction" + help = "Reduction type for land diagnostic outputs. [`average` (default), `instantaneous`, `max`, `min`]" + arg_type = String + default = "average" "--land_spun_up_ic" help = "Boolean flag to indicate whether to use integrated land initial conditions from spun up state [`true` (default), `false`]" arg_type = Bool @@ -501,8 +518,14 @@ function get_coupler_args(config_dict::Dict) # Diagnostics information use_coupler_diagnostics = config_dict["use_coupler_diagnostics"] - use_land_diagnostics = config_dict["use_land_diagnostics"] - (_, diagnostics_dt) = get_diag_period(t_start, t_end) + coupler_diagnostics_reduction = Symbol(config_dict["coupler_diagnostics_reduction"]) + # If no coupler diagnostics period is specified, auto-derive from the simulation duration + coupler_diagnostics_period = config_dict["coupler_diagnostics_period"] + if isnothing(coupler_diagnostics_period) + (_, coupler_diagnostics_period) = get_diag_period(t_start, t_end) + else + coupler_diagnostics_period = TimeManager.time_to_period(coupler_diagnostics_period) + end # Physical simulation information evolving_ocean = config_dict["evolving_ocean"] @@ -523,6 +546,9 @@ function get_coupler_args(config_dict::Dict) lai_source = config_dict["lai_source"] bucket_albedo_type = config_dict["bucket_albedo_type"] bucket_initial_condition = config_dict["bucket_initial_condition"] + land_diagnostics_period = + land_diagnostics_period_to_symbol(config_dict["land_diagnostics_period"]) + land_diagnostics_reduction = Symbol(config_dict["land_diagnostics_reduction"]) # Initial condition setting era5_initial_condition_dir = config_dict["era5_initial_condition_dir"] @@ -605,7 +631,8 @@ function get_coupler_args(config_dict::Dict) restart_cache, save_cache, use_coupler_diagnostics, - diagnostics_dt, + coupler_diagnostics_period, + coupler_diagnostics_reduction, evolving_ocean, energy_check, conservation_softfail, @@ -616,6 +643,8 @@ function get_coupler_args(config_dict::Dict) land_spun_up_ic, lai_source, use_land_diagnostics, + land_diagnostics_period, + land_diagnostics_reduction, bucket_albedo_type, parameter_files, era5_filepaths, @@ -681,6 +710,32 @@ function get_diag_period(t_start, t_end) return (period, diagnostics_dt) end +""" + land_diagnostics_period_to_symbol(period_str) + +Translate the user-facing `land_diagnostics_period` time-string (e.g. `"1hours"`) +to the corresponding ClimaLand `reduction_period` symbol expected by +`ClimaLand.default_diagnostics` (e.g. `:hourly`). + +ClimaLand's diagnostics API only accepts a fixed set of period symbols +(see `ClimaLand.Diagnostics.get_period`), but the rest of the coupler uses +human-readable time strings, so this helper bridges the two. +""" +const _LAND_DIAGNOSTICS_PERIOD_SYMBOLS = Dict( + "30mins" => :halfhourly, + "1hours" => :hourly, + "1days" => :daily, + "10days" => :tendaily, + "1months" => :monthly, +) +function land_diagnostics_period_to_symbol(period_str::AbstractString) + haskey(_LAND_DIAGNOSTICS_PERIOD_SYMBOLS, period_str) || error( + "Unsupported land_diagnostics_period: \"$period_str\". " * + "ClimaLand only supports: $(join(sort(collect(keys(_LAND_DIAGNOSTICS_PERIOD_SYMBOLS))), ", ")).", + ) + return _LAND_DIAGNOSTICS_PERIOD_SYMBOLS[period_str] +end + """ parse_component_dts!(config_dict) diff --git a/src/SimCoordinator.jl b/src/SimCoordinator.jl index f26a4c3482..b97e84e48a 100644 --- a/src/SimCoordinator.jl +++ b/src/SimCoordinator.jl @@ -225,7 +225,8 @@ function Interfacer.CoupledSimulation(config_dict::AbstractDict) restart_cache, save_cache, use_land_diagnostics, - diagnostics_dt, + land_diagnostics_period, + land_diagnostics_reduction, evolving_ocean, land_model, land_temperature_anomaly, @@ -234,6 +235,8 @@ function Interfacer.CoupledSimulation(config_dict::AbstractDict) bucket_albedo_type, energy_check, use_coupler_diagnostics, + coupler_diagnostics_period, + coupler_diagnostics_reduction, output_dir_root, parameter_files, era5_filepaths, @@ -342,6 +345,8 @@ function Interfacer.CoupledSimulation(config_dict::AbstractDict) atmos_h, land_temperature_anomaly, use_land_diagnostics, + land_diagnostics_period, + land_diagnostics_reduction, coupled_param_dict, albedo_type = bucket_albedo_type, bucket_initial_condition, @@ -460,8 +465,9 @@ function Interfacer.CoupledSimulation(config_dict::AbstractDict) dir_paths.coupler_output_dir, start_date, tspan[1], - diagnostics_dt, - Δt_cpl, + coupler_diagnostics_period, + Δt_cpl; + reduction = coupler_diagnostics_reduction, ) else diags_handler = nothing diff --git a/src/SimOutput/diagnostics.jl b/src/SimOutput/diagnostics.jl index 34e3655b46..d5caa8bdb9 100644 --- a/src/SimOutput/diagnostics.jl +++ b/src/SimOutput/diagnostics.jl @@ -2,7 +2,25 @@ import ClimaDiagnostics as CD import ClimaCoupler: Interfacer import Dates -export diagnostics_setup +export diagnostics_setup, get_reduction + +""" + get_reduction(::Val{:instantaneous}) = nothing + get_reduction(::Val{:average}) = (+) + get_reduction(::Val{:max}) = max + get_reduction(::Val{:min}) = min + +Helper that maps a reduction-type symbol to the `reduction_time_func` expected by +`ClimaDiagnostics.ScheduledDiagnostic`. +""" +get_reduction(::Val{:instantaneous}) = nothing +get_reduction(::Val{:average}) = (+) +get_reduction(::Val{:max}) = max +get_reduction(::Val{:min}) = min +get_reduction(val) = error( + "Diagnostic reduction $val not supported. " * + "Supported reductions are: `:instantaneous`, `:average`, `:max`, `:min`.", +) #### Custom Schedule for diagnostics that only get output once at the beginning of the simulation @@ -35,13 +53,23 @@ function CD.orchestrate_diagnostics(cs::Interfacer.CoupledSimulation) end """ - diagnostics_setup(fields, output_dir, start_date, t_start, diagnostics_dt) + diagnostics_setup(fields, output_dir, start_date, t_start, + coupler_diagnostics_period, coupled_dt; + reduction = :average) Set up the default diagnostics for an AMIP simulation, using ClimaDiagnostics. The diagnostics are saved to NetCDF files. Currently, this just includes a diagnostic for turbulent energy fluxes and diagnostics for each of the land, ocean, and sea-ice area fractions. +`coupler_diagnostics_period` is a `Dates.Period` controlling the output cadence +of the turbulent energy flux and ocean/ice fraction diagnostics. + +`reduction` controls the temporal reduction applied to the turbulent energy +flux diagnostic. Supported values are `:average` (default), `:instantaneous`, +`:max`, and `:min` (see `get_reduction`). Area fraction diagnostics are always +output instantaneously. + Return a DiagnosticsHandler object to coordinate the diagnostics. """ function diagnostics_setup( @@ -49,8 +77,9 @@ function diagnostics_setup( output_dir, start_date, t_start, - diagnostics_dt, - coupled_dt, + coupler_diagnostics_period, + coupled_dt; + reduction::Symbol = :average, ) # Create a list to hold the scheduled diagnostics scheduled_diags = [] @@ -78,16 +107,21 @@ function diagnostics_setup( end, ) - # Schedule the turbulent energy fluxes to save at every step and output at the frequency calculated above + # Schedule the turbulent energy fluxes to save at every step and output at the configured period compute_sched = CD.Schedules.EveryStepSchedule() # Note that these are stateful, so we create new ones for each diagnostic - output_sched = CD.Schedules.EveryCalendarDtSchedule(diagnostics_dt; start_date) + output_sched = + CD.Schedules.EveryCalendarDtSchedule(coupler_diagnostics_period; start_date) + reduction_time_func = get_reduction(Val(reduction)) + # `pre_output_hook!` is only needed to finalize the running mean for `:average`; + # all other reductions don't require post-processing. + pre_output_hook! = reduction == :average ? CD.average_pre_output_hook! : nothing F_turb_energy_diag_sched = CD.ScheduledDiagnostic( variable = F_turb_energy_diag, output_writer = netcdf_writer, - reduction_time_func = (+), + reduction_time_func = reduction_time_func, compute_schedule_func = compute_sched, output_schedule_func = output_sched, - pre_output_hook! = CD.average_pre_output_hook!, + pre_output_hook! = pre_output_hook!, ) push!(scheduled_diags, F_turb_energy_diag_sched) @@ -140,10 +174,12 @@ function diagnostics_setup( end, ) - # Schedule the ocean fraction to save and output at diagnostic frequency + # Schedule the ocean fraction to save and output at diagnostic frequency # since it can change in time with evolving sea ice - compute_sched = CD.Schedules.EveryCalendarDtSchedule(diagnostics_dt; start_date) - output_sched = CD.Schedules.EveryCalendarDtSchedule(diagnostics_dt; start_date) + compute_sched = + CD.Schedules.EveryCalendarDtSchedule(coupler_diagnostics_period; start_date) + output_sched = + CD.Schedules.EveryCalendarDtSchedule(coupler_diagnostics_period; start_date) ocean_fraction_diag_sched = CD.ScheduledDiagnostic( variable = ocean_fraction_diag, output_writer = netcdf_writer, @@ -171,9 +207,11 @@ function diagnostics_setup( end, ) - # Schedule the ice fraction to save and output at diagnostic frequency - compute_sched = CD.Schedules.EveryCalendarDtSchedule(diagnostics_dt; start_date) - output_sched = CD.Schedules.EveryCalendarDtSchedule(diagnostics_dt; start_date) + # Schedule the ice fraction to save and output at diagnostic frequency + compute_sched = + CD.Schedules.EveryCalendarDtSchedule(coupler_diagnostics_period; start_date) + output_sched = + CD.Schedules.EveryCalendarDtSchedule(coupler_diagnostics_period; start_date) ice_fraction_diag_sched = CD.ScheduledDiagnostic( variable = ice_fraction_diag, output_writer = netcdf_writer, diff --git a/test/input_tests.jl b/test/input_tests.jl index 0fa2282c76..fb6921ef76 100644 --- a/test/input_tests.jl +++ b/test/input_tests.jl @@ -79,7 +79,11 @@ end "restart_cache" => true, "save_cache" => true, "use_coupler_diagnostics" => true, + "coupler_diagnostics_period" => nothing, + "coupler_diagnostics_reduction" => "average", "use_land_diagnostics" => true, + "land_diagnostics_period" => "1months", + "land_diagnostics_reduction" => "average", "evolving_ocean" => true, "energy_check" => false, "conservation_softfail" => false, @@ -190,6 +194,16 @@ end @test period == "1days" end +@testset "land_diagnostics_period_to_symbol" begin + @test Input.land_diagnostics_period_to_symbol("30mins") == :halfhourly + @test Input.land_diagnostics_period_to_symbol("1hours") == :hourly + @test Input.land_diagnostics_period_to_symbol("1days") == :daily + @test Input.land_diagnostics_period_to_symbol("10days") == :tendaily + @test Input.land_diagnostics_period_to_symbol("1months") == :monthly + @test_throws ErrorException Input.land_diagnostics_period_to_symbol("2hours") + @test_throws ErrorException Input.land_diagnostics_period_to_symbol("hourly") +end + @testset "parse_component_dts!" begin # Test case 1: All component dt's are specified config_dict = Dict{String, Any}( diff --git a/test/sim_output_tests.jl b/test/sim_output_tests.jl index 85da76a9a0..f37d53cac3 100644 --- a/test/sim_output_tests.jl +++ b/test/sim_output_tests.jl @@ -2,6 +2,14 @@ using Test import ClimaCoupler: SimOutput import ArgParse +@testset "get_reduction" begin + @test isnothing(SimOutput.get_reduction(Val(:instantaneous))) + @test SimOutput.get_reduction(Val(:average)) == (+) + @test SimOutput.get_reduction(Val(:max)) == max + @test SimOutput.get_reduction(Val(:min)) == min + @test_throws ErrorException SimOutput.get_reduction(Val(:not_a_reduction)) +end + @testset "get_benchmark_args" begin # Test with empty ARGS (should use defaults) # We need to temporarily set ARGS