Skip to content

Commit dd42268

Browse files
author
Scott Wales
authored
Create UM SST and sea ice ancillaries from ERA data (#11)
1 parent 5350b84 commit dd42268

File tree

11 files changed

+384
-59
lines changed

11 files changed

+384
-59
lines changed

conda/meta.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ requirements:
1616
- python
1717
- cdo>=1.9.4
1818
- cfunits
19-
- dask
19+
- dask>=1.2.0
2020
- mule
2121
- netcdf4
2222
- numpy
@@ -25,6 +25,7 @@ requirements:
2525
- xarray
2626
- whichcraft # [py2k]
2727
- hdf5>=1.10.1 # Missing dependency of CDO
28+
- iris
2829

2930
test:
3031
requires:

scripts/um/era_sst.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2019 Scott Wales
4+
#
5+
# Author: Scott Wales <[email protected]>
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
"""
20+
Set up ERA-Interim SSTs and sea ice for a UM run
21+
"""
22+
23+
from coecms.regrid import esmf_generate_weights, regrid
24+
import argparse
25+
import xarray
26+
import iris
27+
from dask.diagnostics import ProgressBar
28+
29+
def main():
30+
parser = argparse.ArgumentParser(description=__doc__)
31+
parser.add_argument('--start-date', help='ISO-formatted start date')
32+
parser.add_argument('--end-date', help='ISO-formatted end date')
33+
parser.add_argument('--output', '-o', help='Output file name', required=True)
34+
parser.add_argument('--target-mask', help='Target UM land mask', required=True)
35+
parser.add_argument('--frequency', choices=[6, 12, 24],
36+
type=int, help='Update frequency (hours)', default=24)
37+
args = parser.parse_args()
38+
39+
# Read in the source mask
40+
tos = xarray.open_mfdataset('/g/data1a/ub4/erai/netcdf/6hr/ocean/'
41+
'oper_an_sfc/v01/tos/'
42+
'tos_6hrs_ERAI_historical_an-sfc_2001*.nc',
43+
coords='all')
44+
src_mask = tos.tos.isel(time=0)
45+
46+
# Read in the target mask
47+
mask_iris = iris.load_cube(args.target_mask, iris.AttributeConstraint(STASH='m01s00i030'))
48+
mask_iris.coord('latitude').var_name = 'lat'
49+
mask_iris.coord('longitude').var_name = 'lon'
50+
tgt_mask = xarray.DataArray.from_iris(mask_iris).load()
51+
tgt_mask = tgt_mask.where(tgt_mask == 0)
52+
53+
tgt_mask.lon.attrs['standard_name'] = 'longitude'
54+
tgt_mask.lat.attrs['standard_name'] = 'latitude'
55+
tgt_mask.lon.attrs['units'] = 'degrees_east'
56+
tgt_mask.lat.attrs['units'] = 'degrees_north'
57+
58+
print(tgt_mask)
59+
60+
weights = esmf_generate_weights(src_mask, tgt_mask, method='patch')
61+
62+
with ProgressBar():
63+
64+
# Read and slice the source data
65+
tos = xarray.open_mfdataset('/g/data1a/ub4/erai/netcdf/6hr/ocean/'
66+
'oper_an_sfc/v01/tos/'
67+
'tos_6hrs_ERAI_historical_an-sfc_2001*.nc',
68+
coords='all')
69+
sic = xarray.open_mfdataset('/g/data1a/ub4/erai/netcdf/6hr/seaIce/'
70+
'oper_an_sfc/v01/sic/'
71+
'sic_6hrs_ERAI_historical_an-sfc_2001*.nc',
72+
coords='all')
73+
ds = xarray.Dataset({'tos': tos.tos, 'sic': sic.sic})
74+
ds = ds.sel(time=slice(args.start_date, args.end_date))
75+
print(ds)
76+
77+
newds = regrid(ds, weights=weights)
78+
79+
newds['time'] = newds['time'].astype('i4')
80+
newds.to_netcdf(args.output)
81+
82+
if __name__ == '__main__':
83+
main()

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
],
2525
entry_points = {
2626
'console_scripts': [
27+
'coecms=coecms.cli:cli',
2728
]}
2829
)

src/coecms/cli/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2019 Scott Wales
4+
#
5+
# Author: Scott Wales <[email protected]>
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
from .main import cli
20+
from .um import um

src/coecms/cli/main.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2019 Scott Wales
4+
#
5+
# Author: Scott Wales <[email protected]>
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
import click
20+
21+
@click.group()
22+
def cli():
23+
pass

