@@ -136,7 +136,9 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=1, color='k', cmap=None,
136136 ## Add arrows half way along each trajectory.
137137 s = np .cumsum (np .sqrt (np .diff (tx )** 2 + np .diff (ty )** 2 ))
138138 n = np .searchsorted (s , s [- 1 ] / 2. )
139- p = mpp .FancyArrowPatch ((tx [n ], ty [n ]), (tx [n + 1 ], ty [n + 1 ]), ** arrow_kw )
139+ arrow_tail = (tx [n ], ty [n ])
140+ arrow_head = (np .mean (tx [n :n + 2 ]), np .mean (ty [n :n + 2 ]))
141+ p = mpp .FancyArrowPatch (arrow_tail , arrow_head , ** arrow_kw )
140142 axes .add_patch (p )
141143
142144 axes .update_datalim (((x .min (), y .min ()), (x .max (), y .max ())))
@@ -297,6 +299,12 @@ def _update_trajectory(self, xm, ym):
297299class InvalidIndexError (Exception ):
298300 pass
299301
302+ class OutOfBounds (Exception ):
303+ pass
304+
305+ class Terminated (Exception ):
306+ pass
307+
300308
301309# Integrator definitions
302310#========================
@@ -326,6 +334,8 @@ def backward_time(xi, yi):
326334 _integrate = _rk4
327335 elif integrator == 'RK45' :
328336 _integrate = _rk45
337+ elif integrator == 'RK12' :
338+ _integrate = _rk12
329339
330340 def rk4_integrate (x0 , y0 ):
331341 """Return x, y coordinates of trajectory based on starting point.
@@ -353,10 +363,112 @@ def rk4_integrate(x0, y0):
353363
354364 return rk4_integrate
355365
366+ ## I have added this RK12 integrator, which I think supercedes both
367+ ## other integrators. The rationale is two-fold:
368+ ##
369+ ## 1. To get decent looking trajectories and to sample every mask cell
370+ ## on the trajectory we need a small timestep, so a lower order
371+ ## solver doesn't hurt us unless the data is *very* high resolution.
372+ ## In fact, for cases where the user inputs
373+ ## data smaller or of similar grid size to the mask grid, the higher
374+ ## order corrections are negligable because of the v fast linear
375+ ## interpolation used in interpgrid.
376+ ##
377+ ## 2. For high resolution input data (i.e. beyond the mask
378+ ## resolution), we must reduce the timestep. Therefore, an adaptive
379+ ## timestep is more suited to the problem as this would be very hard
380+ ## to judge automatically otherwise.
381+ ##
382+ ## This integrator is about 1.5 - 2x as fast as both the RK4 and RK45
383+ ## solvers in most setups on my machine. I would recommend removing the
384+ ## other two to keep things simple.
385+
386+ def _rk12 (x0 , y0 , dmap , f ):
387+ ## This error is below that needed to match the RK4 integrator. It
388+ ## is set for visual reasons -- too low and corners start
389+ ## appearing ugly and jagged. Can be tuned.
390+ maxerror = 0.003
391+
392+ ## This limit is important (for all integrators) to avoid the
393+ ## trajectory skipping some mask cells. We could relax this
394+ ## condition if we use the code which is commented out below to
395+ ## increment the location gradually. However, due to the efficient
396+ ## nature of the interpolation, this doesn't boost speed by much
397+ ## for quite a bit of complexity.
398+ maxds = min (1. / dmap .mask .nx , 1. / dmap .mask .ny , 0.1 )
399+
400+ ds = maxds
401+ stotal = 0
402+ xi = x0
403+ yi = y0
404+ xf_traj = []
405+ yf_traj = []
406+
407+ try :
408+ while dmap .grid .valid_index (xi , yi ):
409+ xf_traj .append (xi )
410+ yf_traj .append (yi )
411+ try :
412+ k1x , k1y = f (xi , yi )
413+ k2x , k2y = f (xi + ds * k1x ,
414+ yi + ds * k1y )
415+ except IndexError :
416+ # Out of the domain on one of the intermediate steps
417+ raise OutOfBounds
418+
419+ dx1 = ds * k1x
420+ dy1 = ds * k1y
421+ dx2 = ds * 0.5 * (k1x + k2x )
422+ dy2 = ds * 0.5 * (k1y + k2y )
423+
424+ nx , ny = dmap .grid .shape
425+ # Error is normalized to the axes coordinates
426+ error = np .sqrt (((dx2 - dx1 )/ nx )** 2 + ((dy2 - dy1 )/ ny )** 2 )
427+
428+ # Only save step if within error tolerance
429+ if error < maxerror :
430+ xi += dx2
431+ yi += dy2
432+ try :
433+ dmap .update_trajectory (xi , yi )
434+ except InvalidIndexError :
435+ raise Terminated
436+ if (stotal + ds ) > 2 :
437+ raise Terminated
438+ stotal += ds
439+
440+ # recalculate stepsize based on Runge-Kutta-Fehlberg method
441+ ds = min (maxds , 0.85 * ds * (maxerror / error )** 0.2 )
442+
443+ except OutOfBounds :
444+ ## This is raised when the edge of the domain is
445+ ## reached. Here, a simple Euler integration is used up to the
446+ ## boundary of the domain, improving the neatness of the
447+ ## figure.
448+ nx , ny = dmap .grid .shape
449+ xi = xf_traj [- 1 ] # in data coordinates
450+ yi = yf_traj [- 1 ]
451+ cx , cy = f (xi , yi ) # ds.cx is in data coordinates, ds in axis coord.
452+ if cx > 0 :
453+ dsx = (nx - 1 - xi ) / cx
454+ else :
455+ dsx = xi / - cx
456+ if cy > 0 :
457+ dsy = (ny - 1 - yi ) / cy
458+ else :
459+ dsy = yi / - cy
460+ ds = min (dsx , dsy )
461+ xf_traj .append (xi + cx * ds )
462+ yf_traj .append (yi + cy * ds )
463+ stotal += ds
464+ except Terminated :
465+ pass
466+
467+ return stotal , xf_traj , yf_traj
356468
357469def _rk4 (x0 , y0 , dmap , f ):
358470 """4th-order Runge-Kutta algorithm with fixed step size"""
359- ds = 0.01 # min(1./grid.ny , 1./grid .ny, 0.01)
471+ ds = min (1. / dmap . mask . nx , 1. / dmap . mask .ny , 0.01 )
360472 stotal = 0
361473 xi = x0
362474 yi = y0
@@ -394,7 +506,7 @@ def _rk4(x0, y0, dmap, f):
394506def _rk45 (x0 , y0 , dmap , f ):
395507 """5th-order Runge-Kutta algorithm with adaptive step size"""
396508 maxerror = 0.001
397- maxds = 0.03
509+ maxds = min ( 1. / dmap . mask . nx , 1. / dmap . mask . ny , 0.03 )
398510 ds = maxds
399511 stotal = 0
400512 xi = x0
@@ -457,7 +569,6 @@ def _rk45(x0, y0, dmap, f):
457569 ds = min (maxds , 0.85 * ds * (maxerror / error )** 0.2 )
458570 return stotal , xf_traj , yf_traj
459571
460-
461572# Utility functions
462573#========================
463574
0 commit comments