Why is hexbin grid distorted in polar stereo projection - python

I am trying to understand why a hexbin plot in a north or south polar stereo projection shows squashed hexagons, even though the area of the grid is square and the projection is approximately equal area.
I've tried both north and south polar stereo projections using basemap.
import numpy as np
from numpy.random import uniform
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
fig = plt.figure(figsize=(12,10)) # width, height in inches
ax =fig.add_axes([-0.02,0.1,0.74,0.74])
m = Basemap(epsg='3413',lon_0=0.,resolution='l',width=6000000,height=6000000)
m.drawcoastlines()
m.drawmapscale(0.,90.,0.,90.,1000)
npts=2000
lats = uniform(60.,80.,size=npts)
lons = uniform(0.,360.,size=npts)
data = uniform(0.,4800.,size=npts)
x,y=m(lons, lats)
thiscmap=plt.cm.get_cmap('viridis')
p=m.hexbin(x,y,C=data,gridsize=[10,10],cmap=thiscmap)
plt.show()

I don't know why you get squashed hexagons. But you can adjust the hexagon shape by setting appropriate values of gridsize. Here I modify your code and get better plot.
import numpy as np
from numpy.random import uniform
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
fig = plt.figure(figsize=(12,10)) # width, height in inches
ax =fig.add_axes([-0.02, 0.1, 0.74, 0.74])
# North polar stereographic projection epsg='3413'; ***large areal distortion***
#m = Basemap(epsg='3413', lon_0=0., resolution='c', width=6000000, height=6000000)
# 'laea': Lambert Azimuthal Equal Area
# Thematic mapping with ground surface data should be plotted on 'equal-area' projection
m = Basemap(projection='laea', lon_0=0., lat_0=90, resolution='l', width=6000000, height=6000000)
m.drawcoastlines(linewidth=0.5)
m.drawmapscale(0.,90.,0.,90.,1000) # 1000 km?
npts = 2000
lats = uniform(60.,80.,size=npts) # not cover N pole
lons = uniform(0.,360.,size=npts) # around W to E
data = uniform(0.,4800.,size=npts)
x,y = m(lons, lats)
thiscmap = plt.cm.get_cmap('viridis')
# To get 'rounded' hexagons, gridsize should be specified appropriately
# need some trial and error to get them right
#p=m.hexbin(x, y, C=data, gridsize=[10,10], cmap=thiscmap) # original code
m.hexbin(x, y, C=data, gridsize=[16,11], cmap=thiscmap) # better
plt.colorbar() # useful on thematic map
plt.show()
The projection you use (epsg:3413) is stereographic projection which has large areal distortion. More appropriate projection for thematic mapping in this case is Lambert Azimuthal Equal Area.
The resulting plot:

Related

Cartopy projection scale not consistent

I am using cartopy to display a KDE overlayed on a world map. Initially, I was using the ccrs.PlateCarree projection with no issues, but the moment I tried to use another projection it seemed to explode the scale of the projection. For reference, I have included an example that you can test on your own machine below (just comment out the two projec lines to switch between projections)
from scipy.stats import gaussian_kde
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
projec = ccrs.PlateCarree()
#projec = ccrs.InterruptedGoodeHomolosine()
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(projection=projec)
np.random.seed(1)
discrete_points = np.random.randint(0,10,size=(2,400))
kde = gaussian_kde(discrete_points)
x, y = discrete_points
# https://www.oreilly.com/library/view/python-data-science/9781491912126/ch04.html
resolution = 1
x_step = int((max(x)-min(x))/resolution)
y_step = int((max(y)-min(y))/resolution)
xgrid = np.linspace(min(x), max(x), x_step+1)
ygrid = np.linspace(min(y), max(y), y_step+1)
Xgrid, Ygrid = np.meshgrid(xgrid, ygrid)
Z = kde.evaluate(np.vstack([Xgrid.ravel(), Ygrid.ravel()]))
Zgrid = Z.reshape(Xgrid.shape)
ext = [min(x)*5, max(x)*5, min(y)*5, max(y)*5]
earth = plt.cm.gist_earth_r
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', '50m',
edgecolor='black', facecolor="none"))
ax.imshow(Zgrid,
origin='lower', aspect='auto',
extent=ext,
alpha=0.8,
cmap=earth, transform=projec)
ax.axis('on')
ax.get_xaxis().set_visible(True)
ax.get_yaxis().set_visible(True)
ax.set_xlim(-30, 90)
ax.set_ylim(-60, 60)
plt.show()
You'll notice that when using the ccrs.PlateCarree() projection, the KDE is nicely placed over Africa, however when using the ccrs.InterruptedGoodeHomolosine() projection, you can't see the world map at all. This is because the world map is on an enormous scale. Below is an image of both examples:
Plate Carree projection:
Interrupted Goode Homolosine projection (standard zoom):
Interrupted Goode Homolosine projection (zoomed out):
If anyone could explain why this is occurring, and how to fix it so I can plot the same data on different projections, that would be greatly appreciated.
EDIT:
I would also like to specify that I tried adding transform=projec to line 37 in the example I included, namely:
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', '50m',
edgecolor='black', facecolor="none", transform=projec))
However this did not help. In fact, it seemed upon adding this the world map no longer appeared at all.
EDIT:
In response to JohanC's answer, this is the plot I get when using that code:
And zoomed out:
Comments on your plots:
Plot1: (the reference map)
projection: PlateCarree projection
(Zgrid) image extents cover (approx) square area, about 40 degrees on each side
image's lower-left corner is at lat/long: (0,0)
Plot2
Q: Why the topo features are not shown on the map?
A: The plot covers very small area that does not include any of them.
projection: InterruptedGoodeHomolosine
the image data, Zgrid is declared to fit within grid (mapprojection) coordinates (unit: meters)
the map is plotted within a small extents of a few meters in both x and y, and aspect ratio is not equal.
Plot3
Q: Why the Zgrid image are not seen on the map?
A: The plot covers very large area that the image become too small to plot.
projection: InterruptedGoodeHomolosine projection
the (Zgrid) image extent is very small, not visible at this scale
the map is plotted within a large extents, and aspect ratio is not equal.
The remedies (for Plot2 and 3)
Zgrid need proper transformation from lat/long to the axes' projection coordinates
map's extents also need to be transformed and set appropriately
the aspect ratio must be set 'equal', to prevent unequal stretches in x and y
About 'gridlines' plots
useful for location reference
latitude/parallels: OK with InterruptedGoodeHomolosine in this case
longitude/meridians: is problematic (dont know how to fix !!)
Here is the modified code that runs and produces the required map.
# proposed code
from scipy.stats import gaussian_kde
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
fig = plt.figure(figsize=(7, 12))
ax = plt.axes(projection=ccrs.InterruptedGoodeHomolosine())
np.random.seed(1)
discrete_points = np.random.randint(0,10,size=(2,400))
kde = gaussian_kde(discrete_points)
x, y = discrete_points
# https://www.oreilly.com/library/view/python-data-science/9781491912126/ch04.html
resolution = 1
x_step = int((max(x)-min(x))/resolution)
y_step = int((max(y)-min(y))/resolution)
xgrid = np.linspace(min(x), max(x), x_step+1)
ygrid = np.linspace(min(y), max(y), y_step+1)
Xgrid, Ygrid = np.meshgrid(xgrid, ygrid)
Z = kde.evaluate(np.vstack([Xgrid.ravel(), Ygrid.ravel()]))
Zgrid = Z.reshape(Xgrid.shape)
ext = [min(x)*5, max(x)*5, min(y)*5, max(y)*5]
earth = plt.cm.gist_earth_r
ocean110 = cfeature.NaturalEarthFeature('physical', 'ocean', \
scale='110m', edgecolor='none', facecolor=cfeature.COLORS['water'])
ax.add_feature(ocean110, zorder=-5)
land110 = cfeature.NaturalEarthFeature('physical', 'land', '110m', \
edgecolor='black', facecolor="silver")
ax.add_feature(land110, zorder=5)
# extents used by both Zgrid and axes
ext = [min(x)*5, max(x)*5, min(y)*5, max(y)*5]
# plot the image's data array
# note the options: `extent` and `transform`
ax.imshow(Zgrid,
origin='lower', aspect='auto',
extent=ext, #set image's extent
alpha=0.75,
cmap=earth, transform=ccrs.PlateCarree(),
zorder=10)
# set the plot's extent with proper coord transformation
ax.set_extent(ext, ccrs.PlateCarree())
ax.coastlines()
#ax.add_feature(cfeature.BORDERS) #uncomment if you need
ax.gridlines(linestyle=':', linewidth=1, draw_labels=True, dms=True, zorder=30, color='k')
ax.set_aspect('equal') #make sure the aspect ratio is 1
plt.show()
The output map:

How to plot the field of view of an Earth-Observation satellite when close to the poles using basemap?

