Skip to content

Commit d109fc3

Browse files
committed
Use Radar Data from Obs Via Amazon S3 bucket
1 parent 63b8564 commit d109fc3

File tree

5 files changed

+110
-84
lines changed

5 files changed

+110
-84
lines changed

cylc/flow/etc/tutorial/cylc-forecasting-workflow/bin/forecast

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,12 @@ def push_rainfall(rainfall, wind_data, step, resolution, spline_level):
150150
dim_x, dim_y, resolution, resolution,
151151
spline_level)
152152

153+
domain = util.parse_domain(os.environ['DOMAIN'])
154+
153155
while True:
154156
out_of_bounds = []
155157
for itt in range(len(x_values)):
156158
try:
157-
domain = util.parse_domain(os.environ['DOMAIN'])
158159
lng = domain['lng1'] + x_values[itt]
159160
lat = domain['lat1'] + y_values[itt]
160161

cylc/flow/etc/tutorial/cylc-forecasting-workflow/bin/get-observations

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,6 @@ class NoDataException(Exception):
7373
...
7474

7575

76-
def get_datapoint_data(site_id, time, api_key):
77-
"""Get weather data from the DataPoint service."""
78-
# The URL required to get the data.
79-
time = datetime.strptime(time, '%Y%m%dT%H%MZ').strftime('%Y-%m-%dT%H:%MZ')
80-
url = BASE_URL.format(time=time, site_id=site_id, api_key=api_key)
81-
req = requests.get(url)
82-
if req.status_code != 200:
83-
raise Exception(f'{url} returned exit code {req.status_code}')
84-
# Get the data and parse it as JSON.
85-
print('Opening URL: %s' % url)
86-
return req.json()['SiteRep']['DV']['Location']
87-
88-
8976
def get_archived_data(site_id, time):
9077
"""Return archived weather data from the workflow directory."""
9178
print(
@@ -202,13 +189,8 @@ def main(site_id, api_key=None):
202189
site_id, dist = get_nearby_site(site_id, badsites)
203190

204191
if obs is None:
205-
if api_key:
206-
print('Attempting to get weather data from DataPoint...')
207-
data = get_datapoint_data(site_id, cycle_point, api_key)
208-
else:
209-
print('No API key provided, falling back to archived data')
210-
data = get_archived_data(site_id, cycle_point)
211-
192+
print('No API key provided, falling back to archived data')
193+
data = get_archived_data(site_id, cycle_point)
212194
obs = extract_observations(data)
213195

214196
# Write observations.

cylc/flow/etc/tutorial/cylc-forecasting-workflow/bin/get-rainfall

Lines changed: 83 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,20 @@ import math
3838
import os
3939
import shutil
4040

41+
import h5py
42+
from pathlib import Path
43+
import urllib
4144
import requests
4245

43-
try:
44-
from PIL import Image
45-
except ModuleNotFoundError:
46-
# not all PIL installations are created equal
47-
# sometimes we must import like this
48-
import Image
4946
from mercator import get_offset, get_scale, pos_to_coord
5047
import util
5148

52-
53-
URL = ('http://datapoint.metoffice.gov.uk/public/data/layer/wxobs/'
54-
'RADAR_UK_Composite_Highres/png?TIME={time}&key={api_key}')
49+
S3URL = (
50+
'https://met-office-radar-obs-data.s3-eu-west-2.amazonaws.com/radar/'
51+
'{Y}/{m}/{d}/{YYYYmmddHHMM}_ODIM_ng_radar_rainrate_composite_1km_UK.h5'
52+
)
53+
DEBUG=os.environ['CYLC_DEBUG']=='true'
54+
CYLC_TASK_LOG_DIR=os.environ['CYLC_TASK_LOG_DIR']
5555

5656

5757
class Rainfall(object):
@@ -62,16 +62,6 @@ class Rainfall(object):
6262
resolution (float): The length of each grid cell in degrees.
6363
6464
"""
65-
VALUE_MAP = {
66-
(0, 0, 254, 255): 1,
67-
(50, 101, 254, 255): 2,
68-
(127, 127, 0, 255): 3,
69-
(254, 203, 0, 255): 4,
70-
(254, 152, 0, 255): 5,
71-
(254, 0, 0, 255): 6,
72-
(254, 0, 254, 255): 7
73-
}
74-
7565
def __init__(self, domain, resolution):
7666
self.resolution = resolution
7767
self.domain = domain
@@ -98,10 +88,23 @@ class Rainfall(object):
9888
"""
9989
itt_x, itt_y = util.get_grid_coordinates(lng, lat, self.domain,
10090
self.resolution)
101-
try:
102-
self.data[itt_y][itt_x].append(self.VALUE_MAP[value])
103-
except KeyError:
104-
pass
91+
92+
self.data[itt_y][itt_x].append(self.value_map(value))
93+
94+
@staticmethod
95+
def value_map(v):
96+
"""Convert rainfall rate values into colour space values.
97+
98+
Checks if rainfall value above each threshold in turn.
99+
100+
TODO:
101+
- Unit test this
102+
"""
103+
thresholds = {32, 16, 8, 4, 2, 1, .5, .2}
104+
for i, threshold in enumerate(sorted(thresholds, reverse=True)):
105+
if v > threshold:
106+
return 8 - i
107+
return 0
105108

106109
def compute_bins(self):
107110
"""Return this dataset as a 2D matrix."""
@@ -114,25 +117,6 @@ class Rainfall(object):
114117
return self.data
115118

116119

117-
def get_datapoint_radar_image(filename, time, api_key):
118-
"""Retrieve a png image of rainfall from the DataPoint service.
119-
120-
Args:
121-
filename (str): The path to write the image file to.
122-
time (str): The datetime of the image to retrieve in ISO8601 format.
123-
api_key (str): Datapoint API key.
124-
125-
"""
126-
time = datetime.strptime(time, '%Y%m%dT%H%MZ').strftime(
127-
'%Y-%m-%dT%H:%M:%SZ')
128-
url = URL.format(time=time, api_key=api_key)
129-
req = requests.get(url)
130-
if req.status_code != 200:
131-
raise Exception(f'{url} returned exit code {req.status_code}')
132-
with open(filename, 'bw') as png_file:
133-
png_file.write(req.content)
134-
135-
136120
def get_archived_radar_image(filename, time):
137121
"""Retrieve a png image from the archived data in the workflow directory.
138122
@@ -147,8 +131,23 @@ def get_archived_radar_image(filename, time):
147131
filename)
148132

149133

134+
def get_amazon_radar_data(filename, time):
135+
time = datetime.strptime(time, '%Y%m%dT%H%MZ')
136+
url = S3URL.format(
137+
Y=time.strftime('%Y'),
138+
m=time.strftime('%m'),
139+
d=time.strftime('%d'),
140+
YYYYmmddHHMM=time.strftime('%Y%m%d%H%M')
141+
)
142+
print(f'[INFO] Getting data from {url=}')
143+
data = urllib.request.urlopen(url).read()
144+
with open(filename, 'wb') as fh:
145+
fh.write(data)
146+
147+
148+
# def process_rainfall_data(filename, resolution, domain):
150149
def process_rainfall_data(filename, resolution, domain):
151-
"""Generate a 2D matrix of data from the rainfall data in the image.
150+
"""get_amazon_radar_dataGenerate a 2D matrix of data from the rainfall data in the image.
152151
153152
Args:
154153
filename (str): Path to the png image to process.
@@ -160,37 +159,64 @@ def process_rainfall_data(filename, resolution, domain):
160159
list - A 2D matrix of rainfall data.
161160
162161
"""
162+
print(f'{util.INFO}Analysing data from {filename}')
163+
data = h5py.File(filename)['dataset1']['data1']['data']
163164
rainfall = Rainfall(domain, resolution)
164165

165-
image = Image.open(filename)
166-
scale = get_scale(domain, image.width)
166+
# image = Image.open(filename)
167+
height, width = data.shape
168+
169+
# TODO - work out why this isn't working
170+
scale = get_scale(domain, width)
167171
offset = get_offset(domain, scale)
172+
scale = (1672.2334443981335, 3344.466888796267)
173+
offset = (-1100.8461538461539, 1400.6953225710452)
174+
175+
if DEBUG:
176+
from matplotlib import pyplot as plt
177+
Path('CYLC_TASK_LOG_DIR').mkdir(parents=True, exist_ok=True)
178+
plt.imshow(data)
179+
plt.savefig(f'{os.environ['CYLC_TASK_LOG_DIR']}/raw.data.png')
168180

169-
for itt_x in range(image.width):
170-
for itt_y in range(image.height):
181+
for itt_x in range(width):
182+
for itt_y in range(height):
171183
lng, lat = pos_to_coord(
172184
itt_x,
173185
itt_y * (2. / 3.), # Counter aspect ratio.
174-
offset, scale)
175-
rainfall.add(lng, lat, image.getpixel((itt_x, itt_y)))
186+
offset,
187+
scale
188+
)
189+
val = float(data[height - 1 - itt_y][itt_x])
190+
val = 0 if val == -1 else val
191+
rainfall.add(lng, lat, val)
192+
data = rainfall.compute_bins()
176193

177-
return rainfall.compute_bins()
194+
if DEBUG:
195+
plt.imshow(data)
196+
plt.savefig(f'{CYLC_TASK_LOG_DIR}/processed.data.png')
178197

198+
return data
179199

180200
def main():
181201
time = os.environ['CYLC_TASK_CYCLE_POINT']
182202
resolution = float(os.environ['RESOLUTION'])
183203
domain = util.parse_domain(os.environ['DOMAIN'])
184-
api_key = os.environ.get('API_KEY')
185204

186-
if api_key:
187-
print('Attempting to get weather data from the DataPoint service.')
188-
get_datapoint_radar_image('rainfall-radar.png', time, api_key)
205+
# Acts as a switch - if a file-name is given, use that file-name
206+
# TODO - keep the nice S3 formatting with implied metadata
207+
canned_data = os.environ.get('CANNED_DATA', 'fetch')
208+
209+
if canned_data == 'fetch':
210+
print(f'{util.INFO}Attempting to get rainfall data from S3 bucket')
211+
get_amazon_radar_data('radardata.h5', time)
212+
canned_data = 'radardata.h5'
189213
else:
190-
print('No API key provided, falling back to archived data')
191-
get_archived_radar_image('rainfall-radar.png', time)
214+
print(
215+
f'{util.INFO}Canned data provided: {canned_data}')
216+
# get_archived_radar_data(canned_data, time)
217+
218+
data = process_rainfall_data(canned_data, resolution, domain)
192219

