Skip to content

Commit 1662e9f

Browse files
authored
Merge pull request #97 from rubisco-sfa/alt_calm
ADD: CALM active layer thickness
2 parents 3cef891 + acee13e commit 1662e9f

File tree

7 files changed

+2195
-2052
lines changed

7 files changed

+2195
-2052
lines changed

ilamb3/configure/ilamb.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,21 @@ Cryosphere:
323323
analyses:
324324
- Area Comparison
325325

326+
Active Layer Thickness:
327+
328+
CALM:
329+
sources:
330+
active_layer_thickness: active_layer_thickness/CALM/CALM_1990-2024.nc
331+
transforms:
332+
- select_time:
333+
vmin: 1990-01
334+
vmax: 2023-01
335+
- select_lat:
336+
vmin: 30.
337+
vmax: 90.
338+
- active_layer_thickness:
339+
method: slater2013
340+
326341
Radiation and Energy Cycle:
327342

328343
Surface Upward SW Radiation:

ilamb3/dataset.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import warnings
44
from typing import Any, Literal
55

6+
import cftime as cf
67
import numpy as np
78
import xarray as xr
89
from scipy.interpolate import NearestNDInterpolator
@@ -1050,3 +1051,30 @@ def shift_time_by_years(ds: xr.Dataset, years: int) -> xr.Dataset:
10501051
for t in ds[time_dim]
10511052
]
10521053
return ds
1054+
1055+
1056+
def convert_year_to_datetime(ds: xr.Dataset) -> xr.Dataset:
1057+
"""
1058+
Return the dataset with a integer 'years' dimension converted to a
1059+
CF-compliant time.
1060+
"""
1061+
if "year" not in ds.coords:
1062+
raise ValueError("Function expects a 'year' coordinate with integer years.")
1063+
# We are about to reassign the 'time' dimension, let's remove everything
1064+
# with 'time' in it. We are assuming that it is no longer relevant.
1065+
if "time" in ds.coords:
1066+
ds = ds.drop_vars([v for v in ds if "time" in ds[v].dims]).drop_dims("time")
1067+
years = ds["year"]
1068+
ds = ds.rename_dims(year="time").rename_vars(year="time")
1069+
ds["time"] = [cf.DatetimeNoLeap(y, 7, 1) for y in years]
1070+
ds["time_bnds"] = (
1071+
("time", "nb"),
1072+
np.array(
1073+
[
1074+
[cf.DatetimeNoLeap(y, 1, 1) for y in years],
1075+
[cf.DatetimeNoLeap(y + 1, 1, 1) for y in years],
1076+
]
1077+
).T,
1078+
)
1079+
ds["time"].attrs["bounds"] = "time_bnds"
1080+
return ds

ilamb3/registry/ilamb.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
active_layer_thickness/CALM/CALM.nc sha1:fe084ab58a81911e3739c8bb3fad210b3863af27
1+
active_layer_thickness/CALM/CALM_1990-2024.nc sha1:d874d6efb07ceeb135c30607729653cf0d0c6651
22
biomass/ESACCI/biomass.nc sha1:2eeac984738fc17e8b6cdf0564fff192a4a08718
33
biomass/GEOCARBON/biomass.nc sha1:369235319774f273e376a9e7e3ffd665e5d1c83d
44
biomass/NBCD2000/biomass_0.5x0.5.nc sha1:6354c20dfb61f97454ba0fa70023ee3e959c0c77

ilamb3/tests/test_transform.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@ def gen_msftmz_dset(seed: int = 1):
4141
)
4242

4343

44+
def gen_permafrost_dset(seed: int = 1):
45+
rs = np.random.RandomState(seed)
46+
coords = {}
47+
# time, depth, lat, lon
48+
coords["time"] = [
49+
cf.DatetimeNoLeap(2000 + int(m / 12), (m % 12) + 1, 15) for m in range(3 * 12)
50+
]
51+
coords["depth"] = 0.5 * (
52+
np.linspace(0, 5, 10 + 1)[1:] + np.linspace(0, 5, 10 + 1)[:-1]
53+
)
54+
coords["lat"] = 0.5 * (
55+
np.linspace(60, 90, 10 + 1)[1:] + np.linspace(60, 90, 10 + 1)[:-1]
56+
)
57+
coords["lon"] = 0.5 * (
58+
np.linspace(-180, 180, 20 + 1)[1:] + np.linspace(-180, 180, 20 + 1)[:-1]
59+
)
60+
dims = [c for c in coords]
61+
ds = xr.Dataset(
62+
data_vars={
63+
"tsl": xr.DataArray(
64+
rs.rand(*[len(coords[d]) for d in dims]) * 20 - 18,
65+
coords=coords,
66+
dims=dims,
67+
name="tsl",
68+
attrs={"units": "degC"},
69+
),
70+
}
71+
)
72+
ds = ds.cf.add_bounds("depth")
73+
return ds
74+
75+
4476
def gen_expression_dset(
4577
expr: str,
4678
var_meta: dict[str, dict[str, float | str]],
@@ -100,6 +132,7 @@ def gen_expression_dset(
100132
"depth_gradient": generate_test_dset(
101133
"thetao", "K", nyear=1, nlat=2, nlon=4, ndepth=10
102134
),
135+
"active_layer_thickness": gen_permafrost_dset(),
103136
}
104137

105138

@@ -112,12 +145,17 @@ def gen_expression_dset(
112145
("select_depth", {"value": 0}, "thetao", 9.43861150676275),
113146
("select_depth", {"vmin": 1, "vmax": 40}, "thetao", 9.983843647875275),
114147
("depth_gradient", {}, "thetao", -0.005550578833866464),
148+
(
149+
"active_layer_thickness",
150+
{"method": "slater2013"},
151+
"active_layer_thickness",
152+
2.224852071005917,
153+
),
115154
],
116155
)
117156
def test_transform(name, kwargs, out, value):
118157
transform = ALL_TRANSFORMS[name](**kwargs)
119158
ds = transform(DATA[name])
120-
print(ds[out].mean().values) # this value should be the correct one
121159
assert np.allclose(value, ds[out].mean().values)
122160

