2 Streamline plotting for 2D vector fields. 5 from __future__
import (absolute_import, division, print_function,
8 from matplotlib.externals
import six
9 from matplotlib.externals.six.moves
import xrange
13 import matplotlib.cm
as cm
14 import matplotlib.colors
as mcolors
15 import matplotlib.collections
as mcollections
16 import matplotlib.patches
as patches
19 __all__ = [
'streamplot']
22 def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None,
23 cmap=None, norm=None, arrowsize=1, arrowstyle='-|>',
24 minlength=0.1, maxlength=4.0, transform=None, zorder=2,
25 start_points=None, integration_direction='both'):
26 """Draws streamlines of a vector flow. 29 an *evenly spaced* grid. 31 x and y-velocities. Number of rows should match length of y, and 32 the number of columns should match x. 33 *density* : float or 2-tuple 34 Controls the closeness of streamlines. When `density = 1`, the domain 35 is divided into a 30x30 grid---*density* linearly scales this grid. 36 Each cell in the grid can have, at most, one traversing streamline. 37 For different densities in each direction, use [density_x, density_y]. 38 *linewidth* : numeric or 2d array 39 vary linewidth when given a 2d array with the same shape as velocities. 40 *color* : matplotlib color code, or 2d array 41 Streamline color. When given an array with the same shape as 42 velocities, *color* values are converted to colors using *cmap*. 43 *cmap* : :class:`~matplotlib.colors.Colormap` 44 Colormap used to plot streamlines and arrows. Only necessary when using 45 an array input for *color*. 46 *norm* : :class:`~matplotlib.colors.Normalize` 47 Normalize object used to scale luminance data to 0, 1. If None, stretch 48 (min, max) to (0, 1). Only necessary when *color* is an array. 50 Factor scale arrow size. 52 Arrow style specification. 53 See :class:`~matplotlib.patches.FancyArrowPatch`. 55 Minimum length of streamline in axes coordinates. 57 Maximum length of streamline in axes coordinates. 58 *start_points*: Nx2 array 59 Coordinates of starting points for the streamlines. 60 In data coordinates, the same as the ``x`` and ``y`` arrays. 61 *integration_direction* : ['foward','backward','both'] 62 Integrate the streamline in forward, backward or both directions. 68 *stream_container* : StreamplotSet 69 Container object with attributes 71 - lines: `matplotlib.collections.LineCollection` of streamlines 73 - arrows: collection of `matplotlib.patches.FancyArrowPatch` 74 objects representing arrows half-way along stream 77 This container will probably change in the future to allow changes 78 to the colormap, alpha, etc. for both lines and arrows, but these 79 changes should be backward compatible. 88 transform = axes.transData
90 if color
is None and 'color' in axes._get_lines._prop_keys:
91 color = six.next(axes._get_lines.prop_cycler)[
'color']
94 linewidth = matplotlib.rcParams[
'lines.linewidth']
97 arrow_kw =
dict(arrowstyle=arrowstyle, mutation_scale=10 * arrowsize)
99 if integration_direction
not in [
'both',
'forward',
'backward']:
100 errstr =
"Integration direction " \
101 "'%s' not recognised." % integration_direction
102 errstr +=
"Expected 'both', 'forward' or 'backward'." 103 raise ValueError(errstr)
105 if integration_direction ==
'both':
108 use_multicolor_lines = isinstance(color, np.ndarray)
109 if use_multicolor_lines:
110 if color.shape != grid.shape:
111 msg =
"If 'color' is given, must have the shape of 'Grid(x,y)'" 112 raise ValueError(msg)
114 color = np.ma.masked_invalid(color)
116 line_kw[
'color'] = color
117 arrow_kw[
'color'] = color
119 if isinstance(linewidth, np.ndarray):
120 if linewidth.shape != grid.shape:
121 msg =
"If 'linewidth' is given, must have the shape of 'Grid(x,y)'" 122 raise ValueError(msg)
123 line_kw[
'linewidth'] = []
125 line_kw[
'linewidth'] = linewidth
126 arrow_kw[
'linewidth'] = linewidth
128 line_kw[
'zorder'] = zorder
129 arrow_kw[
'zorder'] = zorder
132 if (u.shape != grid.shape)
or (v.shape != grid.shape):
133 msg =
"'u' and 'v' must be of shape 'Grid(x,y)'"
raise ValueError(msg)
135 u = np.ma.masked_invalid(u)
136 v = np.ma.masked_invalid(v)
139 integration_direction)
142 if start_points
is None:
144 if mask[ym, xm] == 0:
145 xg, yg = dmap.mask2grid(xm, ym)
148 trajectories.append(t)
153 sp2 = np.asanyarray(start_points, dtype=np.float).copy()
154 sp2[:, 0] += np.abs(x[0])
155 sp2[:, 1] += np.abs(y[0])
157 xg, yg = dmap.data2grid(xs, ys)
160 trajectories.append(t)
162 if use_multicolor_lines:
164 norm = mcolors.Normalize(color.min(), color.max())
166 cmap = cm.get_cmap(matplotlib.rcParams[
'image.cmap'])
168 cmap = cm.get_cmap(cmap)
172 for t
in trajectories:
176 tx = np.array(t[0]) * grid.dx + grid.x_origin
177 ty = np.array(t[1]) * grid.dy + grid.y_origin
179 points = np.transpose([tx, ty]).reshape(-1, 1, 2)
180 streamlines.extend(np.hstack([points[:-1], points[1:]]))
183 s = np.cumsum(np.sqrt(np.diff(tx) ** 2 + np.diff(ty) ** 2))
184 n = np.searchsorted(s, s[-1] / 2.)
185 arrow_tail = (tx[n], ty[n])
186 arrow_head = (np.mean(tx[n:n + 2]), np.mean(ty[n:n + 2]))
188 if isinstance(linewidth, np.ndarray):
189 line_widths =
interpgrid(linewidth, tgx, tgy)[:-1]
190 line_kw[
'linewidth'].extend(line_widths)
191 arrow_kw[
'linewidth'] = line_widths[n]
193 if use_multicolor_lines:
194 color_values =
interpgrid(color, tgx, tgy)[:-1]
195 line_colors.append(color_values)
196 arrow_kw[
'color'] = cmap(norm(color_values[n]))
198 p = patches.FancyArrowPatch(arrow_tail,
206 lc = mcollections.LineCollection(streamlines,
210 if use_multicolor_lines:
211 lc.set_array(np.ma.hstack(line_colors))
214 axes.add_collection(lc)
215 axes.autoscale_view()
217 ac = matplotlib.collections.PatchCollection(arrows)
219 return stream_container
224 def __init__(self, lines, arrows, **kwargs):
233 """Map representing different coordinate systems. 235 Coordinate definitions: 237 * axes-coordinates goes from 0 to 1 in the domain. 238 * data-coordinates are specified by the input x-y coordinates. 239 * grid-coordinates goes from 0 to N and 0 to M for an N x M grid, 240 where N and M match the shape of the input data. 241 * mask-coordinates goes from 0 to N and 0 to M for an N x M mask, 242 where N and M are user-specified to control the density of streamlines. 244 This class also has methods for adding trajectories to the StreamMask. 245 Before adding a trajectory, run `start_trajectory` to keep track of regions 246 crossed by a given trajectory. Later, if you decide the trajectory is bad 247 (e.g., if the trajectory is very short) just call `undo_trajectory`. 264 """Return nearest space in mask-coords from given grid-coords.""" 276 self.
mask._start_trajectory(xm, ym)
280 self.
mask._current_xy = (xm, ym)
283 if not self.
grid.within_grid(xg, yg):
284 raise InvalidIndexError
286 self.
mask._update_trajectory(xm, ym)
300 if not np.allclose(x_row, x):
301 raise ValueError(
"The rows of 'x' must be equal")
304 raise ValueError(
"'x' can have at maximum 2 dimensions")
310 if not np.allclose(y_col, y.T):
311 raise ValueError(
"The columns of 'y' must be equal")
314 raise ValueError(
"'y' can have at maximum 2 dimensions")
319 self.
dx = x[1] - x[0]
320 self.
dy = y[1] - y[0]
325 self.
width = x[-1] - x[0]
333 """Return True if point is a valid index of grid.""" 336 return xi >= 0
and xi <= self.
nx - 1
and yi >= 0
and yi <= self.
ny - 1
340 """Mask to keep track of discrete regions crossed by streamlines. 342 The resolution of this grid determines the approximate spacing between 343 trajectories. Streamlines are only allowed to pass through zeroed cells: 344 When a streamline enters a cell, that cell is set to 1, and no new 345 streamlines are allowed to enter. 349 if np.isscalar(density):
351 raise ValueError(
"If a scalar, 'density' must be positive")
352 self.
nx = self.
ny = int(30 * density)
354 if len(density) != 2:
355 raise ValueError(
"'density' can have at maximum 2 dimensions")
356 self.
nx = int(30 * density[0])
357 self.
ny = int(30 * density[1])
358 self.
_mask = np.zeros((self.
ny, self.
nx))
367 """Start recording streamline trajectory""" 372 """Remove current trajectory from mask""" 374 self.
_mask.__setitem__(t, 0)
377 """Update current trajectory position in mask. 379 If the new position has already been filled, raise `InvalidIndexError`. 382 if self[ym, xm] == 0:
384 self.
_mask[ym, xm] = 1
387 raise InvalidIndexError
401 def get_integrator(u, v, dmap, minlength, maxlength, integration_direction):
404 u, v = dmap.data2grid(u, v)
407 u_ax = u / dmap.grid.nx
408 v_ax = v / dmap.grid.ny
409 speed = np.ma.sqrt(u_ax ** 2 + v_ax ** 2)
411 def forward_time(xi, yi):
418 return ui * dt_ds, vi * dt_ds
420 def backward_time(xi, yi):
421 dxi, dyi = forward_time(xi, yi)
425 """Return x, y grid-coordinates of trajectory based on starting point. 427 Integrate both forward and backward in time from starting point in 430 Integration is terminated when a trajectory reaches a domain boundary 431 or when it crosses into an already occupied cell in the StreamMask. The 432 resulting trajectory is None if it is shorter than `minlength`. 435 stotal, x_traj, y_traj = 0., [], []
437 dmap.start_trajectory(x0, y0)
438 if integration_direction
in [
'both',
'backward']:
444 if integration_direction
in [
'both',
'forward']:
445 dmap.reset_start_point(x0, y0)
454 if stotal > minlength:
455 return x_traj, y_traj
457 dmap.undo_trajectory()
464 """2nd-order Runge-Kutta algorithm with adaptive step size. 466 This method is also referred to as the improved Euler's method, or Heun's 467 method. This method is favored over higher-order methods because: 469 1. To get decent looking trajectories and to sample every mask cell 470 on the trajectory we need a small timestep, so a lower order 471 solver doesn't hurt us unless the data is *very* high resolution. 472 In fact, for cases where the user inputs 473 data smaller or of similar grid size to the mask grid, the higher 474 order corrections are negligible because of the very fast linear 475 interpolation used in `interpgrid`. 477 2. For high resolution input data (i.e. beyond the mask 478 resolution), we must reduce the timestep. Therefore, an adaptive 479 timestep is more suited to the problem as this would be very hard 480 to judge automatically otherwise. 482 This integrator is about 1.5 - 2x as fast as both the RK4 and RK45 483 solvers in most setups on my machine. I would recommend removing the 484 other two to keep things simple. 497 maxds =
min(1. / dmap.mask.nx, 1. / dmap.mask.ny, 0.1)
507 while dmap.grid.within_grid(xi, yi)
and iterations < 10000:
512 k2x, k2y =
f(xi + ds * k1x,
517 ds, xf_traj, yf_traj =
_euler_step(xf_traj, yf_traj, dmap, f)
520 except TerminateTrajectory:
525 dx2 = ds * 0.5 * (k1x + k2x)
526 dy2 = ds * 0.5 * (k1y + k2y)
528 nx, ny = dmap.grid.shape
530 error = np.sqrt(((dx2 - dx1) / nx) ** 2 + ((dy2 - dy1) / ny) ** 2)
537 dmap.update_trajectory(xi, yi)
538 except InvalidIndexError:
540 if (stotal + ds) > maxlength:
548 ds =
min(maxds, 0.85 * ds * (maxerror / error) ** 0.5)
552 return stotal, xf_traj, yf_traj
556 """Simple Euler integration step that extends streamline to boundary.""" 557 ny, nx = dmap.grid.shape
566 dsx = (nx - 1 - xi) / cx
572 dsy = (ny - 1 - yi) / cy
574 xf_traj.append(xi + cx * ds)
575 yf_traj.append(yi + cy * ds)
576 return ds, xf_traj, yf_traj
583 """Fast 2D, linear interpolation on an integer grid""" 586 if isinstance(xi, np.ndarray):
587 x = xi.astype(np.int)
588 y = yi.astype(np.int)
590 xn = np.clip(x + 1, 0, Nx - 1)
591 yn = np.clip(y + 1, 0, Ny - 1)
611 a0 = a00 * (1 - xt) + a01 * xt
612 a1 = a10 * (1 - xt) + a11 * xt
613 ai = a0 * (1 - yt) + a1 * yt
615 if not isinstance(xi, np.ndarray):
616 if np.ma.is_masked(ai):
617 raise TerminateTrajectory
623 """Yield starting points for streamlines. 625 Trying points on the boundary first gives higher quality streamlines. 626 This algorithm starts with a point on the mask corner and spirals inward. 627 This algorithm is inefficient, but fast compared to rest of streamplot. 637 for i
in xrange(nx * ny):
641 if direction ==
'right':
646 elif direction ==
'up':
651 elif direction ==
'left':
656 elif direction ==
'down':
661 def __getitem__(self, args)
def start_trajectory(self, xg, yg)
def reset_start_point(self, xg, yg)
type(dict_typ) function, pointer, public dict(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16, n17, n18, n19, n20)
Construct a new dictionary from several key/value pairs. Together with the Assign subroutine and over...
def _gen_starting_points(shape)
def grid2mask(self, xi, yi)
def _euler_step(xf_traj, yf_traj, dmap, f)
def mask2grid(self, xm, ym)
subroutine append(buffer, i, d)
def _integrate_rk12(x0, y0, dmap, f, maxlength)
def _start_trajectory(self, xm, ym)
def __init__(self, density)
def within_grid(self, xi, yi)
def update_trajectory(self, xg, yg)
real function, public integrate(fkt, xl, xr, eps, plist, method)
Numerical integration function.
def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, maxlength=4.0, transform=None, zorder=2, start_points=None, integration_direction='both')
def _update_trajectory(self, xm, ym)
def __init__(self, lines, arrows, kwargs)
def data2grid(self, xd, yd)
def __init__(self, grid, mask)
def _undo_trajectory(self)
def undo_trajectory(self)
def get_integrator(u, v, dmap, minlength, maxlength, integration_direction)
def interpgrid(a, xi, yi)