streamplot.py
Go to the documentation of this file.
1"""
2Streamline plotting for 2D vector fields.
3
4"""
5from __future__ import (absolute_import, division, print_function,
6 unicode_literals)
7
8from matplotlib.externals import six
9from matplotlib.externals.six.moves import xrange
10
11import numpy as np
12import matplotlib
13import matplotlib.cm as cm
14import matplotlib.colors as mcolors
15import matplotlib.collections as mcollections
16import matplotlib.patches as patches
17
18
19__all__ = ['streamplot']
20
21
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.
27
28 *x*, *y* : 1d arrays
29 an *evenly spaced* grid.
30 *u*, *v* : 2d arrays
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.
49 *arrowsize* : float
50 Factor scale arrow size.
51 *arrowstyle* : str
52 Arrow style specification.
53 See :class:`~matplotlib.patches.FancyArrowPatch`.
54 *minlength* : float
55 Minimum length of streamline in axes coordinates.
56 *maxlength* : float
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.
63 *zorder* : int
64 any number
65
66 Returns:
67
68 *stream_container* : StreamplotSet
69 Container object with attributes
70
71 - lines: `matplotlib.collections.LineCollection` of streamlines
72
73 - arrows: collection of `matplotlib.patches.FancyArrowPatch`
74 objects representing arrows half-way along stream
75 lines.
76
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.
80
81 """
82 grid = Grid(x, y)
83 mask = StreamMask(density)
84 dmap = DomainMap(grid, mask)
85
86 # default to data coordinates
87 if transform is None:
88 transform = axes.transData
89
90 if color is None and 'color' in axes._get_lines._prop_keys:
91 color = six.next(axes._get_lines.prop_cycler)['color']
92
93 if linewidth is None:
94 linewidth = matplotlib.rcParams['lines.linewidth']
95
96 line_kw = {}
97 arrow_kw = dict(arrowstyle=arrowstyle, mutation_scale=10 * arrowsize)
98
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)
104
105 if integration_direction == 'both':
106 maxlength /= 2.
107
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)
113 line_colors = []
114 color = np.ma.masked_invalid(color)
115 else:
116 line_kw['color'] = color
117 arrow_kw['color'] = color
118
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'] = []
124 else:
125 line_kw['linewidth'] = linewidth
126 arrow_kw['linewidth'] = linewidth
127
128 line_kw['zorder'] = zorder
129 arrow_kw['zorder'] = zorder
130
131
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)
135
136 u = np.ma.masked_invalid(u)
137 v = np.ma.masked_invalid(v)
138
139 integrate = get_integrator(u, v, dmap, minlength, maxlength,
140 integration_direction)
141
142 trajectories = []
143 if start_points is None:
144 for xm, ym in _gen_starting_points(mask.shape):
145 if mask[ym, xm] == 0:
146 xg, yg = dmap.mask2grid(xm, ym)
147 t = integrate(xg, yg)
148 if t is not None:
149 trajectories.append(t)
150 else:
151 # Convert start_points from data to array coords
152 # Shift the seed points from the bottom left of the data so that
153 # data2grid works properly.
154 sp2 = np.asanyarray(start_points, dtype=np.float).copy()
155 sp2[:, 0] += np.abs(x[0])
156 sp2[:, 1] += np.abs(y[0])
157 for xs, ys in sp2:
158 xg, yg = dmap.data2grid(xs, ys)
159 t = integrate(xg, yg)
160 if t is not None:
161 trajectories.append(t)
162
163 if use_multicolor_lines:
164 if norm is None:
165 norm = mcolors.Normalize(color.min(), color.max())
166 if cmap is None:
167 cmap = cm.get_cmap(matplotlib.rcParams['image.cmap'])
168 else:
169 cmap = cm.get_cmap(cmap)
170
171 streamlines = []
172 arrows = []
173 for t in trajectories:
174 tgx = np.array(t[0])
175 tgy = np.array(t[1])
176 # Rescale from grid-coordinates to data-coordinates.
177 tx = np.array(t[0]) * grid.dx + grid.x_origin
178 ty = np.array(t[1]) * grid.dy + grid.y_origin
179
180 points = np.transpose([tx, ty]).reshape(-1, 1, 2)
181 streamlines.extend(np.hstack([points[:-1], points[1:]]))
182
183 # Add arrows half way along each trajectory.
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]))
188
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]
193
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]))
198
199 p = patches.FancyArrowPatch(arrow_tail,
200 arrow_head,
201 transform=transform,
202 #margins=False,
203 **arrow_kw)
204 axes.add_patch(p)
205 arrows.append(p)
206
207 lc = mcollections.LineCollection(streamlines,
208 transform=transform,
209 #margins=False,
210 **line_kw)
211 if use_multicolor_lines:
212 lc.set_array(np.ma.hstack(line_colors))
213 lc.set_cmap(cmap)
214 lc.set_norm(norm)
215 axes.add_collection(lc)
216 axes.autoscale_view()
217
218 ac = matplotlib.collections.PatchCollection(arrows)#, margins=False)
219 stream_container = StreamplotSet(lc, ac)
220 return stream_container
221
222
224
225 def __init__(self, lines, arrows, **kwargs):
226 self.lines = lines
227 self.arrows = arrows
228
229
230# Coordinate definitions
231# ========================
232
234 """Map representing different coordinate systems.
235
236 Coordinate definitions:
237
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.
244
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`.
249 """
250
251 def __init__(self, grid, mask):
252 self.grid = grid
253 self.mask = mask
254 # Constants for conversion between grid- and mask-coordinates
255 self.x_grid2mask = float(mask.nx - 1) / grid.nx
256 self.y_grid2mask = float(mask.ny - 1) / grid.ny
257
258 self.x_mask2grid = 1. / self.x_grid2mask
259 self.y_mask2grid = 1. / self.y_grid2mask
260
261 self.x_data2grid = grid.nx / grid.width
262 self.y_data2grid = grid.ny / grid.height
263
264 def grid2mask(self, xi, yi):
265 """Return nearest space in mask-coords from given grid-coords."""
266 return (int((xi * self.x_grid2mask) + 0.5),
267 int((yi * self.y_grid2mask) + 0.5))
268
269 def mask2grid(self, xm, ym):
270 return xm * self.x_mask2grid, ym * self.y_mask2grid
271
272 def data2grid(self, xd, yd):
273 return xd * self.x_data2grid, yd * self.y_data2grid
274
275 def start_trajectory(self, xg, yg):
276 xm, ym = self.grid2mask(xg, yg)
277 self.mask._start_trajectory(xm, ym)
278
279 def reset_start_point(self, xg, yg):
280 xm, ym = self.grid2mask(xg, yg)
281 self.mask._current_xy = (xm, ym)
282
283 def update_trajectory(self, xg, yg):
284 if not self.grid.within_grid(xg, yg):
285 raise InvalidIndexError
286 xm, ym = self.grid2mask(xg, yg)
287 self.mask._update_trajectory(xm, ym)
288
290 self.mask._undo_trajectory()
291
292
294 """Grid of data."""
295 def __init__(self, x, y):
296
297 if x.ndim == 1:
298 pass
299 elif x.ndim == 2:
300 x_row = x[0, :]
301 if not np.allclose(x_row, x):
302 raise ValueError("The rows of 'x' must be equal")
303 x = x_row
304 else:
305 raise ValueError("'x' can have at maximum 2 dimensions")
306
307 if y.ndim == 1:
308 pass
309 elif y.ndim == 2:
310 y_col = y[:, 0]
311 if not np.allclose(y_col, y.T):
312 raise ValueError("The columns of 'y' must be equal")
313 y = y_col
314 else:
315 raise ValueError("'y' can have at maximum 2 dimensions")
316
317 self.nx = len(x)
318 self.ny = len(y)
319
320 self.dx = x[1] - x[0]
321 self.dy = y[1] - y[0]
322
323 self.x_origin = x[0]
324 self.y_origin = y[0]
325
326 self.width = x[-1] - x[0]
327 self.height = y[-1] - y[0]
328
329 @property
330 def shape(self):
331 return self.ny, self.nx
332
333 def within_grid(self, xi, yi):
334 """Return True if point is a valid index of grid."""
335 # Note that xi/yi can be floats; so, for example, we can't simply check
336 # `xi < self.nx` since `xi` can be `self.nx - 1 < xi < self.nx`
337 return xi >= 0 and xi <= self.nx - 1 and yi >= 0 and yi <= self.ny - 1
338
339
341 """Mask to keep track of discrete regions crossed by streamlines.
342
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.
347 """
348
349 def __init__(self, density):
350 if np.isscalar(density):
351 if density <= 0:
352 raise ValueError("If a scalar, 'density' must be positive")
353 self.nx = self.ny = int(30 * density)
354 else:
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])
359 self._mask = np.zeros((self.ny, self.nx))
360 self.shape = self._mask.shape
361
362 self._current_xy = None
363
364 def __getitem__(self, *args):
365 return self._mask.__getitem__(*args)
366
367 def _start_trajectory(self, xm, ym):
368 """Start recording streamline trajectory"""
369 self._traj = []
370 self._update_trajectory(xm, ym)
371
373 """Remove current trajectory from mask"""
374 for t in self._traj:
375 self._mask.__setitem__(t, 0)
376
377 def _update_trajectory(self, xm, ym):
378 """Update current trajectory position in mask.
379
380 If the new position has already been filled, raise `InvalidIndexError`.
381 """
382 if self._current_xy != (xm, ym):
383 if self[ym, xm] == 0:
384 self._traj.append((ym, xm))
385 self._mask[ym, xm] = 1
386 self._current_xy = (xm, ym)
387 else:
388 raise InvalidIndexError
389
390
392 pass
393
394
395class TerminateTrajectory(Exception):
396 pass
397
398
399# Integrator definitions
400#========================
401
402def get_integrator(u, v, dmap, minlength, maxlength, integration_direction):
403
404 # rescale velocity onto grid-coordinates for integrations.
405 u, v = dmap.data2grid(u, v)
406
407 # speed (path length) will be in axes-coordinates
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)
411
412 def forward_time(xi, yi):
413 ds_dt = interpgrid(speed, xi, yi)
414 if ds_dt == 0:
415 raise TerminateTrajectory()
416 dt_ds = 1. / ds_dt
417 ui = interpgrid(u, xi, yi)
418 vi = interpgrid(v, xi, yi)
419 return ui * dt_ds, vi * dt_ds
420
421 def backward_time(xi, yi):
422 dxi, dyi = forward_time(xi, yi)
423 return -dxi, -dyi
424
425 def integrate(x0, y0):
426 """Return x, y grid-coordinates of trajectory based on starting point.
427
428 Integrate both forward and backward in time from starting point in
429 grid coordinates.
430
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`.
434 """
435
436 stotal, x_traj, y_traj = 0., [], []
437
438 dmap.start_trajectory(x0, y0)
439 if integration_direction in ['both', 'backward']:
440 s, xt, yt = _integrate_rk12(x0, y0, dmap, backward_time, maxlength)
441 stotal += s
442 x_traj += xt[::-1]
443 y_traj += yt[::-1]
444
445 if integration_direction in ['both', 'forward']:
446 dmap.reset_start_point(x0, y0)
447 s, xt, yt = _integrate_rk12(x0, y0, dmap, forward_time, maxlength)
448 if len(x_traj) > 0:
449 xt = xt[1:]
450 yt = yt[1:]
451 stotal += s
452 x_traj += xt
453 y_traj += yt
454
455 if stotal > minlength:
456 return x_traj, y_traj
457 else: # reject short trajectories
458 dmap.undo_trajectory()
459 return None
460
461 return integrate
462
463
464def _integrate_rk12(x0, y0, dmap, f, maxlength):
465 """2nd-order Runge-Kutta algorithm with adaptive step size.
466
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:
469
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`.
477
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.
482
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.
486 """
487 # This error is below that needed to match the RK4 integrator. It
488 # is set for visual reasons -- too low and corners start
489 # appearing ugly and jagged. Can be tuned.
490 maxerror = 0.003
491
492 # This limit is important (for all integrators) to avoid the
493 # trajectory skipping some mask cells. We could relax this
494 # condition if we use the code which is commented out below to
495 # increment the location gradually. However, due to the efficient
496 # nature of the interpolation, this doesn't boost speed by much
497 # for quite a bit of complexity.
498 maxds = min(1. / dmap.mask.nx, 1. / dmap.mask.ny, 0.1)
499
500 ds = maxds
501 stotal = 0
502 xi = x0
503 yi = y0
504 xf_traj = []
505 yf_traj = []
506 iterations = 0
507
508 while dmap.grid.within_grid(xi, yi) and iterations < 10000:
509 xf_traj.append(xi)
510 yf_traj.append(yi)
511 try:
512 k1x, k1y = f(xi, yi)
513 k2x, k2y = f(xi + ds * k1x,
514 yi + ds * k1y)
515 except IndexError:
516 # Out of the domain on one of the intermediate integration steps.
517 # Take an Euler step to the boundary to improve neatness.
518 ds, xf_traj, yf_traj = _euler_step(xf_traj, yf_traj, dmap, f)
519 stotal += ds
520 break
521 except TerminateTrajectory:
522 break
523
524 dx1 = ds * k1x
525 dy1 = ds * k1y
526 dx2 = ds * 0.5 * (k1x + k2x)
527 dy2 = ds * 0.5 * (k1y + k2y)
528
529 nx, ny = dmap.grid.shape
530 # Error is normalized to the axes coordinates
531 error = np.sqrt(((dx2 - dx1) / nx) ** 2 + ((dy2 - dy1) / ny) ** 2)
532
533 # Only save step if within error tolerance
534 if error < maxerror:
535 xi += dx2
536 yi += dy2
537 try:
538 dmap.update_trajectory(xi, yi)
539 except InvalidIndexError:
540 break
541 if (stotal + ds) > maxlength:
542 break
543 stotal += ds
544
545 # recalculate stepsize based on step error
546 if error == 0:
547 ds = maxds
548 else:
549 ds = min(maxds, 0.85 * ds * (maxerror / error) ** 0.5)
550
551 iterations += 1
552
553 return stotal, xf_traj, yf_traj
554
555
556def _euler_step(xf_traj, yf_traj, dmap, f):
557 """Simple Euler integration step that extends streamline to boundary."""
558 ny, nx = dmap.grid.shape
559 xi = xf_traj[-1]
560 yi = yf_traj[-1]
561 cx, cy = f(xi, yi)
562 if cx == 0:
563 dsx = np.inf
564 elif cx < 0:
565 dsx = xi / -cx
566 else:
567 dsx = (nx - 1 - xi) / cx
568 if cy == 0:
569 dsy = np.inf
570 elif cy < 0:
571 dsy = yi / -cy
572 else:
573 dsy = (ny - 1 - yi) / cy
574 ds = min(dsx, dsy)
575 xf_traj.append(xi + cx * ds)
576 yf_traj.append(yi + cy * ds)
577 return ds, xf_traj, yf_traj
578
579
580# Utility functions
581# ========================
582
583def interpgrid(a, xi, yi):
584 """Fast 2D, linear interpolation on an integer grid"""
585
586 Ny, Nx = np.shape(a)
587 if isinstance(xi, np.ndarray):
588 x = xi.astype(np.int)
589 y = yi.astype(np.int)
590 # Check that xn, yn don't exceed max index
591 xn = np.clip(x + 1, 0, Nx - 1)
592 yn = np.clip(y + 1, 0, Ny - 1)
593 else:
594 x = np.int(xi)
595 y = np.int(yi)
596 # conditional is faster than clipping for integers
597 if x == (Nx - 2):
598 xn = x
599 else:
600 xn = x + 1
601 if y == (Ny - 2):
602 yn = y
603 else:
604 yn = y + 1
605
606 a00 = a[y, x]
607 a01 = a[y, xn]
608 a10 = a[yn, x]
609 a11 = a[yn, xn]
610 xt = xi - x
611 yt = yi - y
612 a0 = a00 * (1 - xt) + a01 * xt
613 a1 = a10 * (1 - xt) + a11 * xt
614 ai = a0 * (1 - yt) + a1 * yt
615
616 if not isinstance(xi, np.ndarray):
617 if np.ma.is_masked(ai):
618 raise TerminateTrajectory
619
620 return ai
621
622
624 """Yield starting points for streamlines.
625
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.
629 """
630 ny, nx = shape
631 xfirst = 0
632 yfirst = 1
633 xlast = nx - 1
634 ylast = ny - 1
635 x, y = 0, 0
636 i = 0
637 direction = 'right'
638 for i in xrange(nx * ny):
639
640 yield x, y
641
642 if direction == 'right':
643 x += 1
644 if x >= xlast:
645 xlast -= 1
646 direction = 'up'
647 elif direction == 'up':
648 y += 1
649 if y >= ylast:
650 ylast -= 1
651 direction = 'left'
652 elif direction == 'left':
653 x -= 1
654 if x <= xfirst:
655 xfirst += 1
656 direction = 'down'
657 elif direction == 'down':
658 y -= 1
659 if y <= yfirst:
660 yfirst += 1
661 direction = 'right'
def mask2grid(self, xm, ym)
Definition: streamplot.py:269
def start_trajectory(self, xg, yg)
Definition: streamplot.py:275
def data2grid(self, xd, yd)
Definition: streamplot.py:272
def update_trajectory(self, xg, yg)
Definition: streamplot.py:283
def grid2mask(self, xi, yi)
Definition: streamplot.py:264
def __init__(self, grid, mask)
Definition: streamplot.py:251
def reset_start_point(self, xg, yg)
Definition: streamplot.py:279
def undo_trajectory(self)
Definition: streamplot.py:289
def within_grid(self, xi, yi)
Definition: streamplot.py:333
def shape(self)
Definition: streamplot.py:330
def __init__(self, x, y)
Definition: streamplot.py:295
def _update_trajectory(self, xm, ym)
Definition: streamplot.py:377
def _undo_trajectory(self)
Definition: streamplot.py:372
def _start_trajectory(self, xm, ym)
Definition: streamplot.py:367
def __init__(self, density)
Definition: streamplot.py:349
def __getitem__(self, *args)
Definition: streamplot.py:364
def __init__(self, lines, arrows, **kwargs)
Definition: streamplot.py:225
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...
def min(send, axis=None)
Definition: coplot.py:114
real function, public integrate(fkt, xl, xr, eps, plist, method)
Numerical integration function.
integer, parameter the
def _euler_step(xf_traj, yf_traj, dmap, f)
Definition: streamplot.py:556
def get_integrator(u, v, dmap, minlength, maxlength, integration_direction)
Definition: streamplot.py:402
def interpgrid(a, xi, yi)
Definition: streamplot.py:583
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')
Definition: streamplot.py:25
def _gen_starting_points(shape)
Definition: streamplot.py:623
def _integrate_rk12(x0, y0, dmap, f, maxlength)
Definition: streamplot.py:464