#!/usr/bin/env python
"""Unstack LMR (year, month) netCDF files into (lon, lat, time) files.
The files we receive store monthly data as a (year, month) grid (with
``year`` documented as "Calendar year", starting at 0) instead of a proper
time axis, use inconsistent variable names, and need small unit fixes
before they are useful. This script converts a whole directory of such
files into CF-ish (lon, lat, time) netCDFs in one go.
Each input variable is handled according to the ``VARIABLES`` /
``STATIC_VARIABLES`` tables below. To support a new file, just add an entry
there -- no other code needs to change. Files named ``_ens###.nc``
are treated as ensemble members and written to an ``ensemble/`` subfolder.
Usage:
python unstack.py orig/ForFabien_Pilot_nc_Round2 unstacked/ForFabien_Pilot_nc_Round2
"""
import argparse
import glob
import os
import re
import cftime
import numpy as np
import xarray as xr
from oggm import cfg
ENS_RE = re.compile(r'^(?P.+)_ens(?P\d+)\.nc$')
def temp_c_to_k(da):
da = da + 273.15
da.attrs['units'] = 'K'
return da
def precip_m_to_flux(da):
"""P_monthly is given in meters per month -> kg m-2 s-1."""
da = da * 1000 # m -> mm == kg m-2 per month
dimo = np.array([cfg.DAYS_IN_MONTH[m - 1] for m in da['time.month']])
da = da / (dimo * (60 * 60 * 24))
da.attrs['units'] = 'kg m-2 s-1'
return da
# input variable name -> (output variable name, output file basename, transform)
VARIABLES = {
'P_monthly': ('pr', 'p_monthly', precip_m_to_flux),
'Tp_monthly': ('tas', 'tp_monthly', temp_c_to_k),
'TpNAT_monthly': ('tas', 'tp_nat_monthly', temp_c_to_k),
}
# input variable name -> (output variable name, output file basename)
STATIC_VARIABLES = {
'elevation': ('elevation', 'elev'),
}
DIM_RENAMES = {'longitude': 'lon', 'latitude': 'lat'}
def stack_to_time(ds, varname, calendar='noleap'):
"""Turn a (year, month) DataArray into a (lat, lon, time) one.
Year 0 is dropped if present: it is not a valid calendar year and
causes errors downstream.
"""
ds = ds.sel(year=ds.year[ds.year != 0])
da = ds[varname]
year_grid, month_grid = xr.broadcast(ds.year, ds.month)
years = year_grid.values.ravel()
months = month_grid.values.ravel()
dates = np.array([
cftime.DatetimeNoLeap(int(y), int(m), 1) for y, m in zip(years, months)
])
da = da.stack(time=('year', 'month'))
da = da.drop_vars(['time', 'year', 'month'])
da['time'] = dates
return da.transpose('lat', 'lon', 'time').sortby('time')
def convert_timeseries_file(path, out_dir):
with xr.open_dataset(path) as ds:
ds = ds.load()
varname = list(ds.data_vars)[0]
if varname not in VARIABLES:
print(f' ! no mapping for variable {varname!r}, skipping {path}')
return
out_var, out_base, transform = VARIABLES[varname]
da = transform(stack_to_time(ds, varname))
da.name = out_var
out_ds = xr.Dataset({out_var: da})
m = ENS_RE.match(os.path.basename(path))
if m:
out_path = os.path.join(out_dir, 'ensemble', f'{out_base}_ens{m.group("num")}.nc')
else:
out_path = os.path.join(out_dir, f'{out_base}.nc')
os.makedirs(os.path.dirname(out_path), exist_ok=True)
out_ds.to_netcdf(out_path)
print(f' wrote {out_path}')
def convert_static_file(path, out_dir):
with xr.open_dataset(path) as ds:
ds = ds.load()
varname = list(ds.data_vars)[0]
if varname not in STATIC_VARIABLES:
print(f' ! no mapping for variable {varname!r}, skipping {path}')
return
out_var, out_base = STATIC_VARIABLES[varname]
rename = {k: v for k, v in DIM_RENAMES.items() if k in ds.coords}
if rename:
ds = ds.rename(rename)
if varname != out_var:
ds = ds.rename({varname: out_var})
out_path = os.path.join(out_dir, f'{out_base}.nc')
os.makedirs(out_dir, exist_ok=True)
ds.to_netcdf(out_path)
print(f' wrote {out_path}')
def process_dir(in_dir, out_dir):
files = sorted(glob.glob(os.path.join(in_dir, '*.nc')))
print(f'Found {len(files)} .nc files in {in_dir}')
for path in files:
with xr.open_dataset(path) as ds:
is_timeseries = 'year' in ds.dims and 'month' in ds.dims
if is_timeseries:
convert_timeseries_file(path, out_dir)
else:
convert_static_file(path, out_dir)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('in_dir', help='directory with the original .nc files')
parser.add_argument('out_dir', help='directory to write unstacked .nc files to')
args = parser.parse_args()
process_dir(args.in_dir, args.out_dir)