Skip to content

Commit 20dff34

Browse files
author
Matt Bertrand
committed
CMAP monthly enhanced mean precipitation data, 1979 - near present
1 parent bafeb2e commit 20dff34

9 files changed

Lines changed: 284 additions & 3 deletions

File tree

ansible/roles/dataqs/tasks/main.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
command: "{{ app_code_dir }}/venvs/geonode/bin/python {{app_code_dir}}/venvs/geonode/src/dataqs/dataqs/hifld/hifld.py"
109109
ignore_errors: yes
110110

111+
- include: geoserver_permissions.yml
112+
113+
- name: Create the cmap layer
114+
command: "{{ app_code_dir }}/venvs/geonode/bin/python {{app_code_dir}}/venvs/geonode/src/dataqs/dataqs/cmap/cmap.py"
115+
ignore_errors: yes
111116

112117
- name: Django updatelayers
113118
django_manage: command=updatelayers

ansible/roles/dataqs/templates/dataq_settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
'dataqs.wqp',
3636
'dataqs.hifld',
3737
'dataqs.gistemp',
38+
'dataqs.cmap',
3839
)
3940

4041
# CELERY SETTINGS
@@ -127,6 +128,11 @@
127128
'task': 'dataqs.gistemp.tasks.gistemp_task',
128129
'schedule': crontab(day_of_month=15, hour=12, minute=0),
129130
'args': ()
131+
},
132+
'cmap': {
133+
'task': 'dataqs.cmap.tasks.cmap_task',
134+
'schedule': crontab(day_of_month=15, hour=12, minute=0),
135+
'args': ()
130136
}
131137
}
132138

dataqs/cmap/__init__.py

Whitespace-only changes.

