From a7dc47c455b942c5eda336d25b47b9d77c6dccc7 Mon Sep 17 00:00:00 2001 From: Julia Sloan Date: Thu, 6 Mar 2025 14:06:12 -0800 Subject: [PATCH 1/3] update ocean, sea ice every 1 day --- NEWS.md | 17 +++++++++++ config/ci_configs/amip_component_dts.yml | 2 +- experiments/ClimaEarth/cli_options.jl | 4 +-- experiments/ClimaEarth/user_io/arg_parsing.jl | 29 +++++++------------ 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index ea5a664e44..a1d6dbe41e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,23 @@ ClimaCoupler.jl Release Notes ### ClimaCoupler features +#### Shared component `dt` can be overwritten for individual components +Previously, we required that the user either specify a shared `dt` to be +used by all component models, or specify values for all component models +(`dt_atmos`, `dt_ocean`, `dt_seaice`, `dt_land`). If fewer than 4 +model-specific timesteps were provided, they would be discarded and +`dt` would be used uniformly instead. After this PR, if a user provides +fewer than 4 model-specific timesteps, they will be used for those models, +and the generic `dt` will be used for any models that don't have a more +specific timestep. +This makes choosing the timesteps simpler and allows us to easily set +specific `dt`s only for the models we're interested in. + +This PR also changes the prescribed ocean and sea ice simulations +to update the stored SST/SIC based on a daily schedule. Now, the +input data will be interpolated from monthly to daily instead of +to every timestep. + #### Add default `get_field` methods for surface models PR[#1210](https://github.com/CliMA/ClimaCoupler.jl/pull/1210) Add default methods for `get_field` methods that are commonly not extended for surface models. These return reasonable default diff --git a/config/ci_configs/amip_component_dts.yml b/config/ci_configs/amip_component_dts.yml index d8770b9812..67c5168319 100644 --- a/config/ci_configs/amip_component_dts.yml +++ b/config/ci_configs/amip_component_dts.yml @@ -3,7 +3,7 @@ co2: "maunaloa" dt_atmos: "150secs" dt_cpl: "150secs" dt_land: "50secs" -dt_ocean: "30secs" +dt_ocean: "300secs" dt_rad: "1hours" dt_save_to_sol: "1days" dt_seaice: "37.5secs" diff --git a/experiments/ClimaEarth/cli_options.jl b/experiments/ClimaEarth/cli_options.jl index 4aebdad2e6..7cb987ebef 100644 --- a/experiments/ClimaEarth/cli_options.jl +++ b/experiments/ClimaEarth/cli_options.jl @@ -51,11 +51,11 @@ function argparse_settings() arg_type = String default = "20000101" "--dt_cpl" - help = "Coupling time step in seconds [400 (default); allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]" + help = "Coupling time step in seconds [400secs (default); allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]" arg_type = String default = "400secs" "--dt" - help = "Component model time step [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]" + help = "Component model time step [400secs (default); allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]" arg_type = String default = "400secs" "--dt_atmos" diff --git a/experiments/ClimaEarth/user_io/arg_parsing.jl b/experiments/ClimaEarth/user_io/arg_parsing.jl index 918b81626a..bbd2d40c33 100644 --- a/experiments/ClimaEarth/user_io/arg_parsing.jl +++ b/experiments/ClimaEarth/user_io/arg_parsing.jl @@ -232,27 +232,20 @@ function parse_component_dts!(config_dict) # Specify component model names component_dt_names = ["dt_atmos", "dt_land", "dt_ocean", "dt_seaice"] component_dt_dict = Dict{String, typeof(Δt_cpl)}() - # check if all component dt's are specified - if all(key -> !isnothing(config_dict[key]), component_dt_names) - # when all component dt's are specified, ignore the dt field - if haskey(config_dict, "dt") - @warn "Removing dt in favor of individual component dt's" - delete!(config_dict, "dt") - end - for key in component_dt_names + + @assert all(key -> !isnothing(config_dict[key]), component_dt_names) || haskey(config_dict, "dt") "all model-specific timesteps (dt_atmos, dt_land, dt_ocean, and dt_seaice) or a generic timestep (dt) must be specified" + + for key in component_dt_names + if haskey(config_dict, key) + # Check if the component timestep is specified component_dt = Float64(Utilities.time_to_seconds(config_dict[key])) @assert isapprox(Δt_cpl % component_dt, 0.0) "Coupler dt must be divisible by all component dt's\n dt_cpl = $Δt_cpl\n $key = $component_dt" component_dt_dict[key] = component_dt - end - else - # when not all component dt's are specified, use the dt field - @assert haskey(config_dict, "dt") "dt or (dt_atmos, dt_land, dt_ocean, and dt_seaice) must be specified" - for key in component_dt_names - if !isnothing(config_dict[key]) - @warn "Removing $key from config in favor of dt because not all component dt's are specified" - end - delete!(config_dict, key) - component_dt_dict[key] = Float64(Utilities.time_to_seconds(config_dict["dt"])) + else + # If the component timestep is not specified, use the generic timestep + dt = Float64(Utilities.time_to_seconds(config_dict["dt"])) + @assert isapprox(Δt_cpl % dt, 0.0) "Coupler dt must be divisible by all component dt's\n dt_cpl = $Δt_cpl\n dt = $dt" + component_dt_dict[key] = dt end end config_dict["component_dt_dict"] = component_dt_dict From 0cf77cc7003d7f1f8def64a8db5ac97600ed8141 Mon Sep 17 00:00:00 2001 From: Julia Sloan Date: Thu, 6 Mar 2025 14:41:34 -0800 Subject: [PATCH 2/3] add callbacks to read in prescribed data daily --- config/ci_configs/amip_component_dts.yml | 2 +- .../components/ocean/prescr_ocean.jl | 9 ++++-- .../components/ocean/prescr_seaice.jl | 28 ++++++++++++++++--- experiments/ClimaEarth/setup_run.jl | 2 +- experiments/ClimaEarth/user_io/arg_parsing.jl | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/config/ci_configs/amip_component_dts.yml b/config/ci_configs/amip_component_dts.yml index 67c5168319..b16c614f12 100644 --- a/config/ci_configs/amip_component_dts.yml +++ b/config/ci_configs/amip_component_dts.yml @@ -3,7 +3,7 @@ co2: "maunaloa" dt_atmos: "150secs" dt_cpl: "150secs" dt_land: "50secs" -dt_ocean: "300secs" +dt_ocean: "75secs" dt_rad: "1hours" dt_save_to_sol: "1days" dt_seaice: "37.5secs" diff --git a/experiments/ClimaEarth/components/ocean/prescr_ocean.jl b/experiments/ClimaEarth/components/ocean/prescr_ocean.jl index 3482471adb..d0ce236384 100644 --- a/experiments/ClimaEarth/components/ocean/prescr_ocean.jl +++ b/experiments/ClimaEarth/components/ocean/prescr_ocean.jl @@ -3,6 +3,7 @@ import ClimaUtilities.ClimaArtifacts: @clima_artifact import Interpolations # triggers InterpolationsExt in ClimaUtilities import Thermodynamics as TD import ClimaCoupler: Checkpointer, FieldExchanger, Interfacer +import ClimaDiagnostics as CD """ PrescribedOceanSimulation{C} @@ -84,6 +85,8 @@ function PrescribedOceanSimulation( SST_init = zeros(space) evaluate!(SST_init, SST_timevaryinginput, t_start) + SST_schedule = CD.Schedules.EveryCalendarDtSchedule(TimeManager.time_to_period("1days"); start_date = date0) + # Create the cache cache = (; T_sfc = SST_init, @@ -97,6 +100,7 @@ function PrescribedOceanSimulation( phase = TD.Liquid(), thermo_params = thermo_params, SST_timevaryinginput = SST_timevaryinginput, + SST_schedule = SST_schedule, ) return PrescribedOceanSimulation(cache) end @@ -107,10 +111,11 @@ end Interfacer.step!(sim::PrescribedOceanSimulation, t) Update the cached surface temperature field using the prescribed data -at each timestep. +at each timestep. This doesn't happen at every timestep, +but only when the SST data is scheduled to be updated. """ function Interfacer.step!(sim::PrescribedOceanSimulation, t) - evaluate!(sim.cache.T_sfc, sim.cache.SST_timevaryinginput, t) + sim.cache.SST_schedule(t) && evaluate!(sim.cache.T_sfc, sim.cache.SST_timevaryinginput, t) end function Checkpointer.get_model_cache(sim::PrescribedOceanSimulation) diff --git a/experiments/ClimaEarth/components/ocean/prescr_seaice.jl b/experiments/ClimaEarth/components/ocean/prescr_seaice.jl index 3fc8390faf..e90c9b4849 100644 --- a/experiments/ClimaEarth/components/ocean/prescr_seaice.jl +++ b/experiments/ClimaEarth/components/ocean/prescr_seaice.jl @@ -1,5 +1,6 @@ import SciMLBase import ClimaCore as CC +import ClimaDiagnostics as CD import ClimaTimeSteppers as CTS import ClimaUtilities.TimeVaryingInputs: TimeVaryingInput, evaluate! import ClimaUtilities.ClimaArtifacts: @clima_artifact @@ -143,8 +144,20 @@ function PrescribedIceSimulation( tspan = Float64.(tspan) saveat = Float64.(saveat) end + + # Set up a callback to read in SST data daily + SIC_schedule = CD.Schedules.EveryCalendarDtSchedule(TimeManager.time_to_period("1days"); start_date = date0) + SIC_update_cb = TimeManager.Callback(SIC_schedule, read_sic_data!) + problem = SciMLBase.ODEProblem(ode_function, Y, tspan, (; cache..., params = params)) - integrator = SciMLBase.init(problem, ode_algo, dt = dt, saveat = saveat, adaptive = false) + integrator = SciMLBase.init( + problem, + ode_algo, + dt = dt, + saveat = saveat, + adaptive = false, + callback = SciMLBase.CallbackSet(SIC_update_cb), + ) sim = PrescribedIceSimulation(params, space, integrator) @@ -153,6 +166,16 @@ function PrescribedIceSimulation( return sim end +""" + read_sic_data!(integrator) + +Read in the sea ice concentration data at the current time step. +This function is intended to be used within a callback +""" +function read_sic_data!(integrator) + evaluate!(integrator.p.area_fraction, integrator.p.SIC_timevaryinginput, integrator.t) +end + # extensions required by Interfacer Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:area_fraction}) = sim.integrator.p.area_fraction Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:roughness_buoyancy}) = sim.integrator.p.params.z0b @@ -237,9 +260,6 @@ function ice_rhs!(dY, Y, p, t) FT = eltype(Y) params = p.params - # Update the cached area fraction with the current SIC - evaluate!(p.area_fraction, p.SIC_timevaryinginput, t) - # Overwrite ice fraction with the static land area fraction anywhere we have nonzero land area # max needed to avoid Float32 errors (see issue #271; Heisenbug on HPC) @. p.area_fraction = max(min(p.area_fraction, FT(1) - p.land_fraction), FT(0)) diff --git a/experiments/ClimaEarth/setup_run.jl b/experiments/ClimaEarth/setup_run.jl index 84a56475af..4ff8ccd5c3 100644 --- a/experiments/ClimaEarth/setup_run.jl +++ b/experiments/ClimaEarth/setup_run.jl @@ -527,7 +527,7 @@ function setup_and_run(config_dict::AbstractDict) NB: Eventually, we will call all of radiation from the coupler, in addition to the albedo calculation. =# schedule_checkpoint = EveryCalendarDtSchedule(TimeManager.time_to_period(checkpoint_dt); start_date = date0) - checkpoint_cb = TimeManager.TimeManager.Callback(schedule_checkpoint, Checkpointer.checkpoint_sims) + checkpoint_cb = TimeManager.Callback(schedule_checkpoint, Checkpointer.checkpoint_sims) if sim_mode <: AMIPMode schedule_albedo = EveryCalendarDtSchedule(TimeManager.time_to_period(dt_rad); start_date = date0) diff --git a/experiments/ClimaEarth/user_io/arg_parsing.jl b/experiments/ClimaEarth/user_io/arg_parsing.jl index bbd2d40c33..95c63c0d7f 100644 --- a/experiments/ClimaEarth/user_io/arg_parsing.jl +++ b/experiments/ClimaEarth/user_io/arg_parsing.jl @@ -236,7 +236,7 @@ function parse_component_dts!(config_dict) @assert all(key -> !isnothing(config_dict[key]), component_dt_names) || haskey(config_dict, "dt") "all model-specific timesteps (dt_atmos, dt_land, dt_ocean, and dt_seaice) or a generic timestep (dt) must be specified" for key in component_dt_names - if haskey(config_dict, key) + if !isnothing(config_dict[key]) # Check if the component timestep is specified component_dt = Float64(Utilities.time_to_seconds(config_dict[key])) @assert isapprox(Δt_cpl % component_dt, 0.0) "Coupler dt must be divisible by all component dt's\n dt_cpl = $Δt_cpl\n $key = $component_dt" From 024463586110ac62f5f2358fb0344ffef7f61fe3 Mon Sep 17 00:00:00 2001 From: Julia Sloan Date: Tue, 11 Mar 2025 15:38:05 -0700 Subject: [PATCH 3/3] fix atmos dt parsing --- experiments/ClimaEarth/components/atmosphere/climaatmos.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/experiments/ClimaEarth/components/atmosphere/climaatmos.jl b/experiments/ClimaEarth/components/atmosphere/climaatmos.jl index 9dee582a19..c69f19e01c 100644 --- a/experiments/ClimaEarth/components/atmosphere/climaatmos.jl +++ b/experiments/ClimaEarth/components/atmosphere/climaatmos.jl @@ -453,8 +453,10 @@ function get_atmos_config_dict(coupler_dict::Dict, job_id::String, atmos_output_ # The Atmos `get_simulation` function expects the atmos config to contains its timestep size # in the `dt` field. If there is a `dt_atmos` field in coupler_dict, we add it to the atmos config as `dt` - dt_atmos = haskey(coupler_dict, "dt_atmos") ? coupler_dict["dt_atmos"] : coupler_dict["dt"] - atmos_config["dt"] = dt_atmos + component_dt_dict = coupler_dict["component_dt_dict"] + dt_atmos = haskey(component_dt_dict, "dt_atmos") ? component_dt_dict["dt_atmos"] : component_dt_dict["dt"] + # convert from number of seconds to a string that atmos can parse + atmos_config["dt"] = string(dt_atmos) * "secs" # set restart file to the initial file saved in this location if it is not nothing # TODO this is hardcoded and should be fixed once we have a better restart system