I am trying to draw the maximum (theoretical) field of view of a satellite along its orbit. I am using Basemap, on which I want to plot different positions along the orbit (with scatter), and I would like to add the whole field of view using the tissot method (or equivalent).
The code below works fine until the latitude reaches about 75 degrees North, on a 300km altitude orbit. Beyond which the code outputs a ValueError:
"ValueError: undefined inverse geodesic (may be an antipodal point)"
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import math
earth_radius = 6371000. # m
fig = plt.figure(figsize=(8, 6), edgecolor='w')
m = Basemap(projection='cyl', resolution='l',
llcrnrlat=-90, urcrnrlat=90,
llcrnrlon=-180, urcrnrlon=180)
# draw the coastlines on the empty map
m.drawcoastlines(color='k')
# define the position of the satellite
position = [300000., 75., 0.] # altitude, latitude, longitude
# radius needed by the tissot method
radius = math.degrees(math.acos(earth_radius / (earth_radius + position[0])))
m.tissot(position[2], position[1], radius, 100, facecolor='tab:blue', alpha=0.3)
m.scatter(position[2], position[1], marker='*', c='tab:red')
plt.show()
To be noted that the code works fine at the south pole (latitude lower than -75). I know it's a known bug, is there a known workaround for this issue?
Thanks for your help!
What you found is some of Basemap's limitations. Let's switch to Cartopy for now. The working code will be different but not much.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import math
earth_radius = 6371000.
position = [300000., 75., 0.] # altitude (m), lat, long
radius = math.degrees(math.acos(earth_radius / (earth_radius + position[0])))
print(radius) # in subtended degrees??
fig = plt.figure(figsize=(12,8))
img_extent = [-180, 180, -90, 90]
# here, cartopy's' `PlateCarree` is equivalent with Basemap's `cyl` you use
ax = fig.add_subplot(1, 1, 1, projection = ccrs.PlateCarree(), extent = img_extent)
# for demo purposes, ...
# let's take 1 subtended degree = 112 km on earth surface (*** you set the value as needed ***)
ax.tissot(rad_km=radius*112, lons=position[2], lats=position[1], n_samples=64, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
ax.coastlines(linewidth=0.15)
ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
With the code above, the resulting plot is:
Now, if we use Orthographic projection, (replace relevant line of code with this)
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Orthographic(central_longitude=0.0, central_latitude=60.0))
you get this plot:

How to set the markersize in kilometers?

I want to encircle a certain point. The radius of the circle needs to be 5 km, but how do I set my markersize so that the circle is 5 km on the map?
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
width, height = 400000, 320000
ax=plt.figure(figsize=(20,10))
lonA =[2.631547,2.861595,2.931014]
latA =[51.120983,51.209122,51.238868]
m= Basemap(width=width,height=height,projection='lcc',
resolution='h',lat_0=52.35,lon_0=4.5)
m.drawmapboundary(fill_color='turquoise')
m.fillcontinents(color='white',lake_color='aqua')
m.drawcountries(linestyle='--')
scatter2=m.scatter([], [], s=100, c='white', marker='o', label = 'Aurelia aurita', zorder=3, alpha=0.5, edgecolor='steelblue')
z,a = m(lonA[0:3], latA[0:3])
scatter2.set_offsets(np.c_[z,a])
plt.show()
To plot circles with a specified radius in map units (meters), firstly, I create a function (genCircle2) that accepts input parameters of a circle and returns an array of points along the perimeter of that circle. In my code below, command m(lon,lat) is used to compute (mx,my) in meters of map projection coordinates.
Command genCircle2(cx=mx, cy=my, rad=5000.) computes the points for circle plotting. Here is the working code.
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
def genCircle2(cx=0, cy=0, rad=1):
"""Generate points along perimeters of a circle"""
points = []
segs = 20
for ea in range(segs+1):
xi = cx + rad*np.cos(ea*2.*np.pi/segs)
yi = cy + rad*np.sin(ea*2.*np.pi/segs)
points.append([xi,yi])
return np.array(points)
width, height = 400000, 320000
ax = plt.figure(figsize=(12,10))
# long, lat
lonA = [2.631547, 2.861595, 2.931014]
latA = [51.120983, 51.209122, 51.238868]
# accompanying attributes, colors and ...
clrs = ['r', 'g', 'b']
m = Basemap(width=width, height=height, projection='lcc', \
resolution='i', lat_0=52.35, lon_0=4.5)
m.drawmapboundary(fill_color='turquoise')
m.fillcontinents(color='white', lake_color='aqua')
m.drawcountries(linestyle='--')
# plot circles at points defined by (lonA,latA)
for lon,lat,clr in zip(lonA, latA, clrs):
mx,my = m(lon,lat) # get map coordinates from (lon,lat)
cclpnts = genCircle2(cx=mx, cy=my, rad=5000.) # get points along circle's perimeter
m.plot(cclpnts[:,0], cclpnts[:,1], \
label='Aurelia aurita', color=clr, \
linewidth=0.75) # plot circle
plt.show()
The resulting plot:

How to draw a circle through two points (at diameter end)?

