Skip to content

Commit 2f7aac6

Browse files
authored
Merge pull request #3709 from slevis-lmwg/merge-b4bdev-20250122
Merge b4bdev 20260122 (Note: I mislabeled the branch with the wrong year and decided to leave it.)
2 parents 2aeba73 + 0cf11e0 commit 2f7aac6

20 files changed

+389
-55
lines changed

doc/ChangeLog

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,67 @@
11
===============================================================
2+
Tag name: ctsm5.4.011
3+
Originator(s): slevis (Samuel Levis,UCAR/TSS,303-665-1310)
4+
Date: Thu Jan 22 04:01:52 PM MST 2026
5+
One-line Summary: Merge b4b-dev to master
6+
7+
Purpose and description of changes
8+
----------------------------------
9+
Change some _U and _R history fields to be on by default PR #3667 by Keith Oleson
10+
Update to cime version that fixes the check_input_data --download issue PR #3647 by Erik Kluzek; #3647 updated to cime6.1.145, while updating b4b-dev to master in this PR gets us to cime6.1.146.
11+
Initial fixes to generate_gdd20_baseline PR #3543
12+
Decomp mod unittest PR #3699
13+
14+
Significant changes to scientifically-supported configurations
15+
--------------------------------------------------------------
16+
17+
Does this tag change answers significantly for any of the following physics configurations?
18+
(Details of any changes will be given in the "Answer changes" section below.)
19+
20+
[Put an [X] in the box for any configuration with significant answer changes.]
21+
22+
[ ] clm6_0
23+
24+
[ ] clm5_0
25+
26+
[ ] ctsm5_0-nwp
27+
28+
[ ] clm4_5
29+
30+
31+
Bugs fixed
32+
----------
33+
List of CTSM issues fixed (include CTSM Issue # and description) [one per line]:
34+
No issues were opened for the PRs listed above.
35+
36+
Testing summary:
37+
----------------
38+
39+
[PASS means all tests PASS; OK means tests PASS other than expected fails.]
40+
41+
build-namelist tests (if CLMBuildNamelist.pm has changed):
42+
43+
derecho - OK
44+
45+
python testing (if python code has changed; see instructions in python/README.md; document testing done):
46+
47+
derecho - OK
48+
49+
regular tests (aux_clm: https://github.com/ESCOMP/CTSM/wiki/System-Testing-Guide#pre-merge-system-testing):
50+
51+
derecho ----- OK
52+
izumi ------- OK
53+
54+
Answer changes
55+
--------------
56+
Changes answers relative to baseline: No
57+
58+
Other details
59+
-------------
60+
Pull Requests that document the changes (include PR ids):
61+
https://github.com/ESCOMP/ctsm/pull/3709
62+
63+
===============================================================
64+
===============================================================
265
Tag name: ctsm5.4.010
366
Originator(s): erik (Erik Kluzek,UCAR/TSS,303-497-1326)
467
Date: Wed Jan 21 11:49:59 AM MST 2026

doc/ChangeSum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Tag Who Date Summary
22
============================================================================================================================
3+
ctsm5.4.011 slevis 01/22/2026 Merge b4b-dev to master
34
ctsm5.4.010 erik 01/21/2026 Update cime to version that changes answers for ERI tests
45
ctsm5.4.009 olyson 01/19/2026 Dewpoint Temperature check for bare ground
56
ctsm5.4.008 swensosc 01/12/2026 Add a correction for oversaturated soil layers in SoilWaterMovement (erik)

python/ctsm/crop_calendars/check_rx_obeyed.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def check_rx_obeyed(
144144
continue
145145
ds_thisveg = dates_ds.isel(patch=thisveg_patches)
146146

147-
vegtype_int = utils.vegtype_str2int(vegtype_str)[0]
147+
vegtype_int = utils.vegtype_str2int(vegtype_str)
148148
rx_da = rx_ds[f"gs1_{vegtype_int}"]
149149
rx_array = rx_da.values[
150150
ds_thisveg.patches1d_jxy.values.astype(int) - 1,

python/ctsm/crop_calendars/cropcal_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ def vegtype_str2int(vegtype_str, vegtype_mainlist=None):
265265
convert_to_ndarray = not isinstance(vegtype_str, np.ndarray)
266266
if convert_to_ndarray:
267267
vegtype_str = np.array(vegtype_str)
268+
was_0d = vegtype_str.ndim == 0
269+
vegtype_str = np.atleast_1d(vegtype_str)
268270

269271
if isinstance(vegtype_mainlist, xr.Dataset):
270272
vegtype_mainlist = vegtype_mainlist.vegtype_str.values
@@ -289,6 +291,10 @@ def vegtype_str2int(vegtype_str, vegtype_mainlist=None):
289291
indices[np.where(vegtype_str == vegtype_str_2)] = vegtype_mainlist.index(vegtype_str_2)
290292
if convert_to_ndarray:
291293
indices = [int(x) for x in indices]
294+
295+
if was_0d:
296+
indices = indices[0]
297+
292298
return indices
293299

294300

python/ctsm/crop_calendars/generate_gdd20_baseline.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,7 @@ def _parse_args():
109109
if not os.path.exists(filename):
110110
raise FileNotFoundError(f"Input file not found: {filename}")
111111

112-
# Process time slice
113-
# Assumes CESM behavior where data for e.g. 1987 is saved as 1988-01-01.
114-
# It would be more robust, accounting for upcoming behavior (where timestamp for a year is the
115-
# middle of that year), to do slice("YEAR1-01-03", "YEARN-01-02"), but that's not compatible
116-
# with ctsm_pylib as of the version using python 3.7.9. See safer_timeslice() in cropcal_utils.
117-
if args.first_year is not None:
118-
date_1 = f"{args.first_year+1}-01-01"
119-
else:
120-
date_1 = "0000-01-01"
121-
if args.last_year is not None:
122-
date_n = f"{args.last_year+1}-01-01"
123-
else:
124-
date_n = "9999-12-31"
125-
time_slice = slice(date_1, date_n)
126-
127-
return args, time_slice
112+
return args
128113

129114

130115
def _get_cft_list(crop_list):
@@ -182,7 +167,7 @@ def _get_gddn_for_cft(cft_str, variable):
182167

183168

184169
def _get_output_varname(cft_str):
185-
cft_int = utils.vegtype_str2int(cft_str)[0]
170+
cft_int = utils.vegtype_str2int(cft_str)
186171
return f"gdd20bl_{cft_int}"
187172

188173

@@ -232,7 +217,23 @@ def setup_output_dataset(input_files, author, variable, year_args, ds_in):
232217
return ds_out
233218

234219

235-
def generate_gdd20_baseline(input_files, output_file, author, time_slice, variable, year_args):
220+
def _get_time_slice(year_args):
221+
"""
222+
Based on years from input arguments, return a time slice for selecting from dataset
223+
"""
224+
first_year = year_args[0]
225+
last_year = year_args[1]
226+
date_1 = f"{first_year}-01-01"
227+
date_n = f"{last_year}-12-31"
228+
if first_year is None:
229+
date_1 = "0000-01-01"
230+
if last_year is None:
231+
date_n = "9999-12-31"
232+
time_slice = slice(date_1, date_n)
233+
return time_slice
234+
235+
236+
def generate_gdd20_baseline(input_files, output_file, author, variable, year_args):
236237
"""
237238
Generate stream_fldFileName_gdd20_baseline file from CTSM outputs
238239
"""
@@ -252,6 +253,9 @@ def generate_gdd20_baseline(input_files, output_file, author, time_slice, variab
252253
input_files = list(set(input_files))
253254
input_files.sort()
254255

256+
# Process time slice
257+
time_slice = _get_time_slice(year_args)
258+
255259
# Import history files and ensure they have lat/lon dims
256260
ds_in = import_ds(input_files, my_vars=var_list_in + GRIDDING_VAR_LIST, time_slice=time_slice)
257261
if not all(x in ds_in.dims for x in ["lat", "lon"]):
@@ -275,7 +279,7 @@ def generate_gdd20_baseline(input_files, output_file, author, time_slice, variab
275279
# Process all crops
276280
encoding_dict = {}
277281
for cft_str in MGDCROP_LIST:
278-
cft_int = utils.vegtype_str2int(cft_str)[0]
282+
cft_int = utils.vegtype_str2int(cft_str)
279283
print(f"{cft_str} ({cft_int})")
280284

281285
# Which GDDN history variable does this crop use? E.g., GDD0, GDD10
@@ -323,12 +327,11 @@ def main():
323327
"""
324328
main() function for calling generate_gdd20_baseline.py from command line.
325329
"""
326-
args, time_slice = _parse_args()
330+
args = _parse_args()
327331
generate_gdd20_baseline(
328332
args.input_files,
329333
args.output_file,
330334
args.author,
331-
time_slice,
332335
args.variable,
333336
[args.first_year, args.last_year],
334337
)

python/ctsm/crop_calendars/generate_gdds_functions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ def import_and_process_1yr(
591591
log(logger, f"SKIPPING {vegtype_str}")
592592
continue
593593

594-
vegtype_int = utils.vegtype_str2int(vegtype_str)[0]
594+
vegtype_int = utils.vegtype_str2int(vegtype_str)
595595
this_crop_full_patchlist = list(xr_flexsel(h2_ds, vegtype=vegtype_str).patch.values)
596596

597597
# Get time series for each patch of this type
@@ -1152,7 +1152,7 @@ def make_figures(
11521152
raise RuntimeError(f"If mapping {vegtype_str}, you must provide land use dataset")
11531153
else:
11541154
vegtypes_str = [x for x in incl_vegtypes_str if vegtype_str.lower() in x]
1155-
vegtypes_int = [utils.vegtype_str2int(x)[0] for x in vegtypes_str]
1155+
vegtypes_int = [utils.vegtype_str2int(x) for x in vegtypes_str]
11561156

11571157
# Crop fraction map (for masking and weighting)
11581158
if lu_ds:

python/ctsm/crop_calendars/grid_one_variable.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def create_filled_array(this_ds, fill_value, thisvar_da, new_dims):
8787
"""
8888
Create a Numpy array to be filled with gridded data
8989
"""
90+
91+
if fill_value is None:
92+
fill_value = np.nan
93+
9094
dim_size_list = []
9195
for dim in new_dims:
9296
if dim == "ivt_str":
@@ -97,10 +101,7 @@ def create_filled_array(this_ds, fill_value, thisvar_da, new_dims):
97101
dim_size = this_ds.sizes[dim]
98102
dim_size_list = dim_size_list + [dim_size]
99103
thisvar_gridded = np.empty(dim_size_list)
100-
if fill_value:
101-
thisvar_gridded[:] = fill_value
102-
else:
103-
thisvar_gridded[:] = np.NaN
104+
thisvar_gridded[:] = fill_value
104105
return thisvar_gridded
105106

106107

@@ -160,7 +161,7 @@ def grid_one_variable(this_ds, var, fill_value=None, **kwargs):
160161
# Get DataArrays needed for gridding
161162
thisvar_da, vt_da, spatial_unit, ixy_da, jxy_da = get_ixy_jxy_das(this_ds, var)
162163

163-
if not fill_value and "_FillValue" in thisvar_da.attrs:
164+
if fill_value is None and "_FillValue" in thisvar_da.attrs:
164165
fill_value = thisvar_da.attrs["_FillValue"]
165166

166167
# Renumber vt_da to work as indices on new ivt dimension, if needed.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env python3
2+
3+
"""Unit tests for cropcal_utils.py"""
4+
5+
import unittest
6+
7+
from ctsm import unit_testing
8+
from ctsm.crop_calendars import cropcal_utils as ccu
9+
10+
# Allow names that pylint doesn't like, because otherwise I find it hard
11+
# to make readable unit test names
12+
# pylint: disable=invalid-name
13+
14+
15+
class TestCropCalUtils(unittest.TestCase):
16+
"""Tests of cropcal_utils.py"""
17+
18+
def setUp(self):
19+
self.vegtype_mainlist = ["crop_1", "crop_2", "crop_3"]
20+
21+
def test_vegtype_str2int_1string(self):
22+
"""
23+
Tests vegtype_str2int() for a single string. Result should be an int.
24+
"""
25+
result = ccu.vegtype_str2int("crop_1", vegtype_mainlist=self.vegtype_mainlist)
26+
self.assertEqual(result, 0)
27+
28+
def test_vegtype_str2int_2strings(self):
29+
"""
30+
Tests vegtype_str2int() for two strings. result should be a list of ints.
31+
"""
32+
result = ccu.vegtype_str2int(["crop_1", "crop_3"], vegtype_mainlist=self.vegtype_mainlist)
33+
self.assertListEqual(result, [0, 2])
34+
35+
36+
if __name__ == "__main__":
37+
unit_testing.setup_for_tests()
38+
unittest.main()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Unit tests for grid_one_variable
5+
"""
6+
7+
import unittest
8+
9+
import numpy as np
10+
import xarray as xr
11+
12+
from ctsm import unit_testing
13+
from ctsm.crop_calendars import grid_one_variable as g1v
14+
15+
# Allow test names that pylint doesn't like; otherwise hard to make them
16+
# readable
17+
# pylint: disable=invalid-name
18+
19+
# pylint: disable=protected-access
20+
21+
## Too many instant variables as part of the class (too many self.<varible> in the SetUp)
22+
# pylint: disable=too-many-instance-attributes
23+
24+
25+
class TestCreateFilledArray(unittest.TestCase):
26+
"""Unit tests for create_filled_array"""
27+
28+
def setUp(self):
29+
# Set up this_ds, which will provide us with sizes of dimensions in most cases
30+
lat_vals = [55.0, 56.0, 57.0]
31+
lat_da = xr.DataArray(
32+
data=lat_vals,
33+
dims=["lat"],
34+
coords={"lat": lat_vals},
35+
)
36+
lon_vals = [255.0, 256.0, 257.0]
37+
lon_da = xr.DataArray(
38+
data=lon_vals,
39+
dims=["lon"],
40+
coords={"lon": lon_vals},
41+
)
42+
self.this_ds = xr.Dataset(
43+
data_vars={
44+
"lat": lat_da,
45+
"lon": lon_da,
46+
}
47+
)
48+
49+
def test_create_filled_array_fillNone(self):
50+
"""
51+
Test create_filled_array() with fill_value None: Should be filled with NaN
52+
"""
53+
54+
fill_value = None
55+
thisvar_da_dummy = xr.DataArray()
56+
new_dims = ["lat", "lon"]
57+
58+
result = g1v.create_filled_array(self.this_ds, fill_value, thisvar_da_dummy, new_dims)
59+
60+
self.assertTrue(np.all(np.isnan(result)))
61+
62+
def test_create_filled_array_fill1(self):
63+
"""
64+
Test create_filled_array() with fill_value 1: Should be filled with 1
65+
"""
66+
67+
fill_value = 1.0
68+
thisvar_da_dummy = xr.DataArray()
69+
new_dims = ["lat", "lon"]
70+
71+
result = g1v.create_filled_array(self.this_ds, fill_value, thisvar_da_dummy, new_dims)
72+
73+
self.assertTrue(np.all(result == fill_value))
74+
75+
def test_create_filled_array_fill0(self):
76+
"""
77+
Test create_filled_array() with fill_value 0: Should be filled with 0
78+
"""
79+
80+
fill_value = 0.0
81+
thisvar_da_dummy = xr.DataArray()
82+
new_dims = ["lat", "lon"]
83+
84+
result = g1v.create_filled_array(self.this_ds, fill_value, thisvar_da_dummy, new_dims)
85+
86+
self.assertTrue(np.all(result == fill_value))
87+
88+
89+
if __name__ == "__main__":
90+
unit_testing.setup_for_tests()
91+
unittest.main()

0 commit comments

Comments
 (0)