Skip to content

Commit 47af945

Browse files
committed
Use Radar Data from Obs Via Amazon S3 bucket
Remove unwated ref to datapoint API key remove all references to datapoint api keys
1 parent 63b8564 commit 47af945

File tree

8 files changed

+122
-111
lines changed

8 files changed

+122
-111
lines changed

cylc/flow/etc/tutorial/api-keys

Lines changed: 0 additions & 3 deletions
This file was deleted.

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
set -eux
19-
APIKEY="$(head --lines 1 ../api-keys)"
2019
FLOW_NAME="$(< /dev/urandom tr -dc A-Za-z | head -c6)"
2120
cylc lint .
2221
cylc install --workflow-name "$FLOW_NAME" --no-run-name
23-
sed -i "s/DATAPOINT_API_KEY/$APIKEY/" "$HOME/cylc-run/$FLOW_NAME/flow.cylc"
2422
cylc validate --check-circular --icp=2000 "$FLOW_NAME"
2523
cylc play --no-detach --abort-if-any-task-fails "$FLOW_NAME"
2624
cylc clean "$FLOW_NAME"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def plot_wind_data(wind_x, wind_y, x_range, y_range, x_coords, y_coords,
6161
[x[0] for x in z_coords],
6262
[y[1] for y in z_coords],
6363
color='red')
64-
plt.savefig('wind.png')
64+
plt.savefig(f'{os.environ["CYLC_TASK_LOG_DIR"]}/wind.png')
6565

6666

6767
def get_wind_fields():

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: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,8 @@ Usage:
2424
get-observations
2525
2626
Environment Variables:
27-
SITE_ID: The four digit DataPoint site code identifying the weather station
28-
we are to fetch results for.
29-
API_KEY: The DataPoint API key, required for getting live weather data.
30-
If un-specified then get-observations will fall back to archive data
31-
from the workflow directory.
27+
SITE_ID: The four digit WMO (World Meteorological Organization)
28+
site code identifying the weather station we are to fetch results for.
3229
3330
"""
3431

@@ -43,10 +40,6 @@ import requests
4340
import util
4441

4542

46-
BASE_URL = ('http://datapoint.metoffice.gov.uk/public/data/'
47-
'val/wxobs/all/json/{site_id}'
48-
'?res=hourly&time={time}&key={api_key}')
49-
5043
# Compass bearings for ordinate directions.
5144
# NOTE: We measure direction by where the winds have come from!
5245
ORDINATES = {
@@ -73,19 +66,6 @@ class NoDataException(Exception):
7366
...
7467

7568

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-
8969
def get_archived_data(site_id, time):
9070
"""Return archived weather data from the workflow directory."""
9171
print(
@@ -152,8 +132,12 @@ def synop_grab(site_id, time):
152132
raise NoDataException(
153133
f'Request for data failed, raw request was {req.text}')
154134

135+
# Convert direction from (meteorlogical convention) to
136+
# direction to:
137+
data['direction'] = (int(data['direction']) + 18) % 36
138+
155139
# Parse the direction from 10's of degrees to degrees:
156-
data['direction'] = str(int(data['direction']) * 10)
140+
data['direction'] = str(data['direction'] * 10)
157141

158142
# Convert data in KT to MPH:
159143
data['speed'] = str(int(data['speed']) * 1.15078)
@@ -185,7 +169,7 @@ def get_nearby_site(site_id, badsites):
185169
return int(result[0]), dist
186170

187171

188-
def main(site_id, api_key=None):
172+
def main(site_id):
189173
cycle_point = os.environ['CYLC_TASK_CYCLE_POINT']
190174

191175
# Try to get the information from SYNOPS.
@@ -202,13 +186,8 @@ def main(site_id, api_key=None):
202186
site_id, dist = get_nearby_site(site_id, badsites)
203187

204188
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-
189+
print('Obs unavailable, falling back to archived data')
190+
data = get_archived_data(site_id, cycle_point)
212191
obs = extract_observations(data)
213192

214193
# Write observations.
@@ -218,5 +197,4 @@ def main(site_id, api_key=None):
218197

219198
if __name__ == '__main__':
220199
util.sleep()
221-
main(os.environ['SITE_ID'],
222-
os.environ.get('API_KEY'))
200+
main(os.environ['SITE_ID'])

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

Lines changed: 89 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ Usage:
2424
get-rainfall
2525
2626
Environment Variables:
27-
API_KEY: The DataPoint API key, required for getting live weather data.
28-
If un-specified then get-observations will fall back to archive data
29-
from the workflow directory.
3027
DOMAIN: The area in which to generate forecasts for in the format
3128
(lng1, lat1, lng2, lat2).
3229
RESOLUTION: The length/width of each grid cell in degrees.
@@ -38,20 +35,20 @@ import math
3835
import os
3936
import shutil
4037

41-
import requests
38+
import h5py
39+
from pathlib import Path
40+
import urllib
41+
import requests # noqa: F401 - required implicitly by urllib.
4242

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
4943
from mercator import get_offset, get_scale, pos_to_coord
5044
import util
5145

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

5653

5754
class Rainfall(object):
@@ -62,16 +59,6 @@ class Rainfall(object):
6259
resolution (float): The length of each grid cell in degrees.
6360
6461
"""
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-
7562
def __init__(self, domain, resolution):
7663
self.resolution = resolution
7764
self.domain = domain
@@ -98,10 +85,23 @@ class Rainfall(object):
9885
"""
9986
itt_x, itt_y = util.get_grid_coordinates(lng, lat, self.domain,
10087
self.resolution)
101-
try:
102-
self.data[itt_y][itt_x].append(self.VALUE_MAP[value])
103-
except KeyError:
104-
pass
88+
89+
self.data[itt_y][itt_x].append(self.value_map(value))
90+
91+
@staticmethod
92+
def value_map(v):
93+
"""Convert rainfall rate values into colour space values.
94+
95+
Checks if rainfall value above each threshold in turn.
96+
97+
TODO:
98+
- Unit test this
99+
"""
100+
thresholds = {32, 16, 8, 4, 2, 1, .5, .2}
101+
for i, threshold in enumerate(sorted(thresholds, reverse=True)):
102+
if v > threshold:
103+
return 8 - i
104+
return 0
105105

106106
def compute_bins(self):
107107
"""Return this dataset as a 2D matrix."""
@@ -114,25 +114,6 @@ class Rainfall(object):
114114
return self.data
115115

116116

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-
136117
def get_archived_radar_image(filename, time):
137118
"""Retrieve a png image from the archived data in the workflow directory.
138119
@@ -147,8 +128,23 @@ def get_archived_radar_image(filename, time):
147128
filename)
148129

