2Streamline plotting for 2D vector fields.
5from __future__
import (absolute_import, division, print_function,
8from matplotlib.externals
import six
9from matplotlib.externals.six.moves
import xrange
13import matplotlib.cm
as cm
14import matplotlib.colors
as mcolors
15import matplotlib.collections
as mcollections
16import matplotlib.patches
as patches
19__all__ = [
'streamplot']
22def 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)'"
134 raise ValueError(msg)
136 u = np.ma.masked_invalid(u)
137 v = np.ma.masked_invalid(v)
140 integration_direction)
143 if start_points
is None:
145 if mask[ym, xm] == 0:
146 xg, yg = dmap.mask2grid(xm, ym)
149 trajectories.append(t)
154 sp2 = np.asanyarray(start_points, dtype=np.float).copy()
155 sp2[:, 0] += np.abs(x[0])
156 sp2[:, 1] += np.abs(y[0])
158 xg, yg = dmap.data2grid(xs, ys)
161 trajectories.append(t)
163 if use_multicolor_lines:
165 norm = mcolors.Normalize(color.min(), color.max())
167 cmap = cm.get_cmap(matplotlib.rcParams[
'image.cmap'])
169 cmap = cm.get_cmap(cmap)
173 for t
in trajectories:
177 tx = np.array(t[0]) * grid.dx + grid.x_origin
178 ty = np.array(t[1]) * grid.dy + grid.y_origin
180 points = np.transpose([tx, ty]).reshape(-1, 1, 2)
181 streamlines.extend(np.hstack([points[:-1], points[1:]]))
184 s = np.cumsum(np.sqrt(np.diff(tx) ** 2 + np.diff(ty) ** 2))
185 n = np.searchsorted(s, s[-1] / 2.)
186 arrow_tail = (tx[n], ty[n])
187 arrow_head = (np.mean(tx[n:n + 2]), np.mean(ty[n:n + 2]))
189 if isinstance(linewidth, np.ndarray):
190 line_widths =
interpgrid(linewidth, tgx, tgy)[:-1]
191 line_kw[
'linewidth'].extend(line_widths)
192 arrow_kw[
'linewidth'] = line_widths[n]
194 if use_multicolor_lines:
195 color_values =
interpgrid(color, tgx, tgy)[:-1]
196 line_colors.append(color_values)
197 arrow_kw[
'color'] = cmap(norm(color_values[n]))
199 p = patches.FancyArrowPatch(arrow_tail,
207 lc = mcollections.LineCollection(streamlines,
211 if use_multicolor_lines:
212 lc.set_array(np.ma.hstack(line_colors))
215 axes.add_collection(lc)
216 axes.autoscale_view()
218 ac = matplotlib.collections.PatchCollection(arrows)
220 return stream_container
234 """Map representing different coordinate systems.
236 Coordinate definitions:
238 * axes-coordinates goes from 0 to 1
in the domain.
239 * data-coordinates are specified by the input x-y coordinates.
240 * grid-coordinates goes
from 0 to N
and 0 to M
for an N x M grid,
241 where N
and M match the shape of the input data.
242 * mask-coordinates goes
from 0 to N
and 0 to M
for an N x M mask,
243 where N
and M are user-specified to control the density of streamlines.
245 This
class also has methods for adding trajectories to
the StreamMask.
246 Before adding a trajectory, run `start_trajectory` to keep track of regions
247 crossed by a given trajectory. Later,
if you decide the trajectory
is bad
248 (e.g.,
if the trajectory
is very short) just call `undo_trajectory`.
265 """Return nearest space in mask-coords from given grid-coords."""
277 self.
mask._start_trajectory(xm, ym)
281 self.
mask._current_xy = (xm, ym)
284 if not self.
grid.within_grid(xg, yg):
285 raise InvalidIndexError
287 self.
mask._update_trajectory(xm, ym)
290 self.
mask._undo_trajectory()
301 if not np.allclose(x_row, x):
302 raise ValueError(
"The rows of 'x' must be equal")
305 raise ValueError(
"'x' can have at maximum 2 dimensions")
311 if not np.allclose(y_col, y.T):
312 raise ValueError(
"The columns of 'y' must be equal")
315 raise ValueError(
"'y' can have at maximum 2 dimensions")
320 self.
dx = x[1] - x[0]
321 self.
dy = y[1] - y[0]
331 return self.
ny, self.
nx
334 """Return True if point is a valid index of grid."""
337 return xi >= 0
and xi <= self.
nx - 1
and yi >= 0
and yi <= self.
ny - 1
341 """Mask to keep track of discrete regions crossed by streamlines.
343 The resolution of this grid determines the approximate spacing between
344 trajectories. Streamlines are only allowed to pass through zeroed cells:
345 When a streamline enters a cell, that cell
is set to 1,
and no new
346 streamlines are allowed to enter.
350 if np.isscalar(density):
352 raise ValueError(
"If a scalar, 'density' must be positive")
353 self.
nx = self.
ny = int(30 * density)
355 if len(density) != 2:
356 raise ValueError(
"'density' can have at maximum 2 dimensions")
357 self.
nx = int(30 * density[0])
358 self.
ny = int(30 * density[1])
368 """Start recording streamline trajectory"""
373 """Remove current trajectory from mask"""
375 self.
_mask.__setitem__(t, 0)
378 """Update current trajectory position in mask.
380 If the new position has already been filled, raise `InvalidIndexError`.
383 if self[ym, xm] == 0:
385 self.
_mask[ym, xm] = 1
388 raise InvalidIndexError
405 u, v = dmap.data2grid(u, v)
408 u_ax = u / dmap.grid.nx
409 v_ax = v / dmap.grid.ny
410 speed = np.ma.sqrt(u_ax ** 2 + v_ax ** 2)
412 def forward_time(xi, yi):
415 raise TerminateTrajectory()
419 return ui * dt_ds, vi * dt_ds
421 def backward_time(xi, yi):
422 dxi, dyi = forward_time(xi, yi)
426 """Return x, y grid-coordinates of trajectory based on starting point.
428 Integrate both forward and backward
in time
from starting point
in
431 Integration
is terminated when a trajectory reaches a domain boundary
432 or when it crosses into an already occupied cell
in the StreamMask. The
433 resulting trajectory
is None if it
is shorter than `minlength`.
436 stotal, x_traj, y_traj = 0., [], []
438 dmap.start_trajectory(x0, y0)
439 if integration_direction
in [
'both',
'backward']:
445 if integration_direction
in [
'both',
'forward']:
446 dmap.reset_start_point(x0, y0)
455 if stotal > minlength:
456 return x_traj, y_traj
458 dmap.undo_trajectory()
465 """2nd-order Runge-Kutta algorithm with adaptive step size.
467 This method is also referred to
as the improved Euler
's method, or Heun's
468 method. This method
is favored over higher-order methods because:
470 1. To get decent looking trajectories
and to sample every mask cell
471 on the trajectory we need a small timestep, so a lower order
472 solver doesn
't hurt us unless the data is *very* high resolution.
473 In fact, for cases where the user inputs
474 data smaller
or of similar grid size to the mask grid, the higher
475 order corrections are negligible because of the very fast linear
476 interpolation used
in `interpgrid`.
478 2. For high resolution input data (i.e. beyond the mask
479 resolution), we must reduce the timestep. Therefore, an adaptive
480 timestep
is more suited to the problem
as this would be very hard
481 to judge automatically otherwise.
483 This integrator
is about 1.5 - 2x
as fast
as both the RK4
and RK45
484 solvers
in most setups on my machine. I would recommend removing the
485 other two to keep things simple.
498 maxds =
min(1. / dmap.mask.nx, 1. / dmap.mask.ny, 0.1)
508 while dmap.grid.within_grid(xi, yi)
and iterations < 10000:
513 k2x, k2y =
f(xi + ds * k1x,
518 ds, xf_traj, yf_traj =
_euler_step(xf_traj, yf_traj, dmap, f)
521 except TerminateTrajectory:
526 dx2 = ds * 0.5 * (k1x + k2x)
527 dy2 = ds * 0.5 * (k1y + k2y)
529 nx, ny = dmap.grid.shape
531 error = np.sqrt(((dx2 - dx1) / nx) ** 2 + ((dy2 - dy1) / ny) ** 2)
538 dmap.update_trajectory(xi, yi)
539 except InvalidIndexError:
541 if (stotal + ds) > maxlength:
549 ds =
min(maxds, 0.85 * ds * (maxerror / error) ** 0.5)
553 return stotal, xf_traj, yf_traj
557 """Simple Euler integration step that extends streamline to boundary."""
558 ny, nx = dmap.grid.shape
567 dsx = (nx - 1 - xi) / cx
573 dsy = (ny - 1 - yi) / cy
575 xf_traj.append(xi + cx * ds)
576 yf_traj.append(yi + cy * ds)
577 return ds, xf_traj, yf_traj
584 """Fast 2D, linear interpolation on an integer grid"""
587 if isinstance(xi, np.ndarray):
588 x = xi.astype(np.int)
589 y = yi.astype(np.int)
591 xn = np.clip(x + 1, 0, Nx - 1)
592 yn = np.clip(y + 1, 0, Ny - 1)
612 a0 = a00 * (1 - xt) + a01 * xt
613 a1 = a10 * (1 - xt) + a11 * xt
614 ai = a0 * (1 - yt) + a1 * yt
616 if not isinstance(xi, np.ndarray):
617 if np.ma.is_masked(ai):
618 raise TerminateTrajectory
624 """Yield starting points for streamlines.
626 Trying points on the boundary first gives higher quality streamlines.
627 This algorithm starts with a point on the mask corner
and spirals inward.
628 This algorithm
is inefficient, but fast compared to rest of streamplot.
638 for i
in xrange(nx * ny):
642 if direction ==
'right':
647 elif direction ==
'up':
652 elif direction ==
'left':
657 elif direction ==
'down':
def mask2grid(self, xm, ym)
def start_trajectory(self, xg, yg)
def data2grid(self, xd, yd)
def update_trajectory(self, xg, yg)
def grid2mask(self, xi, yi)
def __init__(self, grid, mask)
def reset_start_point(self, xg, yg)
def undo_trajectory(self)
def within_grid(self, xi, yi)
def _update_trajectory(self, xm, ym)
def _undo_trajectory(self)
def _start_trajectory(self, xm, ym)
def __init__(self, density)
def __getitem__(self, *args)
def __init__(self, lines, arrows, **kwargs)
subroutine append(buffer, i, d)
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...
real function, public integrate(fkt, xl, xr, eps, plist, method)
Numerical integration function.
def _euler_step(xf_traj, yf_traj, dmap, f)
def get_integrator(u, v, dmap, minlength, maxlength, integration_direction)
def interpgrid(a, xi, yi)
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 _gen_starting_points(shape)
def _integrate_rk12(x0, y0, dmap, f, maxlength)