Skip to content

Commit c67e0bb

Browse files
Merge pull request #1187 from CliMA/ne/calibration
Add AMIP calibration pipeline
2 parents 55a6946 + 0136119 commit c67e0bb

File tree

11 files changed

+279
-11
lines changed

11 files changed

+279
-11
lines changed

.buildkite/longruns/pipeline.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ steps:
4444
- "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.precompile()'"
4545
- "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.status()'"
4646

47+
- echo "--- Instantiate calibration env"
48+
- "julia --project=experiments/calibration/ -e 'using Pkg; Pkg.instantiate(;verbose=true); Pkg.develop(;path=\".\")'"
4749
agents:
4850
queue: clima
4951
modules: climacommon/2024_12_16
@@ -315,6 +317,23 @@ steps:
315317
modules: climacommon/2024_12_16
316318
soft_fail: true
317319

320+
- label: "Perfect model calibration test"
321+
key: "amip_pm_calibration"
322+
command:
323+
- "julia --color=yes --project=experiments/calibration/ experiments/calibration/run_calibration.jl"
324+
artifact_paths: "experiments/calibration/output/*"
325+
env:
326+
CLIMACOMMS_DEVICE: "CUDA"
327+
CLIMACOMMS_CONTEXT: "SINGLETON"
328+
agents:
329+
queue: clima
330+
slurm_mem: 96GB
331+
slurm_ntasks: 5
332+
slurm_gpus_per_task: 1
333+
slurm_cpus_per_task: 4
334+
slurm_time: 05:00:00
335+
modules: climacommon/2024_12_16
336+
318337
- wait
319338

320339
- group: "Job analysis and reporting"

experiments/ClimaEarth/components/atmosphere/climaatmos.jl

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ include("climaatmos_extra_diags.jl")
1717
###
1818
### Functions required by ClimaCoupler.jl for an AtmosModelSimulation
1919
###
20-
struct ClimaAtmosSimulation{P, D, I} <: Interfacer.AtmosModelSimulation
20+
struct ClimaAtmosSimulation{P, D, I, OW} <: Interfacer.AtmosModelSimulation
2121
params::P
2222
domain::D
2323
integrator::I
24+
output_writers::OW
2425
end
2526
Interfacer.name(::ClimaAtmosSimulation) = "ClimaAtmosSimulation"
2627

@@ -36,7 +37,7 @@ function ClimaAtmosSimulation(atmos_config)
3637
# By passing `parsed_args` to `AtmosConfig`, `parsed_args` overwrites the default atmos config
3738
FT = atmos_config.parsed_args["FLOAT_TYPE"] == "Float64" ? Float64 : Float32
3839
simulation = CA.get_simulation(atmos_config)
39-
(; integrator) = simulation
40+
(; integrator, output_writers) = simulation
4041
Y = integrator.u
4142
center_space = axes(Y.c.ρe_tot)
4243
face_space = axes(Y.f.u₃)
@@ -69,7 +70,7 @@ function ClimaAtmosSimulation(atmos_config)
6970
@. ᶠradiation_flux = CC.Geometry.WVector(FT(0))
7071
end
7172

72-
sim = ClimaAtmosSimulation(integrator.p.params, spaces, integrator)
73+
sim = ClimaAtmosSimulation(integrator.p.params, spaces, integrator, output_writers)
7374