123161

ilamb3/transform/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
from ilamb3.transform.expression import expression
33
from ilamb3.transform.gradient import depth_gradient
44
from ilamb3.transform.ohc import ocean_heat_content
5-
from ilamb3.transform.permafrost import permafrost_extent
5+
from ilamb3.transform.permafrost import active_layer_thickness, permafrost_extent
66
from ilamb3.transform.runoff_sensitivity import runoff_sensitivity
77
from ilamb3.transform.select import select_depth, select_lat, select_lon, select_time
88
from ilamb3.transform.soilmoisture import soil_moisture_to_vol_fraction
99
from ilamb3.transform.stratification_index import stratification_index
1010

1111
ALL_TRANSFORMS = {
12+
"active_layer_thickness": active_layer_thickness,
1213
"depth_gradient": depth_gradient,
1314
"expression": expression,
1415
"msftmz_to_rapid": msftmz_to_rapid,

ilamb3/transform/permafrost.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def _permafrost_extent_slater2013(
3333
ds["permafrost_extent"] = (frozen & frozen.shift(year=-1, fill_value=False)).any(
3434
dim=depth_dim
3535
)
36+
ds["permafrost_extent"].attrs = {
37+
"long_name": "Permafrost extent based on slater2013",
38+
"units": "1",
39+
}
3640

3741
# the active layer is then the non-frozen thickness over permafrosted areas
3842
depth_bnds = ds[ds.cf.bounds[depth_dim][0]]
@@ -42,19 +46,29 @@ def _permafrost_extent_slater2013(
4246
ds["active_layer_thickness"] = xr.where(
4347
ds["permafrost_extent"], (~frozen * layer_thickness).sum(dim=depth_dim), np.nan
4448
)
49+
ds["active_layer_thickness"].attrs = {
50+
"long_name": "Active layer thickness based on slater2013",
51+
"units": (
52+
ds[depth_dim].attrs["units"] if "units" in ds[depth_dim].attrs else "m"
53+
),
54+
}
4555

4656
return ds
4757

4858

4959
EXTENT_METHODS = {"slater2013": _permafrost_extent_slater2013}
60+
ALT_METHODS = {"slater2013": _permafrost_extent_slater2013}
5061

5162

5263
class permafrost_extent(ILAMBTransform):
5364
"""
54-
An ILAMB transform for .
65+
An ILAMB transform for estimating permafrost from layered soil temperature
66+
(`tsl`).
5567
5668
Parameters
5769
----------
70+
method: str
71+
The method to use to estimate extent from `tsl`.
5872
"""
5973

6074
def __init__(self, method: Literal["slater2013"] = "slater2013", **kwargs: Any):
@@ -75,12 +89,43 @@ def __call__(self, ds: xr.Dataset) -> xr.Dataset:
7589
if not set(self.required_variables()).issubset(ds):
7690
return ds
7791
ds = EXTENT_METHODS[self.method](ds)
78-
ds["permafrost_extent"].attrs = {
79-
"long_name": f"Permafrost extent based on {self.method}",
80-
"units": "1",
81-
}
8292
ds = ds["permafrost_extent"].any(dim="year").squeeze().to_dataset()
8393
ds["permafrost_extent"] = xr.where(
8494
ds["permafrost_extent"] > 0, ds["permafrost_extent"], np.nan
8595
)
96+
ds = ds.drop_vars("tsl")
97+
return ds
98+
99+
100+
class active_layer_thickness(ILAMBTransform):
101+
"""
102+
An ILAMB transform for estimating active layer thickness from layered soil
103+
temperature (`tsl`).
104+
105+
Parameters
106+
----------
107+
method: str
108+
The method to use to estimate active layer thickness from `tsl`.
109+
"""
110+
111+
def __init__(self, method: Literal["slater2013"] = "slater2013", **kwargs: Any):
112+
self.method = method
113+
114+
def required_variables(self) -> list[str]:
115+
"""
116+
Return the required variables for the transform.
117+
"""
118+
return ["tsl"]
119+
120+
def __call__(self, ds: xr.Dataset) -> xr.Dataset:
121+
"""
122+
Compute
123+
"""
124+
if "active_layer_thickness" in ds:
125+
return ds
126+
if not set(self.required_variables()).issubset(ds):
127+
return ds
128+
ds = ALT_METHODS[self.method](ds)
129+
ds = ds.drop_vars("tsl")
130+
ds = dset.convert_year_to_datetime(ds)
86131
return ds

0 commit comments

Comments
 (0)