8

I'm trying to plot hurricane forecast maps using python. I have several forecast positions derived from official advisories, and interpolate them into a smoothed curve, then draw a polygon of 'cone of uncertainty' based on the curve. Example:

1 2

Basically, 'cone of uncertainty' is the footprint of a moving and enlarging circle. I've tried many approaches but none of them is good enough. My current approach is to produce ~100 circles based on the interpolated curve, and make a compound polygon using cascaded_union method in shapely.

import numpy as np
from shapely.geometry import MultiPolygon
from shapely.ops import cascaded_union
from scipy.interpolate import interp1d
# x, y: coords of forecast position
y = [18.3, 19.2, 20.0, 20.4, 20.7, 21.3, 21.6, 21.5, 20.8, 20.8, 21.5]
x = [111.3, 111.2, 110.9, 110.5, 110.2, 110.5, 110.0, 109.2, 109.4, 110.3, 111.8]
# r: radius of uncertainty
r = [0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5]
hours = [0, 6, 12, 18, 24, 36, 48, 60, 72, 96, 120]
# interpolate
points_num = 100
interp_hours = np.linspace(min(hours), max(hours), points_num)
x = interp1d(hours, x, kind='cubic')(interp_hours)
y = interp1d(hours, y, kind='cubic')(interp_hours)
r = interp1d(hours, r, kind='linear')(interp_hours)
# make polygon
thetas = np.linspace(0, 2 * np.pi, 360)
polygon_x = x[:,None] + r[:,None] * np.sin(thetas)
polygon_y = y[:,None] + r[:,None] * np.cos(thetas)
polygons = MultiPolygon([Polygon(i) for i in np.dstack((polygon_x, polygon_y))])
polygons = cascaded_union(polygons).buffer(0)

But it looks nasty near the starting point:

pic

Increasing the number of circles can only partly address the problem and it takes more time. So I wonder if there is a beautiful, efficient and pythonic way to make the 'cone of uncertainty'? Note that hurricanes may change its directions abruptly, and even be stationary!

PolyGeo
  • 65,136
  • 29
  • 109
  • 338
Tidbits
  • 83
  • 5
  • try looking for convex-hull or alpha-shapes – Ian Turton Jun 24 '19 at 07:30
  • 1
    What you need is a variable width buffer that connects the tangents of subsequent circles, see https://twitter.com/geospacedman/status/978172211579228162 for how QGIS does it. This is not built into Shapely as far as I know, would be super useful though if you fancy a PR. – bugmenot123 Jun 24 '19 at 08:19

3 Answers3

11

Shapely geometries have a convex_hull method.

Should be as simple as polygons.convex_hull, but it will work with any Shapely geometry.

A note on cyclones as a domain: you should use the input cyclone positions as input rather than an interpolated curve: weather forecasts are typically made for moments in time, often 3, 6 or 12 hours apart, and the position in-between is uncertain simply because it is left uncalculated. The convex hull (a special kind of alpha shape) will encompass the spaces in-between the foretasted locations, exactly like in your sample images.

Also be careful with the antimeridian...

Edit: on second thought, you probably want a concave hull, or else to generate convex hulls sequentially, starting with the first pair of error shapes, then with the i+1 and i+2 pair, until complete. Then you union this set of pair-wise convex hulls together. If you do a simple convex hull, then your overall shape will be, well, convex rather than somewhat concave. But a naive concave hull may well be too "tight" and cause intrusions into the path that you don't want.

To illustrate (pseudo-code):

shapes = [a, b, c, d] # Ordered list of shapely geometries
parts = []
for first, second in zip(shapes, shapes[1:]):
    parts.append(union(first, second).convex_hull)
union(*parts)
alphabetasoup
  • 8,718
  • 4
  • 38
  • 78
7

If you need a polygon like in the image below, replace the last lines of your code by the following:

#### firstly, import Polygon class ####
from shapely.geometry import MultiPolygon, Polygon
.
.
.
# make polygon
thetas = np.linspace(0, 2 * np.pi, 360)
polygon_x = x[:,None] + r[:,None] * np.sin(thetas)
polygon_y = y[:,None] + r[:,None] * np.cos(thetas)

# circles
ps = [Polygon(i) for i in np.dstack((polygon_x, polygon_y))]

# list of convex hulls of subsequent circles
n = range(len(ps)-1)
convex_hulls = [MultiPolygon([ps[i], ps[i+1]]).convex_hull for i in n]

# Final polygon
polygons = cascaded_union(convex_hulls)

Convex hulls:

enter image description here

Final result:

enter image description here

Kadir Şahbaz
  • 76,800
  • 56
  • 247
  • 389
2

There is an implementation of VariableWidthBuffer in the JTS Lab here. It uses a union of circles around each line vertex and "prisms" around each line segment. That could be a basis for a Python implementation.

This will make it into JTS sometime soon. Then perhaps into GEOS, where it can be exposed by Shapely.

dr_jts
  • 5,038
  • 18
  • 15
  • Actually reviewing the Lab code I realized that the approach there is too simplistic and does not produce quality buffer outlines. I have a new approach coded up and will be committing that to JTS soon . (Warning: much more math involved!) – dr_jts Nov 19 '19 at 20:22
  • 1
    An improved VariableBuffer implementation is now available in JTS here – dr_jts Nov 20 '19 at 20:30
  • It shouldn't be too hard to port the JTS VariableBuffer code to Python, using Shapely for the geometric heavy lifting. – dr_jts Nov 20 '19 at 20:57