In [ ]:
import numpy as np
import matplotlib.pyplot as plt

from scipy import integrate
from matplotlib import mlab
from matplotlib import animation

from sympy.abc import x, y
from sympy import sin, atan2, sqrt, lambdify, latex

from JSAnimation import IPython_display

plt.rc('contour', negative_linestyle='solid')

In [ ]:
def velocity_field(psi):
    u = lambdify((x, y), psi.diff(y), 'numpy')
    v = lambdify((x, y), -psi.diff(x), 'numpy')
    return u, v

def euler(f, pts, dt):
    vel = np.asarray([f(xy) for xy in pts])
    return pts + vel * dt

def rk4(f, pts, dt):
    new_pts = [mlab.rk4(f, xy, [0, dt])[-1] for xy in pts]
    return new_pts

def ode_scipy(f, pts, dt):
    new_pts = [integrate.odeint(f, xy, [0, dt])[-1] for xy in pts]
    return new_pts

available_integrators = dict(euler=euler, rk4=rk4, scipy=ode_scipy)

In [ ]:
def displace_func_from_velocity_funcs(u_func, v_func, method='rk4'):
    """Return function that calculates particle positions after time step.

    Parameters
    ----------
    u_func, v_func : functions
        Velocity fields which return velocities at arbitrary coordinates.
    method : {'euler' | 'rk4' | 'scipy'}
        Integration method to update particle positions at each time step.
    """

    def velocity(xy, t=0):
        """Return (u, v) velocities for given (x, y) coordinates."""
        # Dummy `t` variable required to work with integrators
        # Must return a list (not a tuple) for scipy's integrate functions.
        return [u_func(*xy), v_func(*xy)]

    odeint = available_integrators[method]

    def displace(xy, dt):
        return odeint(velocity, xy, dt)

    return displace

# Initialize velocity field and displace *functions*
radius = 1
def cylinder_stream_function(U=1, R=radius):
    r = sqrt(x**2 + y**2)
    psi = U * (r - R**2 / r) * sin(atan2(y, x))
    return psi

def random_y(ylim):
    yrange = np.diff(ylim)
    return yrange * np.random.rand(1)[0] + ylim[0]

def remove_particles(pts, xlim, ylim):
    if len(pts) == 0:
        return []
    outside_xlim = (pts[:, 0] < xlim[0]) | (pts[:, 0] > xlim[1])
    outside_ylim = (pts[:, 1] < ylim[0]) | (pts[:, 1] > ylim[1])
    keep = ~(outside_xlim | outside_ylim)
    return pts[keep]

In [ ]:
def cylinder_flow(frames=100, interval=30):
    fig, ax = plt.subplots(figsize=(6, 6))
    ax.set_aspect('equal')

    # Non-animated.
    stream_function = cylinder_stream_function()
    u, v = velocity_field(stream_function)

    xlim, ylim = (-3, 3), (-3, 3)
    x0, x1 = xlim
    y0, y1 = ylim
    # Create 100 x 100 grid of coordinates.
    Y, X =  np.mgrid[x0:x1:100j, y0:y1:100j]
    # Horizontal and vertical velocities corresponding to coordinates.
    U = u(X, Y)
    V = v(X, Y)
    ax.streamplot(X, Y, U, V, color='0.7')
    c = plt.Circle((0, 0), radius=radius, facecolor='grey')
    ax.add_patch(c)

    # Animated.
    particles, = ax.plot([], [], 'ro')

    def init():
        global pts
        pts = []
        return particles

    dt = 0.05
    displace = displace_func_from_velocity_funcs(u, v)
    def animate(k):
        global pts
        pts = list(pts)
        pts.append((xlim[0], random_y(ylim)))
        pts = displace(pts, dt)
        pts = np.asarray(pts)
        pts = remove_particles(pts, xlim, ylim)
        x, y = np.asarray(pts).transpose()
        particles.set_data(x, y)
        return particles

    return animation.FuncAnimation(fig, animate, init_func=init,
                                frames=frames, interval=interval)

In [ ]:
cylinder_flow(frames=300)

In [ ]: