-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathInput.jl
More file actions
877 lines (784 loc) · 34.7 KB
/
Input.jl
File metadata and controls
877 lines (784 loc) · 34.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
"""
Input
Module for parsing command-line arguments and configuration files.
"""
module Input
import ArgParse
import YAML
import Dates
import ClimaAtmos as CA
import ClimaUtilities.TimeManager: ITime
import ClimaUtilities.SpaceVaryingInputs: SpaceVaryingInput
import ClimaUtilities.ClimaArtifacts: @clima_artifact
import ClimaCore as CC
import ClimaCoupler
import ..Checkpointer
import ..Interfacer
import ..Utilities
export argparse_settings,
parse_commandline,
get_coupler_config_dict,
get_coupler_args,
get_land_fraction,
get_era5_filepaths
const MODE_NAME_DICT = Dict(
"amip" => Interfacer.AMIPMode,
"cmip" => Interfacer.CMIPMode,
"slabplanet" => Interfacer.SlabplanetMode,
"slabplanet_aqua" => Interfacer.SlabplanetAquaMode,
"slabplanet_terra" => Interfacer.SlabplanetTerraMode,
"subseasonal" => Interfacer.SubseasonalMode,
)
"""
argparse_settings()
Create and return an `ArgParseSettings` object with all command-line arguments
for ClimaCoupler simulations. Each option should include an argument type,
a default value, and a brief help string including the valid values for this option.
"""
function argparse_settings()
s = ArgParse.ArgParseSettings()
ArgParse.@add_arg_table! s begin
### ClimaCoupler flags
# Simulation-identifying information
"--config_file"
help = "A yaml file used to set the configuration of the coupled model [\"config/ci_configs/amip_default.yml\" (default)]"
arg_type = String
default = joinpath(pkgdir(ClimaCoupler), "config/ci_configs/amip_default.yml")
"--job_id"
help = "A unique identifier for this run, defaults to the config file name"
arg_type = String
default = nothing
"--print_config_dict"
help = "Boolean flag indicating whether to print the final configuration dictionary [`true` (default), `false`]"
arg_type = Bool
default = true
"--mode_name"
help = "Mode of coupled simulation. [`cmip`, `amip` (default), `subseasonal`, `slabplanet`, `slabplanet_aqua`, `slabplanet_terra`]"
arg_type = String
default = "amip"
"--coupler_toml"
help = "An optional list of paths to toml files used to overwrite the default model parameters."
arg_type = Vector{String}
default = []
# Computational simulation setup information
"--unique_seed"
help = "Boolean flag indicating whether to set the random number seed to a unique value [`false` (default), `true`]"
arg_type = Bool
default = false
"--FLOAT_TYPE"
help = "Floating point precision [`Float64` (default), `Float32`]"
arg_type = String
default = "Float64"
"--device"
help = "Device type to use [\"auto\" (default), \"CPUSingleThreaded\", \"CPUMultiThreaded\", \"CUDADevice\"]"
arg_type = String
default = "auto"
# Time information
"--use_itime"
help = "Boolean flag indicating whether to use ITime (integer time) or not (will use Float64) [`true` (default), `false`]"
arg_type = Bool
default = true
"--t_end"
help = "End time of the simulation [\"800secs\"; allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
default = "800secs"
"--t_start"
help = "Start time of the simulation [\"0secs\" (default); allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
default = "0secs"
"--start_date"
help = "Start date of the simulation, in format \"YYYYMMDD\" [\"20100101\" (default)]"
arg_type = String
default = "20000101"
"--dt_cpl"
help = "Coupling time step in seconds [400 (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\"]"
arg_type = String
default = "400secs"
"--dt_atmos"
help = "Atmos simulation time step (alternative to `dt`; no default) [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
"--dt_land"
help = "Land simulation time step (alternative to `dt`; no default) [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
"--dt_ocean"
help = "Ocean simulation time step (alternative to `dt`; no default) [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
"--dt_seaice"
help = "Sea ice simulation time step (alternative to `dt`; no default) [allowed formats: \"Nsecs\", \"Nmins\", \"Nhours\", \"Ndays\", \"Inf\"]"
arg_type = String
"--checkpoint_dt"
help = "Time interval for checkpointing [\"90days\" (default)]"
arg_type = String
default = "90days"
# Space information
"--h_elem"
help = "Number of horizontal elements to use for the boundary space [16 (default)]"
arg_type = Int
default = 16
"--nh_poly"
help = "Polynomial order to use for the boundary space [3 (default)]"
arg_type = Int
default = 3
"--share_surface_space"
help = "Boolean flag indicating whether to share the surface space between the surface models, atmosphere, and boundary [`true` (default), `false`]"
arg_type = Bool
default = true
# Restart information
"--detect_restart_files"
help = "Boolean flag indicating whether to automatically use restart files if available [`false` (default), `true`]"
arg_type = Bool
default = false
"--restart_dir"
help = "Directory containing restart files"
arg_type = String
default = nothing
"--restart_t"
help = "Time in seconds rounded to the nearest index to use at `t_start` for restarted simulation [nothing (default)]"
arg_type = Int
default = nothing
"--restart_cache"
help = "Boolean flag indicating whether to read the cache from the restart file if available [`true` (default), `false`]"
arg_type = Bool
default = true
"--save_cache"
help = "Boolean flag indicating whether to save the state and cache or only the state when checkpointing [`true` (default), `false`]"
arg_type = Bool
default = true
# Diagnostics information
"--use_coupler_diagnostics"
help = "Boolean flag indicating whether to compute and output coupler diagnostics [`true` (default), `false`]"
arg_type = Bool
default = true
# 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`]"
arg_type = Bool
default = true
# Conservation and RMSE check information
"--energy_check"
help = "Boolean flag indicating whether to check energy conservation [`false` (default), `true`]"
arg_type = Bool
default = false
"--conservation_softfail"
help = "Boolean flag indicating whether to soft fail on conservation errors [`false` (default), `true`]"
arg_type = Bool
default = false
"--rmse_check"
help = "Boolean flag indicating whether to check RMSE of some physical fields [`false` (default), `true`]"
arg_type = Bool
default = false
# Output information
"--coupler_output_dir"
help = "Directory to save output files. Note that TempestRemap fails if interactive and paths are too long. [\"output\" (default)]"
arg_type = String
default = "output"
# ClimaAtmos specific
"--surface_setup"
help = "Triggers ClimaAtmos into the coupled mode [`PrescribedSurface` (default), `DefaultMoninObukhov`]" # retained here for standalone Atmos benchmarks
arg_type = String
default = "PrescribedSurface"
"--atmos_config_file"
help = "An optional YAML file used to overwrite the default model parameters."
arg_type = String
default = nothing
"--atmos_log_progress"
help = "Use the ClimaAtmos walltime logging callback instead of the default ClimaCoupler one [`false` (default), `true`]"
arg_type = Bool
default = false
"--albedo_model"
help = "Type of albedo model. [`ConstantAlbedo`, `RegressionFunctionAlbedo`, `CouplerAlbedo` (default)]"
arg_type = String
default = "CouplerAlbedo"
"--extra_atmos_diagnostics"
help = "List of dictionaries containing information about additional atmosphere diagnostics to output [nothing (default)]"
arg_type = Vector{Dict{Any, Any}}
default = []
### ClimaLand specific
"--land_model"
help = "Land model to use. [`bucket` (default), `integrated`, `nothing`]"
arg_type = String
default = "bucket"
"--land_temperature_anomaly"
help = "Type of temperature anomaly for land model. [`amip`, `aquaplanet` (default), `nothing`]"
arg_type = String
default = "aquaplanet"
"--use_land_diagnostics"
help = "Boolean flag indicating whether to compute and output land model diagnostics [`true` (default), `false`]"
arg_type = Bool
default = true
"--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
default = true
"--lai_source"
help = "Source for leaf area index data. [`modis_monthly` (default), `modis_monthly_climatology`]"
arg_type = String
default = "modis_monthly"
# BucketModel specific
"--bucket_albedo_type"
help = "Access bucket surface albedo information from data file. [`map_static` (default), `function`, `map_temporal`, `era5`]"
arg_type = String
default = "map_static" # to be replaced by land config file, when available
"--bucket_initial_condition"
help = "A file path for a NetCDF file (read documentation about requirements)"
arg_type = String
default = ""
"--era5_initial_condition_dir"
help = "Directory containing ERA5 initial condition files (subseasonal mode). Filenames inferred from start_date [none (default)]. Generated with `https://github.com/CliMA/WeatherQuest`"
arg_type = String
default = nothing
# Ocean model specific
"--ocean_model"
help = "Ocean model to use. [`prescribed` (default), `oceananigans`, `slab`, `nothing`]"
arg_type = String
default = "prescribed"
"--simple_ocean"
help = "Boolean flag indicating whether to use a simpler ocean model setup with Oceananigans [`false` (default), `true`]"
arg_type = Bool
default = false
"--sst_adjustment"
help = "Adjustment to add to prescribed SST after conversion to Kelvin (default: 0.0)"
arg_type = Float64
default = 0.0
# Ice model specific
"--ice_model"
help = "Sea ice model to use. [`prescribed` (default), `clima_seaice`, `nothing`]"
arg_type = String
default = "prescribed"
"--land_fraction_source"
help = "Source for land fraction data. [`etopo` (default) uses ETOPO-derived landsea_mask artifact, `era5` uses ERA5 land fraction artifact]"
arg_type = String
default = "etopo"
"--binary_area_fraction"
help = "Boolean flag indicating whether to use binary (thresholded) area fractions for land and ice [`true` (default), `false`]. When true, land fraction > eps becomes 1, and ice fraction > 0.5 becomes 1."
arg_type = Bool
default = true
end
return s
end
"""
parse_commandline(settings)
Parse command-line arguments using the provided `ArgParseSettings` object.
# Arguments
- `settings`: An `ArgParse.ArgParseSettings` object containing the command-line arguments to parse
# Returns
- A dictionary of parsed command-line arguments
"""
parse_commandline(settings) = ArgParse.parse_args(ARGS, settings)
"""
get_coupler_config_dict(config_file)
Read in the configuration file and job ID from the command line.
A dictionary is constructed from the input configuration file and returned.
Since the atmosphere model also uses a configuration file, we read in the atmosphere
configuration file specified in the coupler configuration file (if any), and overwrite
it with the coupler configuration.
The order of priority for overwriting configuration options from lowest to highest is:
1. ClimaAtmos defaults
2. ClimaCoupler defaults (defined in `Input.argparse_settings()`)
3. Command line arguments provided to ClimaCoupler
4. ClimaAtmos configuration file (if specified in coupler config file)
5. ClimaCoupler configuration file
# Returns
- `config_dict`: A dictionary mapping configuration keys to the specified settings
"""
function get_coupler_config_dict(config_file)
# Get the coupler default configuration dictionary, overwritten by any command line arguments
# Typically the command line arguments are only `config_file` and `job_id`
coupler_default_cli = parse_commandline(argparse_settings())
# Extract the job ID from the command line arguments, or from the config file name if not provided
job_id = coupler_default_cli["job_id"]
coupler_default_cli["job_id"] =
isnothing(job_id) ? splitext(basename(config_file))[1] : job_id
# Load the coupler config file into a dictionary
coupler_config_dict = YAML.load_file(config_file)
# Get ClimaAtmos default configuration dictionary
atmos_default = CA.default_config_dict()
atmos_config_file = merge(coupler_default_cli, coupler_config_dict)["atmos_config_file"]
if isnothing(atmos_config_file)
@info "Using Atmos default configuration"
# Merge the atmos default config, coupler default config + command line inputs,
# and user-provided coupler config
config_dict = merge(atmos_default, coupler_default_cli, coupler_config_dict)
else
@info "Using Atmos configuration from ClimaCoupler in $atmos_config_file"
atmos_config_dict =
YAML.load_file(joinpath(pkgdir(ClimaCoupler), atmos_config_file))
# Merge the atmos default config, coupler default config + command line inputs,
# user-provided atmos config, and user-provided coupler config
config_dict = merge(
atmos_default,
coupler_default_cli,
atmos_config_dict,
coupler_config_dict,
)
end
# Select the correct timestep for each component model based on which are available
parse_component_dts!(config_dict)
update_t_start_for_restarts!(config_dict)
return config_dict
end
"""
update_t_start_for_restarts!(config_dict)
Update `t_start` in `config_dict` for restarts.
If the user specifies to restart via `detect_restart_files` but a restart time
`restart_t` isn't specified, the restart time is inferred from the checkpointed
files. Otherwise, the provided `restart_t` is used.
If the simulation is not restarting, the input `config_dict` is unchanged.
"""
function update_t_start_for_restarts!(config_dict)
# Update t_start for restarts
(; detect_restart_files, output_dir_root, restart_dir, restart_t) =
get_coupler_args(config_dict)
# Checkpoint directory is hardcoded and can be wrong if
# Utilities.setup_output_dirs is updated
checkpoints_dir = joinpath(output_dir_root, "checkpoints")
if detect_restart_files
isnothing(restart_t) &&
(restart_t = Checkpointer.t_start_from_checkpoint(checkpoints_dir))
isnothing(restart_dir) && (restart_dir = checkpoints_dir)
end
should_restart = !isnothing(restart_t) && !isnothing(restart_dir)
if should_restart
# We only support a round number of seconds
isinteger(float(restart_t)) ||
error("Cannot restart from a non integer number of seconds")
restart_t_int = Int(float(restart_t))
config_dict["t_start"] = "$(restart_t_int)secs"
end
return nothing
end
"""
get_coupler_args(config_dict)
Extract the necessary arguments from the coupled configuration dictionary.
# Arguments
- `config_dict`: A dictionary mapping configuration keys to the specified settings
# Returns
- A NamedTuple of all arguments needed for the coupled simulation
"""
function get_coupler_args(config_dict::Dict)
# Vector of TOML files containing model parameters
# We need to modify this Dict entry to be consistent with ClimaAtmos TOML files
config_dict["coupler_toml"] = map(config_dict["coupler_toml"]) do file
isfile(file) ? file : joinpath(pkgdir(ClimaCoupler), file)
end
parameter_files = config_dict["coupler_toml"]
# Make a copy so that we don't modify the original input
config_dict = copy(config_dict)
# Simulation-identifying information; Print `config_dict` if requested
config_dict["print_config_dict"] && @info(config_dict)
job_id = config_dict["job_id"]
mode_name = config_dict["mode_name"]
sim_mode = MODE_NAME_DICT[mode_name]
use_itime = config_dict["use_itime"]
# Computational simulation setup information
random_seed = config_dict["unique_seed"] ? time_ns() : 1234
FT = config_dict["FLOAT_TYPE"] == "Float64" ? Float64 : Float32
# Time information
t_end = Float64(Utilities.time_to_seconds(config_dict["t_end"]))
t_start = Float64(Utilities.time_to_seconds(config_dict["t_start"]))
start_date = Dates.DateTime(config_dict["start_date"], Dates.dateformat"yyyymmdd")
Δt_cpl = Float64(Utilities.time_to_seconds(config_dict["dt_cpl"]))
if use_itime
t_end = ITime(t_end, epoch = start_date)
t_start = ITime(t_start, epoch = start_date)
# A period of Dates.Second(1) is passed when initializing Δt_cpl to
# ensure consistent Δt_cpl for when restarting a simulation. ITime
# automatically choose the appropriate period based on time value. For
# example, the result ITime(0) gives a period of 1 second, but
# ITime(120) gives a period of 1 minute.
Δt_cpl = ITime(Int64(Δt_cpl), period = Dates.Second(1), epoch = start_date)
times = promote(
t_end,
t_start,
Δt_cpl,
ITime.(values(config_dict["component_dt_dict"]))...,
)
t_end, t_start, Δt_cpl = (times[1], times[2], times[3])
component_dt_dict = Dict(
component => first(promote(ITime(dt), t_end)) for
(component, dt) in config_dict["component_dt_dict"]
)
else
component_dt_dict = config_dict["component_dt_dict"]
end
# Save solution to integrator.sol at the beginning and end
saveat = [t_start, t_end]
# Space information
share_surface_space = config_dict["share_surface_space"]
nh_poly = config_dict["nh_poly"]
h_elem = config_dict["h_elem"]
# Checkpointing information
checkpoint_dt = config_dict["checkpoint_dt"]
# Restart information
detect_restart_files = config_dict["detect_restart_files"]
restart_dir = config_dict["restart_dir"]
restart_t = config_dict["restart_t"]
restart_cache = config_dict["restart_cache"]
save_cache = config_dict["save_cache"]
# 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)
# Physical simulation information
evolving_ocean = config_dict["evolving_ocean"]
# Conservation information
energy_check = config_dict["energy_check"]
conservation_softfail = config_dict["conservation_softfail"]
rmse_check = config_dict["rmse_check"]
# Output information
output_dir_root = joinpath(config_dict["coupler_output_dir"], job_id)
# ClimaLand-specific information
land_model = Val(Symbol(config_dict["land_model"]))
land_temperature_anomaly = lowercase(config_dict["land_temperature_anomaly"])
use_land_diagnostics = config_dict["use_land_diagnostics"]
land_spun_up_ic = config_dict["land_spun_up_ic"]
lai_source = config_dict["lai_source"]
bucket_albedo_type = config_dict["bucket_albedo_type"]
bucket_initial_condition = config_dict["bucket_initial_condition"]
# Initial condition setting
era5_initial_condition_dir = config_dict["era5_initial_condition_dir"]
# Build ERA5-based file paths (only populated for subseasonal mode)
era5_filepaths = get_era5_filepaths(
sim_mode,
era5_initial_condition_dir,
start_date,
bucket_initial_condition,
)
# Ocean model-specific information
ocean_model = Val(Symbol(config_dict["ocean_model"]))
simple_ocean = config_dict["simple_ocean"]
sst_adjustment = FT(config_dict["sst_adjustment"])
# Ice model-specific information
ice_model = Val(Symbol(config_dict["ice_model"]))
# Validate and correct model types based on simulation mode
ocean_model, ice_model, land_model =
validate_model_types_for_mode(sim_mode, ocean_model, ice_model, land_model)
# Land fraction source
land_fraction_source = config_dict["land_fraction_source"]
# Binary area fraction
binary_area_fraction = config_dict["binary_area_fraction"]
return (;
job_id,
sim_mode,
random_seed,
FT,
t_end,
t_start,
start_date,
Δt_cpl,
component_dt_dict,
share_surface_space,
nh_poly,
h_elem,
saveat,
checkpoint_dt,
detect_restart_files,
restart_dir,
restart_t,
restart_cache,
save_cache,
use_coupler_diagnostics,
diagnostics_dt,
evolving_ocean,
energy_check,
conservation_softfail,
rmse_check,
output_dir_root,
land_model,
land_temperature_anomaly,
land_spun_up_ic,
lai_source,
use_land_diagnostics,
bucket_albedo_type,
parameter_files,
era5_filepaths,
ocean_model,
simple_ocean,
sst_adjustment,
ice_model,
land_fraction_source,
binary_area_fraction,
)
end
### Helper functions used in argument parsing ###
"""
get_diag_period(t_start, t_end)
Determine the frequency at which to average and output diagnostics based on the
simulation start and end times.
The default periods are:
- 1 month for simulations longer than 90 days
- 10 days for simulations longer than 30 days
- 1 day for simulations longer than 1 day
- 1 hour for simulations shorter than 1 day
# Arguments
- `t_start`: The start time of the simulation
- `t_end`: The end time of the simulation
# Returns
- `period`: A String of how often to average and output diagnostics
- `diagnostics_dt`: A DateTime interval representing the period
"""
function get_diag_period(t_start, t_end)
sim_duration = float(t_end - t_start)
secs_per_day = 86400
if sim_duration >= 90 * secs_per_day
# if duration >= 90 days, take monthly means
period = "1months"
diagnostics_dt = Dates.Month(1)
elseif sim_duration >= 30 * secs_per_day
# if duration >= 30 days, take means over 10 days
period = "10days"
diagnostics_dt = Dates.Day(10)
elseif sim_duration >= secs_per_day
# if duration >= 1 day, take daily means
period = "1days"
diagnostics_dt = Dates.Day(1)
else
# if duration < 1 day, take hourly means
period = "1hours"
diagnostics_dt = Dates.Hour(1)
end
return (period, diagnostics_dt)
end
"""
parse_component_dts!(config_dict)
Check which timesteps are specified in the config file, and use them to choose
the correct timestep for each component model.
If all component timesteps `dt_\$component` are specified in the config file, use those
and remove `dt` if it was provided.
Otherwise, use the generic component timestep `dt` specified in the config file.
If some (but not all) component timesteps and the generic timestep `dt` are specified,
use the generic timestep and remove the others from the config dict.
The timestep for each component model is stored in the `component_dt_dict` field of the config dict.
# Arguments
- `config_dict`: A dictionary mapping configuration keys to the specified settings
"""
function parse_component_dts!(config_dict)
# Retrieve coupling timestep
Δt_cpl = Float64(Utilities.time_to_seconds(config_dict["dt_cpl"]))
# 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 -> haskey(config_dict, 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
component_dt = Float64(Utilities.time_to_seconds(config_dict[key]))
if key == "dt_atmos"
# ensure that the coupler dt is an integer multiple of the atmos dt
@assert isapprox(Δt_cpl % component_dt, 0.0) "Coupler time step must be an integer multiple of the atmos dt\n dt_cpl = $Δt_cpl\n $key = $component_dt"
else
# all other (surface) model dts must be divisible by the coupler dt
@assert isapprox(component_dt % Δt_cpl, 0.0) "All surface component dts must be divisible by the coupler dt\n $key = $component_dt\n dt_cpl = $Δt_cpl"
end
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 haskey(config_dict, key) && !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"]))
end
end
config_dict["component_dt_dict"] = component_dt_dict
return nothing
end
"""
get_land_fraction(boundary_space, comms_ctx; land_fraction_source = "etopo", binary_area_fraction = true)
Read and remap the land-sea fraction field onto the coupler boundary grid.
# Arguments
- `boundary_space`: The boundary space onto which to remap the land fraction.
- `comms_ctx`: The communications context.
- `land_fraction_source`: Source of land fraction data. Either "etopo" (default) or "era5".
- `binary_area_fraction`: If true (default), threshold land fraction to binary (0 or 1).
- `mode_name`: The name of the simulation mode.
# Returns
- A field containing land fraction values (0 to 1) on the boundary space.
In the terraplanet mode, the land fraction is 1 over the entire surface.
Note:
Land-sea Fraction
This is a static field that contains the area fraction of land and sea, ranging from 0 to 1.
If applicable, sea ice is included in the sea fraction at this stage.
Note that land-sea area fraction is different to the land-sea mask, which is a binary field
(masks are used internally by the coupler to indicate passive cells that are not populated by a given component model).
Two sources are supported via the `land_fraction_source` config option:
- "etopo": ETOPO-derived binary land-sea mask (landsea_mask_60arcseconds artifact)
- "era5": ERA5 land fraction field (era5_land_fraction artifact)
"""
function get_land_fraction(
boundary_space,
comms_ctx;
land_fraction_source::String = "etopo",
binary_area_fraction::Bool = true,
sim_mode = Interfacer.AMIPMode,
)
sim_mode <: Interfacer.SlabplanetTerraMode && return ones(boundary_space)
FT = CC.Spaces.undertype(boundary_space)
if land_fraction_source == "era5"
land_fraction_data = joinpath(
@clima_artifact("era5_land_fraction", comms_ctx),
"era5_land_fraction.nc",
)
land_fraction = SpaceVaryingInput(land_fraction_data, "lsm", boundary_space)
elseif land_fraction_source == "etopo"
land_fraction_data = joinpath(
@clima_artifact("landsea_mask_60arcseconds", comms_ctx),
"landsea_mask.nc",
)
land_fraction = SpaceVaryingInput(land_fraction_data, "landsea", boundary_space)
else
error(
"Unknown land_fraction_source: $land_fraction_source. Must be \"etopo\" or \"era5\".",
)
end
# Ensure land fraction is finite/not NaN and clamp to [0, 1]
land_fraction = ifelse.(isfinite.(land_fraction), land_fraction, FT(0))
land_fraction = max.(min.(land_fraction, FT(1)), FT(0))
if binary_area_fraction
land_fraction = ifelse.(land_fraction .> eps(FT), FT(1), FT(0))
else
land_fraction = ifelse.(land_fraction .> eps(FT), land_fraction, FT(0))
end
return land_fraction
end
"""
validate_model_types_for_mode(sim_mode, ocean_model, ice_model, land_model)
Validate and correct model types based on simulation mode requirements.
Issues warnings and returns updated model types if they don't match the expected values.
# Returns
- `(ocean_model, ice_model, land_model)`: Tuple of validated model types
"""
function validate_model_types_for_mode(sim_mode, ocean_model, ice_model, land_model)
expected_ocean = ocean_model
expected_ice = ice_model
expected_land = land_model
if sim_mode <: Interfacer.AMIPMode
# AMIP: prescribed ocean, prescribed ice
expected_ocean = Val(:prescribed)
expected_ice = Val(:prescribed)
elseif sim_mode <: Interfacer.CMIPMode
# CMIP: Oceananigans ocean, ClimaSeaIce ice
expected_ocean = Val(:oceananigans)
expected_ice = Val(:clima_seaice)
elseif sim_mode <: Interfacer.SlabplanetMode
# slabplanet: slab ocean, no ice
expected_ocean = Val(:slab)
expected_ice = Val(:nothing)
elseif sim_mode <: Interfacer.SlabplanetAquaMode
# slabplanet_aqua: slab ocean, no ice, no land
expected_ocean = Val(:slab)
expected_ice = Val(:nothing)
expected_land = Val(:nothing)
elseif sim_mode <: Interfacer.SlabplanetTerraMode
# slabplanet_terra: no ocean, no ice
expected_ocean = Val(:nothing)
expected_ice = Val(:nothing)
end
# Check and update ocean model
if ocean_model != expected_ocean
exp_model_name = typeof(expected_ocean).parameters[1]
actual_model_name = typeof(ocean_model).parameters[1]
@warn "Simulation mode $(nameof(sim_mode)) requires ocean_model=$(exp_model_name), but got $(actual_model_name). Updating to required value."
ocean_model = expected_ocean
end
# Check and update ice model
if ice_model != expected_ice
exp_model_name = typeof(expected_ice).parameters[1]
actual_model_name = typeof(ice_model).parameters[1]
@warn "Simulation mode $(nameof(sim_mode)) requires ice_model=$(exp_model_name), but got $(actual_model_name). Updating to required value."
ice_model = expected_ice
end
# Check and update land model (only for slabplanet_aqua)
if land_model != expected_land
exp_model_name = typeof(expected_land).parameters[1]
actual_model_name = typeof(land_model).parameters[1]
@warn "Simulation mode $(nameof(sim_mode)) requires land_model=$(exp_model_name), but got $(actual_model_name). Updating to required value."
land_model = expected_land
end
# Perform some final model consistency checks
ocean_model == Val(:slab) &&
@assert ice_model == Val(:nothing) "Slab ocean model cannot be used with a sea ice model"
ice_model == Val(:clima_seaice) &&
@assert ocean_model == Val(:oceananigans) "ClimaSeaIce sea ice model requires Oceananigans ocean model"
return ocean_model, ice_model, land_model
end
"""
get_era5_filepaths(::Type{<:Interfacer.SubseasonalMode}, era5_initial_condition_dir, start_date, bucket_initial_condition)
Build ERA5-based file paths for subseasonal mode simulations.
Filenames are inferred from the start_date.
# Arguments
- `sim_mode`: The simulation mode type (must be SubseasonalMode)
- `era5_initial_condition_dir`: Directory containing ERA5 initial condition files
- `start_date`: The start date of the simulation (DateTime)
- `bucket_initial_condition`: User-specified bucket IC path (empty string if not specified)
# Returns
A NamedTuple with fields:
- `sst_path`: Path to SST file
- `sic_path`: Path to sea ice concentration file
- `land_ic_path`: Path to land initial condition file
- `albedo_path`: Path to albedo file
- `bucket_initial_condition`: Path to bucket IC (user-specified if provided, otherwise ERA5-derived)
"""
function get_era5_filepaths(
::Type{<:Interfacer.SubseasonalMode},
era5_initial_condition_dir,
start_date,
bucket_initial_condition,
)
isnothing(era5_initial_condition_dir) &&
error("subseasonal mode requires --era5_initial_condition_dir")
datestr = Dates.format(start_date, Dates.dateformat"yyyymmdd")
# Use ERA5-derived bucket IC if user didn't specify one
isempty(bucket_initial_condition) && (
bucket_initial_condition = joinpath(
era5_initial_condition_dir,
"era5_bucket_processed_$(datestr)_0000.nc",
)
)
return (
sst_path = joinpath(era5_initial_condition_dir, "sst_processed_$(datestr)_0000.nc"),
sic_path = joinpath(era5_initial_condition_dir, "sic_processed_$(datestr)_0000.nc"),
land_ic_path = joinpath(
era5_initial_condition_dir,
"era5_land_processed_$(datestr)_0000.nc",
),
albedo_path = joinpath(
era5_initial_condition_dir,
"albedo_processed_$(datestr)_0000.nc",
),
bucket_initial_condition,
)
end
"""
get_era5_filepaths(::Type{<:Interfacer.AbstractSimulationMode}, era5_initial_condition_dir, start_date, bucket_initial_condition)
Fallback for non-subseasonal modes. Returns nothing for file paths, passes through bucket_initial_condition.
"""
function get_era5_filepaths(
::Type{<:Interfacer.AbstractSimulationMode},
era5_initial_condition_dir,
start_date,
bucket_initial_condition,
)
return (
sst_path = nothing,
sic_path = nothing,
land_ic_path = nothing,
albedo_path = nothing,
bucket_initial_condition = bucket_initial_condition,
)
end
end # module Input