dataqs/cmap/cmap.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from __future__ import absolute_import
2+
import logging
3+
import os
4+
import re
5+
import shutil
6+
from datetime import date
7+
from ftplib import FTP
8+
9+
from dateutil.relativedelta import relativedelta
10+
from dataqs.processor_base import GeoDataMosaicProcessor, GS_DATA_DIR, \
11+
GS_TMP_DIR
12+
from dataqs.helpers import get_band_count, gdal_translate, \
13+
nc_convert, style_exists, cdo_fixlng
14+
15+
logger = logging.getLogger("dataqs.processors")
16+
script_dir = os.path.dirname(os.path.realpath(__file__))
17+
18+
19+
class CMAPProcessor(GeoDataMosaicProcessor):
20+
"""
21+
Processor for Land-Ocean Temperature Index, ERSSTv4, 1200km smoothing
22+
from the NASA Goddard Institute for Space Studies' Surface Temperature
23+
Analysis (GISTEMP).
24+
More info at http://data.giss.nasa.gov/gistemp/
25+
"""
26+
prefix = "cmap"
27+
base_url = "ftp.cdc.noaa.gov"
28+
base_path = "/Datasets/cmap/enh/"
29+
base_name = "precip.mon.mean.nc"
30+
layer_name = 'cmap'
31+
bounds = '-178.75,178.75,-88.75,88.75'
32+
title = 'CPC Merged Analysis of Precipitation, 1979/01 - {}'
33+
abstract = """The CPC Merged Analysis of Precipitation ("CMAP") is a
34+
technique which produces pentad and monthly analyses of global precipitation
35+
in which observations from raingauges are merged with precipitation estimates
36+
from several satellite-based algorithms (infrared and microwave). The analyses
37+
are are on a 2.5 x 2.5 degree latitude/longitude grid and extend back to 1979.
38+
These data are comparable (but should not be confused with) similarly combined
39+
analyses by the Global Precipitation Climatology Project which are described in
40+
Huffman et al (1997).
41+
42+
It is important to note that the input data sources to make these analyses are
43+
not constant throughout the period of record. For example, SSM/I (passive
44+
microwave - scattering and emission) data became available in July of 1987;
45+
prior to that the only microwave-derived estimates available are from the MSU
46+
algorithm (Spencer 1993) which is emission-based thus precipitation estimates
47+
are avaialble only over oceanic areas. Furthermore, high temporal resolution IR
48+
data from geostationary satellites (every 3-hr) became available during 1986;
49+
prior to that, estimates from the OPI technique (Xie and Arkin 1997) are used
50+
based on OLR from polar orbiting satellites.
51+
52+
The merging technique is thoroughly described in Xie and Arkin (1997). Briefly,
53+
the methodology is a two-step process. First, the random error is reduced by
54+
linearly combining the satellite estimates using the maximum likelihood method,
55+
in which case the linear combination coefficients are inversely proportional to
56+
the square of the local random error of the individual data sources. Over global
57+
land areas the random error is defined for each time period and grid location
58+
by comparing the data source with the raingauge analysis over the surrounding
59+
area. Over oceans, the random error is defined by comparing the data sources
60+
with the raingauge observations over the Pacific atolls. Bias is reduced when
61+
the data sources are blended in the second step using the blending technique of
62+
Reynolds (1988). Here the data output from step 1 is used to define the "shape"
63+
of the precipitation field and the rain gauge data are used to constrain the
64+
amplitude.
65+
66+
Monthly and pentad CMAP estimates back to the 1979 are available from CPC ftp
67+
server.
68+
69+
References:
70+
71+
Huffman, G. J. and co-authors, 1997: The Global Precipitation Climatology
72+
Project (GPCP) combined data set. Bull. Amer. Meteor. Soc., 78, 5-20.
73+
74+
Reynolds, R. W., 1988: A real-time global sea surface temperature analysis. J.
75+
Climate, 1, 75-86.
76+
77+
Spencer, R. W., 1993: Global oceanic precipitation from the MSU during 1979-91
78+
and comparisons to other climatologies. J. Climate, 6, 1301-1326.
79+
80+
Xie P., and P. A. Arkin, 1996: Global precipitation: a 17-year monthly analysis
81+
based on gauge observations, satellite estimates, and numerical model outputs.
82+
Bull. Amer. Meteor. Soc., 78, 2539-2558.
83+
"""
84+
85+
def download(self, url, tmp_dir=GS_TMP_DIR, filename=None):
86+
if not filename:
87+
filename = url.rsplit('/')[-1]
88+
ftp = FTP(self.base_url)
89+
ftp.login('anonymous', 'anonymous')
90+
ftp.cwd(self.base_path)
91+
with open(os.path.join(self.tmp_dir, filename),
92+
'wb') as outfile:
93+
ftp.retrbinary('RETR %s' % self.base_name, outfile.write)
94+
return filename
95+
96+
def convert(self, nc_file):
97+
nc_transform = nc_convert(nc_file)
98+
cdo_transform = cdo_fixlng(nc_transform, bounds=self.bounds)
99+
return cdo_transform
100+
101+
def extract_band(self, tif, band, outname):
102+
outfile = os.path.join(self.tmp_dir, outname)
103+
gdal_translate(tif, outfile, bands=[band],
104+
projection='EPSG:4326',
105+
options=['TILED=YES', 'COMPRESS=LZW'])
106+
return outfile
107+
108+
def get_date(self, months):
109+
start_month = date(1979, 1, 1)
110+
return start_month + relativedelta(months=months - 1)
111+
112+
def get_title(self, months):
113+
end_month = self.get_date(months)
114+
return self.title.format(end_month.strftime('%Y/%m'))
115+
116+
def run(self):
117+
"""
118+
Retrieve and process the latest NetCDF file.
119+
"""
120+
ncfile = self.download(
121+
self.base_url, filename='{}.nc'.format(self.layer_name))
122+
cdf_file = self.convert(os.path.join(self.tmp_dir, ncfile))
123+
bands = get_band_count(cdf_file)
124+
img_list = self.get_mosaic_filenames(self.layer_name)
125+
for band in range(1, bands + 1):
126+
band_date = re.sub('[\-\.]+', '', self.get_date(band).isoformat())
127+
img_name = '{}_{}T000000000Z.tif'.format(self.layer_name, band_date)
128+
if img_name not in img_list:
129+
band_tif = self.extract_band(cdf_file, band, img_name)
130+
dst_file = self.data_dir.format(gsd=GS_DATA_DIR,
131+
ws=self.workspace,
132+
layer=self.layer_name,
133+
file=img_name)
134+
dst_dir = os.path.dirname(dst_file)
135+
if not os.path.exists(dst_dir):
136+
os.makedirs(dst_dir)
137+
if dst_file.endswith('.tif'):
138+
shutil.move(os.path.join(self.tmp_dir, band_tif), dst_file)
139+
self.post_geoserver(dst_file, self.layer_name)
140+
141+
if not style_exists(self.layer_name):
142+
with open(os.path.join(script_dir,
143+
'resources/cmap.sld')) as sld:
144+
self.set_default_style(self.layer_name, self.layer_name,
145+
sld.read().format(latest_band=bands))
146+
self.update_geonode(self.layer_name, title=self.get_title(bands),
147+
description=self.abstract,
148+
store=self.layer_name,
149+
bounds=('-178.75', '178.75', '-88.75', '88.75',
150+
'EPSG:4326'))
151+
self.truncate_gs_cache(self.layer_name)
152+
self.cleanup()
153+
154+
155+
if __name__ == '__main__':
156+
processor = CMAPProcessor()
157+
processor.run()