149130

131+
def get_amazon_radar_data(filename, time):
132+
time = datetime.strptime(time, '%Y%m%dT%H%MZ')
133+
url = S3URL.format(
134+
Y=time.strftime('%Y'),
135+
m=time.strftime('%m'),
136+
d=time.strftime('%d'),
137+
YYYYmmddHHMM=time.strftime('%Y%m%d%H%M')
138+
)
139+
print(f'[INFO] Getting data from {url=}')
140+
data = urllib.request.urlopen(url).read()
141+
with open(filename, 'wb') as fh:
142+
fh.write(data)
143+
144+
145+
# def process_rainfall_data(filename, resolution, domain):
150146
def process_rainfall_data(filename, resolution, domain):
151-
"""Generate a 2D matrix of data from the rainfall data in the image.
147+
"""get_amazon_radar_dataGenerate a 2D matrix of data from the rainfall data in the image.
152148
153149
Args:
154150
filename (str): Path to the png image to process.
@@ -160,37 +156,67 @@ def process_rainfall_data(filename, resolution, domain):
160156
list - A 2D matrix of rainfall data.
161157
162158
"""
159+
print(f'{util.INFO}Analysing data from {filename}')
160+
data = h5py.File(filename)['dataset1']['data1']['data']
163161
rainfall = Rainfall(domain, resolution)
164162

165-
image = Image.open(filename)
166-
scale = get_scale(domain, image.width)
167-
offset = get_offset(domain, scale)
168-
169-
for itt_x in range(image.width):
170-
for itt_y in range(image.height):
163+
# image = Image.open(filename)
164+
height, width = data.shape
165+
166+
scale = get_scale(domain, width)
167+
# TODO Fix Mercator - the new data is in a totally
168+
# different projection, transverse Mercator, rather
169+
# than Mercator.
170+
# offset = get_offset(domain, scale)
171+
offset = (-1100.8461538461539, 1400.6953225710452)
172+
173+
if DEBUG:
174+
print(f'[INFO] {scale=}, {offset=}')
175+
from matplotlib import pyplot as plt
176+
Path(CYLC_TASK_LOG_DIR).mkdir(parents=True, exist_ok=True)
177+
plt.imshow(data)
178+
plt.savefig(f'{CYLC_TASK_LOG_DIR}/raw.data.png')
179+
180+
for itt_x in range(width):
181+
for itt_y in range(height):
171182
lng, lat = pos_to_coord(
172183
itt_x,
173184
itt_y * (2. / 3.), # Counter aspect ratio.
174-
offset, scale)
175-
rainfall.add(lng, lat, image.getpixel((itt_x, itt_y)))
185+
offset,
186+
scale
187+
)
188+
val = float(data[itt_y][itt_x])
189+
# Original data uses -1 to indicate radar mask
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: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,22 @@
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+
DOMAIN = -12,46,12,61
45+
46+
[[[meta]]]
47+
metadata source = Met Office Observations R&D
48+
ellipsiod = "Airy 1830"
49+
projection = "Transverse Mercator"
50+
origin = "49,2"
51+
SW = 43.8341,-12.0118
52+
NW = 62.9207,-17.9739
53+
NE = 62.6515,16.1289
54+
SE = 437009,941578
4455

4556
[[get_observations<station>]]
4657
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
5158

5259
[[get_observations<station=aldergrove>]]
5360
[[[environment]]]
@@ -68,9 +75,12 @@
6875
[[get_rainfall]]
6976
script = get-rainfall
7077
[[[environment]]]
71-
# The key required to get weather data from the DataPoint service.
72-
# To use archived data comment this line out.
73-
API_KEY = DATAPOINT_API_KEY
78+
# A template Map file
79+
MAP_FILE = "${CYLC_TASK_LOG_ROOT}-map.html"
80+
# Create the html map file in the task's log directory.
81+
MAP_FILE = "${CYLC_TASK_LOG_ROOT}-map.html"
82+
# The path to the template file used to generate the html map.
83+
MAP_TEMPLATE = "$CYLC_WORKFLOW_RUN_DIR/lib/template/map.html"
7484

7585
[[forecast]]
7686
script = forecast 60 5 # Generate 5 forecasts at 60 minute intervals.

0 commit comments

Comments
 (0)