193-
data = process_rainfall_data('rainfall-radar.png', resolution, domain)
194220
util.write_csv('rainfall.csv', data)
195221

196222

cylc/flow/etc/tutorial/cylc-forecasting-workflow/flow.cylc

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,24 @@
3939
[[[environment]]]
4040
# The dimensions of each grid cell in degrees.
4141
RESOLUTION = 0.2
42+
4243
# The area to generate forecasts for (lng1, lat1, lng2, lat2)
43-
DOMAIN = -12,48,5,61 # Do not change!
44+
# Amazon S3 domain (my interpolation)
45+
DOMAIN = -14,44,12,63
46+
47+
[[[meta]]]
48+
metadata source = Met Office Observations R&D
49+
ellipsiod = "Airy 1830"
50+
projection = "Transverse Mercator"
51+
origin = "49,2"
52+
SW = 43.8341,-12.0118
53+
NW = 62.9207,-17.9739
54+
NE = 62.6515,16.1289
55+
SE = 437009,941578
56+
old domain = -12,48,5,61
4457

4558
[[get_observations<station>]]
4659
script = get-observations
47-
[[[environment]]]
48-
# The key required to get weather data from the DataPoint service.
49-
# To use archived data comment this line out.
50-
API_KEY = DATAPOINT_API_KEY
5160

5261
[[get_observations<station=aldergrove>]]
5362
[[[environment]]]
@@ -70,7 +79,13 @@
7079
[[[environment]]]
7180
# The key required to get weather data from the DataPoint service.
7281
# To use archived data comment this line out.
73-
API_KEY = DATAPOINT_API_KEY
82+
API_KEY = amazon
83+
MAP_FILE = "${CYLC_TASK_LOG_ROOT}-map.html"
84+
85+
# Create the html map file in the task's log directory.
86+
MAP_FILE = "${CYLC_TASK_LOG_ROOT}-map.html"
87+
# The path to the template file used to generate the html map.
88+
MAP_TEMPLATE = "$CYLC_WORKFLOW_RUN_DIR/lib/template/map.html"
7489

7590
[[forecast]]
7691
script = forecast 60 5 # Generate 5 forecasts at 60 minute intervals.
@@ -90,5 +105,6 @@
90105
[[post_process<site>]]
91106
# Generate a forecast for the location <site> 60 minutes in the future.
92107
script = post-process $CYLC_TASK_PARAM_site 60
108+
post-script = sleep 365000
93109

94110
{% include 'etc/python-job.settings' %}

cylc/flow/etc/tutorial/cylc-forecasting-workflow/lib/python/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import jinja2
2929

3030
R_0 = 6371. # Radius of the Earth (km).
31+
INFO = '[INFO] '
3132

3233

3334
def frange(start, stop, step):

0 commit comments

Comments
 (0)