7475
# DSS state to ensure we have continuous fields
7576
dss_state!(sim)
@@ -351,9 +352,11 @@ function get_atmos_config_dict(coupler_dict::Dict, job_id::String, atmos_output_
351352
atmos_toml = joinpath.(pkgdir(CA), atmos_config["toml"])
352353
coupler_toml = joinpath.(pkgdir(ClimaCoupler), coupler_dict["coupler_toml"])
353354
toml = isempty(coupler_toml) ? atmos_toml : coupler_toml
354-
355+
if haskey(atmos_config, "calibration_toml")
356+
push!(toml, atmos_config["calibration_toml"])
357+
end
355358
if !isempty(toml)
356-
@info "Overwriting Atmos parameters from input TOML file(s)"
359+
@info "Overwriting Atmos parameters from input TOML file(s): $toml"
357360
atmos_config = merge(atmos_config, Dict("toml" => toml))
358361
end
359362

experiments/ClimaEarth/components/land/climaland_bucket.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import SciMLBase
33
import Statistics
44
import ClimaCore as CC
55
import ClimaTimeSteppers as CTS
6-
import ClimaParams
76
import Thermodynamics as TD
87
import ClimaLand as CL
98
import ClimaLand.Parameters as LP

experiments/ClimaEarth/setup_run.jl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,16 +152,18 @@ function solve_coupler!(cs)
152152
return nothing
153153
end
154154

155+
function setup_and_run(config_file = joinpath(pkgdir(ClimaCoupler), "config/ci_configs/amip_default.yml"))
156+
config_dict = get_coupler_config_dict(config_file)
157+
return setup_and_run(config_dict)
158+
end
155159
"""
156160
setup_and_run(config_file = joinpath(pkgdir(ClimaCoupler), "config/ci_configs/amip_default.yml"))
157161
158162
This function sets up and runs the coupled model simulation specified by the
159163
input config file. It initializes the component models, all coupler objects,
160164
diagnostics, and conservation checks, and then runs the simulation.
161165
"""
162-
function setup_and_run(config_file = joinpath(pkgdir(ClimaCoupler), "config/ci_configs/amip_default.yml"))
163-
# Parse the configuration file
164-
config_dict = get_coupler_config_dict(config_file)
166+
function setup_and_run(config_dict::AbstractDict)
165167
# Initialize communication context (do this first so all printing is only on root)
166168
comms_ctx = Utilities.get_comms_context(config_dict)
167169
# Select the correct timestep for each component model based on which are available
@@ -734,5 +736,6 @@ function setup_and_run(config_file = joinpath(pkgdir(ClimaCoupler), "config/ci_c
734736
end
735737

736738
# Close all diagnostics file writers
737-
!isnothing(cs.diags_handler) && map(diag -> close(diag.output_writer), cs.diags_handler.scheduled_diagnostics)
739+
isnothing(cs.diags_handler) || foreach(diag -> close(diag.output_writer), cs.diags_handler.scheduled_diagnostics)
740+
isnothing(atmos_sim.output_writers) || foreach(close, atmos_sim.output_writers)
738741
end

experiments/ClimaEarth/test/component_model_tests/climaatmos_tests.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ for FT in (Float32, Float64)
1616
u = (; state_field1 = CC.Fields.ones(boundary_space), state_field2 = CC.Fields.zeros(boundary_space)),
1717
p = (; cache_field = CC.Fields.zeros(boundary_space)),
1818
)
19-
sim = ClimaAtmosSimulation(nothing, nothing, integrator)
19+
sim = ClimaAtmosSimulation(nothing, nothing, integrator, nothing)
2020

2121
# make field non-constant to check the impact of the dss step
2222
coords_lat = CC.Fields.coordinate_field(sim.integrator.u.state_field2).lat
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[deps]
2+
ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63"
3+
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
4+
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
5+
ClimaAnalysis = "29b5916a-a76c-4e73-9657-3c8fd22e65e6"
6+
ClimaAtmos = "b2c96348-7fb7-4fe0-8da9-78d88439e717"
7+
ClimaCalibrate = "4347a170-ebd6-470c-89d3-5c705c0cacc2"
8+
ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d"
9+
ClimaCore = "d414da3d-4745-48bb-8d80-42e94e092884"
10+
ClimaCoreMakie = "908f55d8-4145-4867-9c14-5dad1a479e4d"
11+
ClimaCoupler = "4ade58fe-a8da-486c-bd89-46df092ec0c7"
12+
ClimaDiagnostics = "1ecacbb8-0713-4841-9a07-eb5aa8a2d53f"
13+
ClimaLand = "08f4d4ce-cf43-44bb-ad95-9d2d5f413532"
14+
ClimaTimeSteppers = "595c0a79-7f3d-439a-bc5a-b232dc3bde79"
15+
ClimaUtilities = "b3f4f4ca-9299-4f7f-bd9b-81e1242a7513"
16+
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
17+
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
18+
EnsembleKalmanProcesses = "aa8a2aa5-91d8-4396-bcef-d4f2ec43552d"
19+
GeoMakie = "db073c08-6b98-4ee5-b6a4-5efafb3259c6"
20+
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
21+
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
22+
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
23+
NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab"
24+
Poppler_jll = "9c32591e-4766-534b-9725-b71a8799265b"
25+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
26+
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
27+
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
28+
SurfaceFluxes = "49b00bb7-8bd4-4f2b-b78c-51cd0450215f"
29+
Thermodynamics = "b60c26fb-14c3-4610-9d3e-2d17fe7ff00c"
30+
YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6"
31+
32+
[compat]
33+
ClimaCalibrate = "0.0.10"
34+
ClimaTimeSteppers = "=0.8.1"

experiments/calibration/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# ClimaCoupler Calibration Experiments
2+
3+
This folder contains a trivial perfect-model calibration of the atmosphere coupled with the bucket model.
4+
The calibration uses 30-day and lat/lon averages of top-of-atmosphere shortwave
5+
radiation to calibrate the `total_solar_irradiance` parameter in a perfect model setting.
6+
The current run script uses the `ClimaCalibrate.SlurmManager` to add Slurm workers
7+
which run each ensemble member in parallel.
8+
9+
To run this calibration on a Slurm cluster, ensure that `run_calibration.sh` is
10+
configured for your cluster and run `sbatch run_calibration.sh`. The output will
11+
be generated in `experiments/calibration/output`.
12+
13+
Components:
14+
- run_calibration.sh: SBATCH script used to instantiate the project and run the calibration on a Slurm cluster.
15+
- run_calibration.jl: Julia script for the overall calibration and postprocessing. Contains the expriment configuration, such as ensemble size and number of iterations.
16+
- model_interface.jl: Contains `forward_model`, the function that gets run during calibration. This basically just uses the `setup_run` function.
17+
- model_config.yml: Contains the configuration for the coupler
18+
-
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
FLOAT_TYPE: "Float32"
2+
albedo_model: "CouplerAlbedo"
3+
atmos_config_file: "config/longrun_configs/amip_target_diagedmf.yml"
4+
checkpoint_dt: "720hours"
5+
coupler_toml: ["toml/amip.toml"]
6+
deep_atmosphere: false
7+
dt: "240secs"
8+
dt_cpl: "240secs"
9+
dz_bottom: 100.0
10+
energy_check: false
11+
h_elem: 8
12+
land_albedo_type: "map_temporal"
13+
mode_name: "amip"
14+
mono_surface: false
15+
netcdf_interpolation_num_points: [90, 45, 31]
16+
netcdf_output_at_levels: true
17+
output_default_diagnostics: false
18+
use_coupler_diagnostics: false
19+
override_precip_timescale: false
20+
rayleigh_sponge: true
21+
start_date: "20100101"
22+
surface_setup: "PrescribedSurface"
23+
coupler_output_dir: "experiments/calibration/output"
24+
t_end: "30days"
25+
topo_smoothing: true
26+
topography: "Earth"
27+
turb_flux_partition: "CombinedStateFluxesMOST"
28+
viscous_sponge: true
29+
z_elem: 39
30+
z_max: 60000.0
31+
insolation: timevarying
32+
dt_rad: 6hours
33+
rad: clearsky
34+
extra_atmos_diagnostics:
35+
- reduction_time: average
36+
short_name: rsut
37+
period: 30days
38+
writer: nc
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import ClimaCoupler
2+
import ClimaCalibrate
3+
include(joinpath(pkgdir(ClimaCoupler), "experiments", "ClimaEarth", "setup_run.jl"))
4+
5+
function ClimaCalibrate.forward_model(iter, member)
6+
config_file = joinpath(ClimaCalibrate.project_dir(), "model_config.yml")
7+
config_dict = get_coupler_config_dict(config_file)
8+
9+
output_dir_root = config_dict["coupler_output_dir"]
10+
# Set member parameter file
11+
sampled_parameter_file = ClimaCalibrate.parameter_path(output_dir_root, iter, member)
12+
config_dict["calibration_toml"] = sampled_parameter_file
13+
# Set member output directory
14+
member_output_dir = ClimaCalibrate.path_to_ensemble_member(output_dir_root, iter, member)
15+
config_dict["coupler_output_dir"] = member_output_dir
16+
return setup_and_run(config_dict)
17+
end
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using Distributed
2+
import ClimaCalibrate as CAL
3+
using ClimaCalibrate
4+
using ClimaAnalysis
5+
import ClimaAnalysis: SimDir, get, slice, average_xy
6+
import ClimaComms
7+
import EnsembleKalmanProcesses: I, ParameterDistributions.constrained_gaussian
8+
9+
# Ensure ClimaComms doesn't use MPI
10+
ENV["CLIMACOMMS_CONTEXT"] = "SINGLETON"
11+
ClimaComms.@import_required_backends
12+
13+
single_member_dims = (1,)
14+
function CAL.observation_map(iteration)
15+
G_ensemble = Array{Float64}(undef, single_member_dims..., ensemble_size)
16+
17+
for m in 1:ensemble_size
18+
member_path = CAL.path_to_ensemble_member(output_dir, iteration, m)
19+
simdir_path = joinpath(member_path, "model_config/output_active/clima_atmos")
20+
if isdir(simdir_path)
21+
simdir = SimDir(simdir_path)
22+
G_ensemble[:, m] .= process_member_data(simdir)
23+
else
24+
@info "No data found for member $m."
25+
G_ensemble[:, m] .= NaN
26+
end
27+
end
28+
return G_ensemble
29+
end
30+
31+
function process_member_data(simdir::SimDir)
32+
output = zeros(single_member_dims...)
33+
days = 86_400
34+
isempty(simdir) && return NaN
35+
36+
rsut = get(simdir; short_name = "rsut", reduction = "average", period = "30d")
37+
rsut_slice = slice(average_lon(average_lat(rsut)); time = 30days).data
38+
return rsut_slice
39+
end
40+
41+
addprocs(CAL.SlurmManager())
42+
# Make variables and the forward model available on the worker sessions
43+
@everywhere begin
44+
import ClimaComms, CUDA
45+
ENV["CLIMACOMMS_DEVICE"] = "CUDA"
46+
ENV["CLIMACOMMS_CONTEXT"] = "SINGLETON"
47+
import ClimaCalibrate as CAL
48+
import JLD2
49+
50+
experiment_dir = CAL.project_dir()
51+
include(joinpath(experiment_dir, "model_interface.jl"))
52+
output_dir = joinpath(experiment_dir, "output")
53+
obs_path = joinpath(experiment_dir, "observations.jld2")
54+
end
55+
56+
# Experiment Configuration
57+
ensemble_size = 10
58+
n_iterations = 5
59+
noise = 0.1 * I
60+
prior = constrained_gaussian("total_solar_irradiance", 1000, 500, 250, 2000)
61+
62+
# Generate observations if needed
63+
if !isfile(obs_path)
64+
import JLD2
65+
@info "Generating observations"
66+
obs_output_dir = CAL.path_to_ensemble_member(output_dir, 0, 0)
67+
mkpath(obs_output_dir)
68+
touch(joinpath(obs_output_dir, "parameters.toml"))
69+
CAL.forward_model(0, 0)
70+
observations = Vector{Float64}(undef, 1)
71+
observations .= process_member_data(SimDir(joinpath(obs_output_dir, "amip_config/output_active/clima_atmos")))
72+
JLD2.save_object(obs_path, observations)
73+
end
74+
75+
# Initialize experiment data
76+
@everywhere observations = JLD2.load_object(obs_path)
77+
78+
eki = CAL.calibrate(CAL.WorkerBackend, ensemble_size, n_iterations, observations, noise, prior, output_dir)
79+
80+
# Postprocessing
81+
import EnsembleKalmanProcesses as EKP
82+
import Statistics: var, mean
83+
using Test
84+
import CairoMakie
85+
86+
function scatter_plot(eki::EKP.EnsembleKalmanProcess)
87+
f = CairoMakie.Figure(resolution = (800, 600))
88+
ax = CairoMakie.Axis(f[1, 1], ylabel = "Parameter Value", xlabel = "Top of atmosphere radiative SW flux")
89+
90+
g = vec.(EKP.get_g(eki; return_array = true))
91+
params = vec.((EKP.get_ϕ(prior, eki)))
92+
93+
for (gg, uu) in zip(g, params)
94+
CairoMakie.scatter!(ax, gg, uu)
95+
end
96+
97+
CairoMakie.vlines!(ax, observations, linestyle = :dash)
98+
99+
output = joinpath(output_dir, "scatter.png")
100+
CairoMakie.save(output, f)
101+
return output
102+
end
103+
104+
function param_versus_iter_plot(eki::EKP.EnsembleKalmanProcess)
105+
f = CairoMakie.Figure(resolution = (800, 600))
106+
ax = CairoMakie.Axis(f[1, 1], ylabel = "Parameter Value", xlabel = "Iteration")
107+
params = EKP.get_ϕ(prior, eki)
108+
for (i, param) in enumerate(params)
109+
CairoMakie.scatter!(ax, fill(i, length(param)), vec(param))
110+
end
111+
112+
output = joinpath(output_dir, "param_vs_iter.png")
113+
CairoMakie.save(output, f)
114+
return output
115+
end
116+
117+
scatter_plot(eki)
118+
param_versus_iter_plot(eki)
119+
120+
params = EKP.get_ϕ(prior, eki)
121+
spread = map(var, params)
122+
123+
# Spread should be heavily decreased as particles have converged
124+
@test last(spread) / first(spread) < 0.1

0 commit comments

Comments
 (0)