There are two points on the stereographic projection as shown in the figure:
These points are supposed to be on the end points of a dimeter of a circle. How to draw a circle passing through these two points?
Code for the above plot:
import matplotlib.pylab as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
from scipy.interpolate import splev, splrep
# create instance of basemap, note we want a south polar projection to 90 = E
myMap = Basemap(projection='spstere',boundinglat=0,lon_0=180,resolution='l',round=True,suppress_ticks=True)
# set the grid up
gridX, gridY = 10.0, 15.0
parallelGrid = np.arange(-90.0,90.0,gridX)
meridianGrid = np.arange(-180.0,180.0,gridY)
# draw parallel and meridian grid, not labels are off. We have to manually create these.
myMap.drawparallels(parallelGrid,labels=[False,False,False,False])
myMap.drawmeridians(meridianGrid,labels=[False,False,False,False],labelstyle='+/-',fmt='%i')
# plot azimuth labels, with a North label.
ax = plt.gca()
ax.text(0.5,1.025,'N',transform=ax.transAxes,horizontalalignment='center',verticalalignment='bottom',size=25)
for para in np.arange(gridY,360,gridY):
x= (1.1*0.5*np.sin(np.deg2rad(para)))+0.5
y= (1.1*0.5*np.cos(np.deg2rad(para)))+0.5
ax.text(x,y,u'%i\N{DEGREE SIGN}'%para,transform=ax.transAxes,horizontalalignment='center',verticalalignment='center')
summerAzi = np.array([0, 360])
summerAlt = np.array([40, 4])
summerX, summerY = myMap(summerAzi, -summerAlt)
summerX_new = np.linspace(summerX.min(), summerX.max(),30)
summerY_smooth = splev(summerX_new, splrep(summerX, summerY, k=1))
myMap.plot(summerX_new, summerY_smooth, 'g')
myMap.plot(summerX, summerY, 'go')
plt.show()
Inbuilt tissot() function is good enough to plot circles on a conformal projections (as in this case). On non-conformal projections, it plots ellipses.
Here the mid point of the tissot indicatrix is (0, -22) in degrees.
Its radius = (40-4)/2 = 18 in degrees.
Number of points = 36 is fine.
The relevant code is:
myMap.tissot(0, -22, 18, 36, \
facecolor='none', \
edgecolor='#ff0000', \
linewidth=1, \
alpha=1)
The circle in this polar representation will not look like a circle on a rectangular grid (i.e. "round"). Apart from that you can draw a circle just as you would on the cartesian plane, starting in polar coordinates, transforming to cartesian coordinates, offset the center and use the plot function.
summerAzi = np.array([0, 360])
summerAlt = -np.array([40, 4])
summerX, summerY = myMap(summerAzi, summerAlt)
phi = np.linspace(0,2.*np.pi)
r = np.abs(np.diff(summerAlt))/2.
x = r*np.cos(phi)
y = -r*np.sin(phi)+summerAlt.mean()
X,Y= myMap(x,y)
myMap.plot(X,Y, color="crimson")
myMap.plot(summerX, summerY, color="gold", marker="o")

Strange behavior with contours in Cartopy polar stereographic plot

I'm trying to create a contour plot on a North Polar Stereographic map projection using Cartopy. I used add_cyclic_point() to try and get around the problem of having a gap between longitude 0 and longitude 35X and followed an example from the documentation (always_circular_stereographic) to set up the map axes.
When I call plt.contour, I get the following plot. It looks like the contour plotter is getting confused at the transition from 355 to 0 longitude, and sends contour lines around the globe.
Here is my code:
import numpy as np
import cartopy.crs as ccrs
from cartopy.util import add_cyclic_point
import matplotlib.pyplot as plt
def define_map():
from matplotlib.path import Path
fig = plt.figure(figsize=(10,10))
ax = plt.axes(projection=ccrs.NorthPolarStereo())
ax.coastlines()
# From example: http://scitools.org.uk/cartopy/docs/latest/examples/always_circular_stereo.html
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = Path(verts * radius + center)
ax.set_boundary(circle, transform=ax.transAxes)
return(fig, ax)
lats = np.arange(65,91,5)
lons = add_cyclic_point(np.arange(0,359,5))
data = add_cyclic_point(np.random.random((len(lats),len(lons)-1)))
fig, ax = define_map()
plt.contour(lons,lats,data,5,transform=ccrs.PlateCarree(), cmap=plt.cm.Blues)
plt.colorbar(fraction=0.05, shrink=0.9)
plt.show()
How do I do a Cartopy contour plot properly?
Also, why do the contours only show up with transform=ccrs.PlateCarree() and not with transform=ccrs.NorthPolarStereo()?
Apparently the add_cyclic_point function is just for the data; the contour routine treats 0 different than 360. So the simple fix is to set
lons = np.arange(0,360,5)

Categories