dataqs/cmap/resources/cmap.nc

43.6 KB
Binary file not shown.

dataqs/cmap/resources/cmap.sld

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?><sld:StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" version="1.0.0">
2+
<sld:NamedLayer>
3+
<sld:Name>cmap</sld:Name>
4+
<sld:UserStyle>
5+
<sld:Name>cmap</sld:Name>
6+
<sld:Title>cmap</sld:Title>
7+
<sld:FeatureTypeStyle>
8+
<sld:Name>name</sld:Name>
9+
<sld:Rule>
10+
<sld:RasterSymbolizer>
11+
<Opacity>1.0</Opacity>
12+
<ChannelSelection>
13+
<GrayChannel>
14+
<SourceChannelName>{latest_band}</SourceChannelName>
15+
</GrayChannel>
16+
</ChannelSelection>
17+
<ColorMap extended="true">
18+
<sld:ColorMapEntry color="#2b83ba" label="0 mm" opacity="1.0" quantity="0"/>
19+
<sld:ColorMapEntry color="#74b6ad" label="10 mm" opacity="1.0" quantity="10"/>
20+
<sld:ColorMapEntry color="#b7e1a7" label="20 mm" opacity="1.0" quantity="20"/>
21+
<sld:ColorMapEntry color="#e7f5b7" label="30 mm" opacity="1.0" quantity="30"/>
22+
<sld:ColorMapEntry color="#fee7a4" label="40 mm" opacity="1.0" quantity="40"/>
23+
<sld:ColorMapEntry color="#fdb96e" label="50 mm" opacity="1.0" quantity="50"/>
24+
<sld:ColorMapEntry color="#ec6e43" label="60 mm" opacity="1.0" quantity="60"/>
25+
<sld:ColorMapEntry color="#d7191c" label="70 mm" opacity="1.0" quantity="70"/>
26+
</ColorMap>
27+
</sld:RasterSymbolizer>
28+
</sld:Rule>
29+
</sld:FeatureTypeStyle>
30+
</sld:UserStyle>
31+
</sld:NamedLayer>
32+
</sld:StyledLayerDescriptor>

dataqs/cmap/tasks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import absolute_import
2+
3+
from celery import shared_task
4+
from dataqs.cmap.cmap import CMAPProcessor
5+
6+
7+
@shared_task
8+
def cmap_task():
9+
processor = CMAPProcessor()
10+
processor.run()

