UCLA snow reanalysis (Margulis et al.)#

The UCLA snow reanalysis is a daily 480 m record of posterior snow water equivalent, snow depth and snow-covered area from a particle-batch smoother that assimilates Landsat fractional snow cover into an ensemble of snow model runs. Each variable comes with five ensemble statistics (mean, standard deviation, median, 25th and 75th percentiles), so the spread is part of the product.

Two regions from NSIDC, both behind Earthdata Login: region="wus" (the default, WUS_UCLA_SR v1, water years 1985-2021 over the western United States) and region="hma" (HMA_SR_D v1, water years 2000-2017 over High Mountain Asia). The granules are one NetCDF-4 file per water year and 1 degree tile; past a handful of them virtualize="auto" switches to cached kerchunk references.

The figures show SWE on 1 April 2020 over Mount Rainier, the water year of basin-mean SWE, and the ensemble spread around it.

import numpy as np

import easysnowdata as esd

aoi = (-121.94, 46.72, -121.54, 46.99)  # Mount Rainier, WA
water_year = "2019-10-01/2020-09-30"

box = esd.parse_aoi(aoi)
grid = box.to_geobox(crs="utm", resolution=500)  # the map below is drawn on this grid
print(box)
print(grid)
AOI(bounds=(-121.9400, 46.7200, -121.5400, 46.9900), clip=True)
GeoBox((62, 63), Affine(500.0, 0.0, 580500.0,
       0.0, -500.0, 5205500.0), CRS('EPSG:32610'))

Where the product comes from, and what each route asks for.

for src in esd.catalog.get("ucla-snow-reanalysis").sources:
    print(f"{src.id:22} {src.title:45} {', '.join(src.requires) or 'no account'}")
nsidc                  NSIDC (Earthdata)                             earthdata
nsidc-hma              NSIDC HMA (Earthdata)                         earthdata

One water year of the ensemble-mean SWE. A single granule is opened straight through earthaccess.open, which the virtualized attribute records. The box, loaded with a 1 km margin so the UTM grid is fully covered, is 366 days of about 65 by 95 pixels, under 10 MB, so it is pulled into memory once and everything after this is instant.

swe_da = esd.snow.ucla_sr.load(box.buffer(1000), water_year).compute()
print(swe_da)
print(f"virtualized={swe_da.attrs['virtualized']}, access={swe_da.attrs['access']}")
<xarray.DataArray 'SWE_Post' (time: 366, latitude: 66, longitude: 97)> Size: 9MB
array([[[0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        ...,
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ]],

       [[0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        ...,
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ]],

       [[0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        ...,
...
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.0006, 0.0202, ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.0012, ..., 0.    , 0.    , 0.    ]],

       [[0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        ...,
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.0006, 0.02  , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.0012, ..., 0.    , 0.    , 0.    ]],

       [[0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        ...,
        [0.    , 0.    , 0.    , ..., 0.    , 0.    , 0.    ],
        [0.    , 0.0006, 0.0182, ..., 0.    , 0.    , 0.    ],
        [0.    , 0.    , 0.001 , ..., 0.    , 0.    , 0.    ]]],
      shape=(366, 66, 97), dtype=float32)
Coordinates:
  * time         (time) datetime64[us] 3kB 2019-10-01 2019-10-02 ... 2020-09-30
  * latitude     (latitude) float32 264B 47.0 46.99 46.99 ... 46.72 46.71 46.71
  * longitude    (longitude) float32 388B -122.0 -121.9 -121.9 ... -121.5 -121.5
    spatial_ref  int64 8B 0
Attributes: (12/19)
    Units:                 meters
    source:                nsidc
    source_id:             nsidc
    source_title:          NSIDC (Earthdata)
    source_url:            https://nsidc.org/data/wus_ucla_sr/versions/1
    product_id:            ucla-snow-reanalysis
    ...                    ...
    statistic:             mean
    variable:              SWE_Post
    virtualized:           False
    access:                indirect
    long_name:             posterior snow water equivalent
    units:                 m
virtualized=False, access=indirect

SWE on 1 April 2020. Like SNODAS, the reanalysis never melts perennial ice out, so SWE over the summit ice cap grows year on year: the maximum in this box is over 100 m while the median is under 1 m. Those are the model’s values, passed through untouched, and the colour scale is capped so the seasonal snowpack around the mountain is visible.

april_da = swe_da.sel(time="2020-04-01")
print(f"median {float(april_da.median()):.2f} m, maximum {float(april_da.max()):.1f} m")
print(f"pixels above 10 m (the ice cap): {int((april_da > 10).sum())}")

ax = esd.plotting.map(
    april_da.odc.reproject(grid, resampling="bilinear"),
    cmap="Blues",
    vmin=0,
    vmax=3,
    title="ensemble-mean SWE, 2020-04-01",
)
ax.figure.tight_layout()
ensemble-mean SWE, 2020-04-01
median 0.62 m, maximum 107.1 m
pixels above 10 m (the ice cap): 143

The water year as a basin mean over the seasonal snowpack only. A fixed cap leaves glacier pixels with a few metres of carried-over SWE in the mean, so the mask here is the water year itself: keep the pixels that melt out.

melts_out_da = swe_da.min(dim="time") < 0.01
print(
    f"pixels that melt out in the water year: {int(melts_out_da.sum())} of {melts_out_da.size}"
)
seasonal_da = swe_da.where(melts_out_da)
mean_da = seasonal_da.mean(dim=["latitude", "longitude"])
mean_da.attrs.update({"long_name": "basin-mean posterior SWE", "units": "m"})

ax = esd.plotting.timeseries(mean_da, color="tab:blue")
ax.set_ylim(bottom=0)
ax.figure.tight_layout()

peak_day = str(mean_da["time"].values[int(np.nanargmax(mean_da.values))])[:10]
print(f"peak basin-mean SWE {float(np.nanmax(mean_da.values)):.2f} m on {peak_day}")
basin-mean posterior SWE
pixels that melt out in the water year: 5636 of 6402
peak basin-mean SWE 0.60 m on 2020-04-05

The ensemble spread: the same water year’s standard deviation, drawn as a band of one standard deviation around the mean.

std_da = esd.snow.ucla_sr.load(box.buffer(1000), water_year, stats="std").compute()
spread_da = std_da.where(melts_out_da).mean(dim=["latitude", "longitude"])

ax = esd.plotting.timeseries(mean_da, color="tab:blue", label="ensemble mean")
ax.fill_between(
    mean_da["time"].values,
    (mean_da - spread_da).values,
    (mean_da + spread_da).values,
    color="tab:blue",
    alpha=0.25,
    linewidth=0,
    label="mean ± 1 standard deviation",
)
ax.set_ylim(bottom=0)
ax.legend(fontsize=8, frameon=False, loc="upper left")
ax.figure.tight_layout()
basin-mean posterior SWE

The High Mountain Asia sibling is the same reanalysis over a different domain, and the same call reaches it:

hma_da = esd.snow.ucla_sr.load(
    (80.0, 30.0, 81.0, 31.0), "2000-10-01/2001-09-30", region="hma"
)
print(f"reference cache for long series: {esd.config.cache_dir('virtual', 'ucla_sr')}")
reference cache for long series: /home/runner/work/_temp/esd-cache/virtual/ucla_sr

Total running time of the script: (1 minutes 5.205 seconds)