src/coecms/cli/um.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2019 Scott Wales
4+
#
5+
# Author: Scott Wales <[email protected]>
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
from .main import cli
20+
from ..grid import UMGrid
21+
from ..regrid import regrid, esmf_generate_weights
22+
from ..um.create_ancillary import create_surface_ancillary
23+
import click
24+
import pandas
25+
import mule
26+
import iris
27+
import xarray
28+
from dask.diagnostics import ProgressBar
29+
import dask.distributed
30+
import matplotlib.pyplot as plt
31+
32+
@cli.group()
33+
def um():
34+
"""
35+
Tools for working with the Unified Model
36+
"""
37+
pass
38+
39+
@um.group()
40+
def ancil():
41+
"""
42+
Tools for working with ancil files
43+
"""
44+
pass
45+
46+
def validate_date(ctx, param, value):
47+
"""
48+
Ensures an argument is a valid date
49+
"""
50+
try:
51+
return pandas.to_datetime(value, utc=True, dayfirst=True)
52+
except ValueError:
53+
raise click.BadParameter(f'unable to parse "{value}" as a date')
54+
55+
def validate_um_ancil(ctx, param, value):
56+
"""
57+
Ensures an argument is a UM file
58+
"""
59+
try:
60+
return mule.AncilFile.from_file(value)
61+
except:
62+
raise click.BadParameter(f'"{value}" does not seem to be a UM ancil file')
63+
64+
@ancil.command()
65+
@click.option('--start-date', callback=validate_date, required=True)
66+
@click.option('--end-date', callback=validate_date, required=True)
67+
@click.option('--target-mask',
68+
type=click.Path(exists=True, dir_okay=False))
69+
@click.option('--output', required=True,
70+
type=click.Path(writable=True, dir_okay=False))
71+
def era_sst(start_date, end_date, target_mask, output):
72+
"""
73+
Create ancil files from ERA reanalysis data
74+
"""
75+
76+
um_grid = UMGrid.from_mask(target_mask)
77+
78+
file_start = start_date - pandas.offsets.MonthBegin()
79+
file_end = end_date + pandas.offsets.MonthEnd()
80+
file_a = pandas.date_range(file_start,file_end,freq='MS')
81+
file_b = file_a + pandas.offsets.MonthEnd()
82+
83+
dates = [f'{a.strftime("%Y%m%d")}_{b.strftime("%Y%m%d")}'
84+
for a,b in zip(file_a, file_b)]
85+
86+
# Read and slice the source data
87+
tos = xarray.open_mfdataset(['/g/data1a/ub4/erai/netcdf/6hr/ocean/'
88+
'oper_an_sfc/v01/tos/'
89+
'tos_6hrs_ERAI_historical_an-sfc_'+d+'.nc'
90+
for d in dates],
91+
chunks={'time': 1,})
92+
sic = xarray.open_mfdataset(['/g/data1a/ub4/erai/netcdf/6hr/seaIce/'
93+
'oper_an_sfc/v01/sic/'
94+
'sic_6hrs_ERAI_historical_an-sfc_'+d+'.nc'
95+
for d in dates],
96+
chunks={'time': 1,})
97+
ds = xarray.Dataset({'tos': tos.tos, 'sic': sic.sic})
98+
ds = ds.sel(time=slice(start_date, end_date))
99+
100+
weights = esmf_generate_weights(tos.tos.isel(time=0), um_grid, method='patch')
101+
newds = regrid(ds, weights=weights)
102+
103+
print(newds)
104+
105+
ancil = create_surface_ancillary(newds, {'tos': 24, 'sic': 31})
106+
ancil.to_file(output)

src/coecms/grid.py

+19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import six
2020
import xarray
2121
import numpy
22+
import iris
2223

2324
"""
2425
Different grid types
@@ -174,6 +175,24 @@ def to_scrip(self):
174175
return scrip
175176

176177

178+
class UMGrid(LonLatGrid):
179+
@classmethod
180+
def from_mask(cls, mask_path):
181+
mask = iris.load_cube(mask_path, iris.AttributeConstraint(STASH='m01s00i030'))
182+
mask.coord('latitude').var_name = 'lat'
183+
mask.coord('longitude').var_name = 'lon'
184+
185+
mask = xarray.DataArray.from_iris(mask).load()
186+
mask = mask.where(mask == 0)
187+
188+
mask.lon.attrs['standard_name'] = 'longitude'
189+
mask.lat.attrs['standard_name'] = 'latitude'
190+
mask.lon.attrs['units'] = 'degrees_east'
191+
mask.lat.attrs['units'] = 'degrees_north'
192+
193+
return mask
194+
195+
177196
class ScripGrid(Grid):
178197
def __init__(self, grid):
179198
self._grid = grid

0 commit comments

Comments
 (0)