dataqs/cmap/tests.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import glob
2+
import os
3+
import unittest
4+
from datetime import date
5+
from django.test import TestCase
6+
from dataqs.cmap.cmap import CMAPProcessor
7+
8+
script_dir = os.path.dirname(os.path.realpath(__file__))
9+
10+
11+
class CMAPPTest(TestCase):
12+
"""
13+
Tests the dataqs.cmap module. Since each processor is highly
14+
dependent on a running GeoNode instance for most functions, only
15+
independent functions are tested here.
16+
"""
17+
18+
def setUp(self):
19+
self.processor = CMAPProcessor()
20+
self.dl_file = os.path.join(script_dir, 'resources/cmap.nc')
21+
22+
def tearDown(self):
23+
self.processor.cleanup()
24+
25+
def test_convert(self):
26+
"""
27+
Verify that a NetCDF file is converted via netcdf and cdo apps.
28+
Skip if these apps are not installed.
29+
"""
30+
try:
31+
converted_nc = self.processor.convert(self.dl_file)
32+
self.assertTrue(os.path.exists(converted_nc))
33+
except OSError:
34+
# cdo and/or netcdf not installed
35+
raise unittest.SkipTest()
36+
37+
def test_extract_band(self):
38+
"""
39+
Verify that a GeoTIFF file is created.
40+
"""
41+
dl_tif = self.processor.extract_band(
42+
self.dl_file, 1, os.path.join(self.processor.tmp_dir, 'cmap.tif'))
43+
self.assertTrue(os.path.exists(dl_tif))
44+
45+
def test_get_title(self):
46+
"""
47+
Verify that the correct title is returned
48+
"""
49+
title = self.processor.get_title(451)
50+
self.assertEquals(
51+
'CPC Merged Analysis of Precipitation, 1979/01 - 2016/07', title)
52+
53+
def test_get_date(self):
54+
"""
55+
Verify that the correct date is returned
56+
"""
57+
band_date = self.processor.get_date(451)
58+
self.assertEquals(band_date, date(2016, 7, 1))
59+
60+
def test_cleanup(self):
61+
"""
62+
Make sure temporary files are deleted.
63+
"""
64+
self.processor.extract_band(
65+
self.dl_file, 1, os.path.join(self.processor.tmp_dir, 'cmap.tif'))
66+
self.assertNotEqual([], glob.glob(os.path.join(
67+
self.processor.tmp_dir, self.processor.prefix + '*')))
68+
self.processor.cleanup()
69+
self.assertEquals([], glob.glob(os.path.join(
70+
self.processor.tmp_dir, self.processor.prefix + '*')))

dataqs/helpers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def nc_convert(filename):
124124
"""
125125
output_file = re.sub('\.nc$', '.classic.nc', filename)
126126
subprocess.check_call(
127-
["/usr/local/bin/nccopy", "-k", "classic", filename, output_file])
127+
["nccopy", "-k", "classic", filename, output_file])
128128
return output_file
129129

130130

@@ -143,15 +143,16 @@ def cdo_invert(filename):
143143
return output_file
144144

145145

146-
def cdo_fixlng(filename):
146+
def cdo_fixlng(filename, bounds="-180,180,-90,90"):
147147
"""
148148
Change the longitude coordinates of a NetCDF file from 0->360 to -180->180
149149
:param filename: Full path * name of NetCDF image to process
150+
:param bounds: String representation of bounds (minX,maxX,minY,maxY)
150151
:return: output filename
151152
"""
152153

153154
output_file = "{}.lng.nc".format(os.path.splitext(filename)[0])
154-
subprocess.check_call(["cdo", "sellonlatbox,-180,180,-90,90", "{}".format(
155+
subprocess.check_call(["cdo", "sellonlatbox,{}".format(bounds), "{}".format(
155156
filename), output_file])
156157
return output_file
157158

0 commit comments

Comments
 (0)