Matplotlib streamplot arrows pointing the wrong way - python

I am generating a groundwater elevation contour and a streamplot in matplotlib
The contour indicates that the elevation is decreasing in many areas but the groundwater flow (streamplot) is pointed uphill. I have circled the arrows that seem to be pointed the wrong direction.
The arrows toward the bottom of the map appear to be pointed the correct direction. Does anyone know why this might be?
And here is most of the code which generates this plot:
#create empty arrays to fill up!
x_values = []
y_values = []
z_values = []
#iterate over wells and fill the arrays with well data
for well in well_arr:
x_values.append(well['xpos'])
y_values.append(well['ypos'])
z_values.append(well['value'])
#initialize numpy array as required for interpolation functions
x = np.array(x_values, dtype=np.float)
y = np.array(y_values, dtype=np.float)
z = np.array(z_values, dtype=np.float)
#create a list of x, y coordinate tuples
points = zip(x, y)
#create a grid on which to interpolate data
xi, yi = np.linspace(0, image['width'], image['width']),
np.linspace(0, image['height'], image['height'])
xi, yi = np.meshgrid(xi, yi)
#interpolate the data with the matlab griddata function
zi = griddata(x, y, z, xi, yi, interp='nn')
#create a matplotlib figure and adjust the width and heights
fig = plt.figure(figsize=(image['width']/72, image['height']/72))
#create a single subplot, just takes over the whole figure if only one is specified
ax = fig.add_subplot(111, frameon=False, xticks=[], yticks=[])
#create the contours
kwargs = {}
if groundwater_contours:
kwargs['colors'] = 'b'
CS = plt.contour(xi, yi, zi, linewidths=linewidth, **kwargs)
#add a streamplot
dx, dy = np.gradient(zi)
plt.streamplot(xi, yi, dx, dy, color='c', density=1, arrowsize=3)

Summary
I'm guessing, but your problem is probably because you have an inherent transpose going on. 2D numpy arrays are indexed as row, column. "x, y" indexing is column, row. In this context, numpy.gradient is basically going to return dy, dx and not dx, dy.
Try changing the line:
dx, dy = np.gradient(zi)
to:
dy, dx = np.gradient(zi)
Also, if your depths are defined as positive-up, it should be:
dy, dx = np.gradient(-zi)
However, I'm assuming you have positive-down depth conventions, so I'll leave that part of of the examples below. (So higher values are assumed to be deeper/lower in the example data below, and water will flow towards the high values.)
Reproducing the problem
For example, if we modify the code you gave to use random data and fill in a few variables that are coming from outside the scope of your code sample (so that it's a stand-alone example):
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.mlab import griddata
# Generate some reproducible but random data
np.random.seed(1981)
width, height = 200, 300
x, y, z = np.random.random((3,10))
x *= width
y *= height
#create a list of x, y coordinate tuples
points = zip(x, y)
#create a grid on which to interpolate data
xi, yi = np.linspace(0, width, width), np.linspace(0, height, height)
xi, yi = np.meshgrid(xi, yi)
#interpolate the data with the matlab griddata function
zi = griddata(x, y, z, xi, yi, interp='nn')
#create a matplotlib figure and adjust the width and heights
fig = plt.figure()
#create a single subplot, just takes over the whole figure if only one is specified
ax = fig.add_subplot(111, frameon=False, xticks=[], yticks=[])
#create the contours
CS = plt.contour(xi, yi, zi, linewidths=1, colors='b')
#add a streamplot
dx, dy = np.gradient(zi)
plt.streamplot(xi, yi, dx, dy, color='c', density=1, arrowsize=3)
plt.show()
The result will look like this:
Notice that there are lots of places where the flow lines are not perpendicular to the contours. That's an even easier indicator than the incorrect direction of the arrows that something is going wrong. (Though "perpendicular" assumes an aspect ratio of 1 for the plot, which isn't quite true for these plots unless you set it.)
Fixing the problem
If we just change the line
dx, dy = np.gradient(zi)
to:
dy, dx = np.gradient(zi)
We'll get the correct result:
Interpolation suggestions
On a side note, griddata is a poor choice in this case.
First, it's not a "smooth" interpolation method. It uses delaunay triangulation, which makes "sharp" ridges at triangle boundaries. This leads to anomalous gradients in those locations.
Second, it limits interpolation to the convex hull of your data points, which may or may not be a good choice.
A radial basis function (or any other smooth interpolant) is a much better choice for interpolation.
As an example, if we modify your code snippet to use an RBF:
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import Rbf
# Generate data
np.random.seed(1981)
width, height = 200, 300
x, y, z = np.random.random((3,10))
x *= width
y *= height
#create a grid on which to interpolate data
xi, yi = np.mgrid[0:width:1j*width, 0:height:1j*height]
#interpolate the data with the matlab griddata function
interp = Rbf(x, y, z, function='linear')
zi = interp(xi, yi)
#create a matplotlib figure and adjust the width and heights
fig, ax = plt.subplots(subplot_kw=dict(frameon=False, xticks=[], yticks=[]))
#create the contours and streamplot
CS = plt.contour(xi, yi, zi, linewidths=1, colors='b')
dy, dx = np.gradient(zi.T)
plt.streamplot(xi[:,0], yi[0,:], dx, dy, color='c', density=1, arrowsize=3)
plt.show()
(You'll notice the intersections are not quite perpendicular due to the non-equal aspect ratio of the plot. They're all 90 degrees if we set the aspect ratio of the plot to 1, however.)
As a side-by-side comparison of the two methods:

You can specify the arrow style with arrowstyle='->'. Try both of these and see if this works for you:
plt.streamplot(xi, yi, dx, dy, color='c', density=1, arrowsize=3,
arrowstyle='<-')
plt.streamplot(xi, yi, dx, dy, color='c', density=1, arrowsize=3,
arrowstyle='->')

Related

Improve/smooth 3D-plot of DEM(Digital elevation model) terrain surface from GeoTIFF using python and matplotlib

the first results of my DEM plotting with matplotlib are working, but are RAM consuming and do not look very pretty.
Since Im just a beginner I dont know how to improve the result further, so it will look more like a terrain surface (in terms of elevations).
What I did so far:
gathering DEM geotiff data. Plotting it in 2d is simple and the result will look like that:
secondly i used that geotiff to squeez it into some plotting code samples i gathered online (especially from here: https://jackmckew.dev/3d-terrain-in-python.html )
As seen, the 3d-plot is on square basis and the surface is very "spiky". Id like to keep it in real proportions (geo-projections) and have it less spiky.
I already tried to lower the Z-ratio, but that does not change the end-result. The spiked are still there. I got no problems with smoothing the data and lose/change the data a bit.
The code so far:
from osgeo import gdal
import matplotlib.pyplot as plt
import numpy as np
source_file_dem = 'path_to_the_tif_file'
dem = gdal.Open(source_file_dem)
gt = dem.GetGeoTransform()
dem_array = dem.ReadAsArray()
lin_x = np.linspace(0,1,dem_array.shape[0],endpoint=False)
lin_y = np.linspace(0,1,dem_array.shape[1],endpoint=False)
y,x = np.meshgrid(lin_y,lin_x)
z = dem_array
# Creating figure
fig = plt.figure(figsize=(10,7))
ax = plt.axes(projection='3d')
surf = ax.plot_surface(x,y,z,cmap='terrain', edgecolor='none')
fig.colorbar(surf, ax=ax, shrink=0.5, aspect=5)
ax.set_title('Surface plot')
plt.xticks([]) # disabling xticks by Setting xticks to an empty list
plt.yticks([]) # disabling yticks by setting yticks to an empty list
# show plot
plt.show()
TIF files can be obtained here:
https://srtm.csi.cgiar.org/download
or you can just download a randomly picked tile in the US:
https://srtm.csi.cgiar.org/wp-content/uploads/files/srtm_5x5/TIFF/srtm_15_06.zip
For all who want to try that out:
the python gdal wheels can be found here:
https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal
Does anyone know where to look or could help out to reach a better looking result?
Thanks
UPDATE:
after some further research and hints from simon I got some results.
The current state result looks like that:
The code for the plot + an addition class for drawing arrows in 3D. Feel free to use it or even improve it. Id be glad to hear about improvements:
Plot:
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from osgeo import gdal
import matplotlib.pyplot as plt
import scipy as sp
import scipy.ndimage
from PIL import Image
from arrows3dplot import * # python_file in project with class
import matplotlib.cm as cm
source_file_dem = 'path_to_the_tif_file'
# Set max number of pixel to: 'None' to prevent errors. Its not nice, but works for that case. Big images will load RAM+CPU heavily (like DecompressionBomb)
Image.MAX_IMAGE_PIXELS = None # first we set no limit to open
img = Image.open(source_file_dem)
# get aspect ratio of tif file for late plot box-plot-ratio
y_ratio,x_ratio = img.size
# open georeference TIF file
dem = gdal.Open(source_file_dem)
gt = dem.GetGeoTransform()
dem_array = dem.ReadAsArray()
# create arrays and declare x,y,z variables
lin_x = np.linspace(0,1,dem_array.shape[0],endpoint=False)
lin_y = np.linspace(0,1,dem_array.shape[1],endpoint=False)
y,x = np.meshgrid(lin_y,lin_x)
z = dem_array
# Apply gaussian filter, with sigmas as variables. Higher sigma = more smoothing and more calculations. Downside: min and max values do change due to smoothing
sigma_y =100
sigma_x = 100
sigma = [sigma_y, sigma_x]
z_smoothed = sp.ndimage.gaussian_filter(z, sigma)
# Some min and max and range values coming from gaussian_filter calculations
z_smoothed_min = np.amin(z_smoothed)
z_smoothed_max = np.amax(z_smoothed)
z_range = z_smoothed_max - z_smoothed_min
# Creating figure
fig = plt.figure(figsize=(12,10))
ax = plt.axes(projection='3d')
ax.azim = -30
ax.elev = 42
ax.set_box_aspect((x_ratio,y_ratio,((x_ratio+y_ratio)/8)))
ax.arrow3D(1,1,z_smoothed_max, -1,0,1, mutation_scale=20, ec ='black', fc='red') #draw arrow to "north" which is not correct north. But with georeferenced sources it should work
surf = ax.plot_surface(x,y,z_smoothed, cmap='terrain', edgecolor='none')
# setting colors for colorbar range
m = cm.ScalarMappable(cmap=surf.cmap, norm=surf.norm)
m.set_array(z_smoothed)
cbar = fig.colorbar(m, shrink=0.5, aspect=20, ticks=[z_smoothed_min, 0, (z_range*0.25+z_smoothed_min), (z_range*0.5+z_smoothed_min), (z_range*0.75+z_smoothed_min), z_smoothed_max])
cbar.ax.set_yticklabels([f'{z_smoothed_min}', ' ', f'{(z_range*0.25+z_smoothed_min)}', f'{(z_range*0.5+z_smoothed_min)}', f'{(z_range*0.75+z_smoothed_min)}', f'{z_smoothed_max}'])
plt.xticks([]) # disabling xticks by Setting xticks to an empty list
plt.yticks([]) # disabling yticks by setting yticks to an empty list
# draw flat rectangle at z = 0 to indicate where mean sea level is in 3d
x_rectangle = [0,1,1,0]
y_rectangle = [0,0,1,1]
z_rectangle = [0,0,0,0]
verts = [list(zip(x_rectangle,y_rectangle,z_rectangle))]
ax.add_collection3d(Poly3DCollection(verts, alpha=0.5))
fig.tight_layout()
plt.show()
Class 3D Arrow (taken from here: https://gist.github.com/WetHat/1d6cd0f7309535311a539b42cccca89c )
import numpy as np
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.proj3d import proj_transform
class Arrow3D(FancyArrowPatch):
def __init__(self, x, y, z, dx, dy, dz, *args, **kwargs):
super().__init__((0, 0), (0, 0), *args, **kwargs)
self._xyz = (x, y, z)
self._dxdydz = (dx, dy, dz)
def draw(self, renderer):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
super().draw(renderer)
def do_3d_projection(self, renderer=None):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
def _arrow3D(ax, x, y, z, dx, dy, dz, *args, **kwargs):
'''Add an 3d arrow to an `Axes3D` instance.'''
arrow = Arrow3D(x, y, z, dx, dy, dz, *args, **kwargs)
ax.add_artist(arrow)
setattr(Axes3D, 'arrow3D', _arrow3D)
If you make a 2D numpy array from the data, you can apply a convolution to it. Look into OpenCV; it has functions for blurring and such.

How does the Matplotlib trisurf plot work?

I am unable to understand from the matplotlib documentation(https://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html), the working of a trisurf plot. Can someone please explain how the X,Y and Z arguments result in a 3-D plot?
Let me talk you through this example taken from the docs
'''
======================
Triangular 3D surfaces
======================
Plot a 3D surface with a triangular mesh.
'''
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
n_radii = 8
n_angles = 36
# Make radii and angles spaces (radius r=0 omitted to eliminate duplication).
radii = np.linspace(0.125, 1.0, n_radii)
angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)
# Repeat all angles for each radius.
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
# Convert polar (radii, angles) coords to cartesian (x, y) coords.
# (0, 0) is manually added at this stage, so there will be no duplicate
# points in the (x, y) plane.
x = np.append(0, (radii*np.cos(angles)).flatten())
y = np.append(0, (radii*np.sin(angles)).flatten())
# Compute z to make the pringle surface.
z = np.sin(-x*y)
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_trisurf(x, y, z, linewidth=0.2, antialiased=True)
plt.show()
The x, y values are a range of values over which we calculate the surface. For each (x, y) pair of coordinates, we have a single value of z, which represents the height of the surface at that point.

Ellipsoid creation in Python

I have ran into a problem relating to the drawing of the Ellipsoid.
The ellipsoid that I am drawing to draw is the following:
x**2/16 + y**2/16 + z**2/16 = 1.
So I saw a lot of references relating to calculating and plotting of an Ellipse void and in multiple questions a cartesian to spherical or vice versa calculation was mentioned.
Ran into a website that had a calculator for it, but I had no idea on how to successfully perform this calculation. Also I am not sure as to what the linspaces should be set to. Have seen the ones that I have there as defaults, but as I got no previous experience with these libraries, I really don't know what to expect from it.
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=plt.figaspect(1)) # Square figure
ax = fig.add_subplot(111, projection='3d')
multip = (1, 1, 1)
# Radii corresponding to the coefficients:
rx, ry, rz = 1/np.sqrt(multip)
# Spherical Angles
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
# Cartesian coordinates
#Lots of uncertainty.
#x =
#y =
#z =
# Plot:
ax.plot_surface(x, y, z, rstride=4, cstride=4, color='b')
# Axis modifications
max_radius = max(rx, ry, rz)
for axis in 'xyz':
getattr(ax, 'set_{}lim'.format(axis))((-max_radius, max_radius))
plt.show()
Your ellipsoid is not just an ellipsoid, it's a sphere.
Notice that if you use the substitution formulas written below for x, y and z, you'll get an identity. It is in general easier to plot such a surface of revolution in a different coordinate system (spherical in this case), rather than attempting to solve an implicit equation (which in most plotting programs ends up jagged, unless you take some countermeasures).
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
phi = np.linspace(0,2*np.pi, 256).reshape(256, 1) # the angle of the projection in the xy-plane
theta = np.linspace(0, np.pi, 256).reshape(-1, 256) # the angle from the polar axis, ie the polar angle
radius = 4
# Transformation formulae for a spherical coordinate system.
x = radius*np.sin(theta)*np.cos(phi)
y = radius*np.sin(theta)*np.sin(phi)
z = radius*np.cos(theta)
fig = plt.figure(figsize=plt.figaspect(1)) # Square figure
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(x, y, z, color='b')

Two-dimensional interpolation/smoothing of unevenly sampled angular values

I have some data that consist of unevenly sampled 2D spatial locations, where each x, y coordinate has an associated phase value theta between 0 and 2pi. I'd like to be able to interpolate the theta values onto a regular x, y grid. The data is degenerate in the sense that the same (or very nearby) x, y locations may be associated with multiple phase values, and vice versa for values of theta, so this is strictly speaking a smoothing problem rather than straight interpolation.
I've briefly experimented with scipy's radial basis functions, but these give nasty edge effects because of the discontinuity in the theta values from 2pi --> 0.
Here's a toy example (the real spatial distribution of phases is a lot messier):
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import colorbar
from matplotlib.colors import Normalize
from scipy import interpolate
# randomly sampled spatial locations
x, y = np.random.uniform(-1, 1, size=(2, 1000))
# theta varies smoothly with location apart from the singularity at 0, 0
z = np.arctan2(x, y) % (2 * np.pi)
# smooth with a simple linear RBF
rbf = interpolate.Rbf(x, y, z, function='linear', smooth=0.1)
# resample on a finer grid
xi, yi = np.mgrid[-1:1:100j, -1:1:100j].reshape(2, -1)
zi = rbf(xi, yi) % (2 * np.pi)
# plotting
fig, ax = plt.subplots(1, 1, subplot_kw={'aspect': 'equal'})
ax.hold(True)
norm = Normalize(0, 2 * np.pi)
im = ax.imshow(zi.reshape(100, 100).T, extent=(-1, 1, -1, 1),
origin='lower', cmap=plt.cm.hsv, norm=norm)
sc = ax.scatter(x, y, s=30, c=z, cmap=im.cmap, norm=norm)
cax, kw = colorbar.make_axes_gridspec(ax)
cb = plt.colorbar(im, cax=cax, **kw)
ax.set_xlabel(r'$X_0$', fontsize='x-large')
ax.set_ylabel(r'$Y_0$', fontsize='x-large')
cb.set_ticks(np.arange(0, 2.1*np.pi, np.pi/2.))
cb.set_ticklabels([r'$0$', r'$\frac{\pi}{2}$', r'$\pi$',
r'$\frac{3\pi}{2}$', r'$2\pi$'])
cb.set_label(r'$\theta$', fontsize='x-large')
cb.ax.tick_params(labelsize='x-large')
plt.show()
What would be a good way to go about interpolating angular quantities like this? Does scipy have any built in interpolation method that will deal with angles nicely, or will I have to write my own?
I feel pretty stupid now!
The answer was very simple - this answer on MathOverflow clued me in. There is no problem with discontinuity provided that I convert from a polar coordinate space to a Cartesian one, then interpolate the x and y components of the vector independently:
x, y = np.random.uniform(-1, 1, size=(2, 1000))
z = np.arctan2(y, x) % (2*np.pi)
# convert from polar --> cartesian
u, v = np.cos(z), np.sin(z)
# interpolate x and y components separately
rbf_u = interpolate.Rbf(x, y, u, function='linear', smooth=0.1)
rbf_v = interpolate.Rbf(x, y, v, function='linear', smooth=0.1)
xi, yi = np.mgrid[-1:1:100j, -1:1:100j].reshape(2, -1)
ui = rbf_u(xi, yi)
vi = rbf_v(xi, yi)
# convert from cartesian --> polar
zi = np.arctan2(ui, vi) % (2*np.pi)
It would be nice performance-wise if there was a way to avoid performing two separate interpolations on the x and y components, but I don't really see a way around this.
Expanding on the accepted answer, this can be done in a single pass by using complex numbers to store the coordinates:
x, y = np.random.uniform(-1, 1, size=(2, 1000))
z = np.arctan2(y, x) % (2*np.pi)
# convert to cartesian coordinates on the complex plane
u = np.sin(z) + np.cos(z) * 1j
# interpolate x and y components separately
rbf_u = sp.interpolate.Rbf(x, y, u, function='linear', smooth=0.1)
xi, yi = np.mgrid[-1:1:100j, -1:1:100j].reshape(2, -1)
ui = rbf_u(xi, yi)
# convert from cartesian --> polar
zi = np.angle(ui) % (2*np.pi)

Contour plot with python

This is my code. I tried using it to plot the contour, however, I get two plots on the same grid. exactly half instead of a full plot. I mean my grid is 20 units in x direction however, my resulting plot is 10 units in x direction with replica plot from 10 to 20 units. Can you please look whats wrong?
v is a numpy array of 20x10
x, y = range(0, 20), range(0,10)
xi, yi = np.meshgrid(x, y)
# Interpolate
rbf = scipy.interpolate.Rbf(xi, yi, v)
zi = rbf(xi, yi)
plt.imshow(zi, vmin = 1.6, vmax=2)
plt.colorbar()
plt.show()

Categories