import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Circle, PathPatch
from matplotlib.path import Path
# Set up grid
x = np.linspace ( -2 , 2 , 500 )
y = np.linspace ( -2 , 2 , 500 )
X, Y = np.meshgrid ( x, y)
# Convert to polar
r = np.sqrt ( X**2 + Y**2 )
theta = np.arctan2 ( Y, X)
# Parameters
U_inf = 1.0 # Freestream velocity
R = 0.8 # Circle radius
lambda_vals = np.linspace ( 0 , 0.15 , 30 ) # Joukowsky parameter range
# Starting points for streamlines (12 points)
start_y = np.linspace ( -1.5 , 1.5 , 12 )
start_points = np.array ( [ [ -2 * np.ones ( 12 ) , start_y] ] ) .T .reshape ( -1 , 2 )
# Joukowsky transformation
def joukowsky( z, lambda_) :
return z + lambda_**2 / z
# Initialize figure
fig, ax = plt.subplots ( figsize= ( 8 , 8 ) )
ax.set_xlim ( -2 , 2 )
ax.set_ylim ( -2 , 2 )
ax.set_facecolor ( 'black' )
ax.set_xticks ( [ ] )
ax.set_yticks ( [ ] )
# Circle (initial shape)
circle = Circle( ( 0 , 0 ) , R, color= '#FAF9F6' , fill= True , zorder= 3 )
ax.add_patch ( circle)
# Pre-compute velocity fields for the circle (unchanged)
U_r = U_inf * ( 1 - ( R**2 ) / ( r**2 ) ) * np.cos ( theta)
U_theta = -U_inf * ( 1 + ( R**2 ) / ( r**2 ) ) * np.sin ( theta)
U = U_r * np.cos ( theta) - U_theta * np.sin ( theta)
V = U_r * np.sin ( theta) + U_theta * np.cos ( theta)
# Initial streamlines (circle)
strm = ax.streamplot ( X, Y, U, V, color= '#E63946' , linewidth= 1.5 ,
start_points= start_points, arrowsize= 0 , zorder= 2 )
# Add arrows manually (same as your original)
for line in strm.lines .get_segments ( ) :
arrow_interval = max ( 1 , ( len ( line) //8 ) *2 )
for i in range ( 0 , len ( line) - arrow_interval, arrow_interval) :
p_start, p_end = line[ i] , line[ i + 1 ]
dx, dy = p_end[ 0 ] - p_start[ 0 ] , p_end[ 1 ] - p_start[ 1 ]
ax.arrow ( p_start[ 0 ] , p_start[ 1 ] , dx, dy, shape= 'full' , lw= 1 ,
head_width= 0.05 , head_length= 0.1 , color= '#E63946' , zorder= 2 )
# Animation function
def update( frame) :
global strm, circle
lambda_ = lambda_vals[ frame]
# Clear previous streamlines and circle
for art in ax.collections [ :] : # Remove all streamlines/arrows
art.remove ( )
ax.patches .remove ( circle)
# Apply Joukowsky transformation to grid and circle
Z = X + 1j*Y
Z_transformed = joukowsky( Z, lambda_)
# Transform velocity field (approximate: assumes potential flow transforms)
# Note: For exact physics, recompute potential in transformed plane
U_transformed = U.copy ( )
V_transformed = V.copy ( )
# Update streamlines
strm = ax.streamplot ( X, Y, U_transformed, V_transformed, color= '#E63946' ,
linewidth= 1.5 , start_points= start_points, arrowsize= 0 , zorder= 2 )
# Add arrows
for line in strm.lines .get_segments ( ) :
arrow_interval = max ( 1 , ( len ( line) //8 ) *2 )
for i in range ( 0 , len ( line) - arrow_interval, arrow_interval) :
p_start, p_end = line[ i] , line[ i + 1 ]
dx, dy = p_end[ 0 ] - p_start[ 0 ] , p_end[ 1 ] - p_start[ 1 ]
ax.arrow ( p_start[ 0 ] , p_start[ 1 ] , dx, dy, shape= 'full' , lw= 1 ,
head_width= 0.05 , head_length= 0.1 , color= '#E63946' , zorder= 2 )
# Draw transformed shape (airfoil)
theta_circle = np.linspace ( 0 , 2 *np.pi , 200 )
circle_points = R * np.exp ( 1j * theta_circle)
airfoil_points = joukowsky( circle_points, lambda_)
airfoil_path = Path( np.column_stack ( [ np.real ( airfoil_points) , np.imag ( airfoil_points) ] ) )
airfoil = PathPatch( airfoil_path, color= '#FAF9F6' , fill= True , zorder= 3 )
ax.add_patch ( airfoil)
ax.set_title ( f"Joukowsky Transformation (λ={lambda_:.3f})" )
return [ ]
# Create animation
ani = FuncAnimation( fig, update, frames= len ( lambda_vals) , blit= False , interval= 100 )
# Uncomment to save:
# ani.save('joukowsky_airfoil.gif', writer='pillow', fps=10)
# Or save frames:
# for i in range(len(lambda_vals)):
# update(i)
# plt.savefig(f'frame_{i:03d}.png', dpi=150, facecolor='black')
plt.show ( )
aW1wb3J0IG51bXB5IGFzIG5wCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQKZnJvbSBtYXRwbG90bGliLmFuaW1hdGlvbiBpbXBvcnQgRnVuY0FuaW1hdGlvbgpmcm9tIG1hdHBsb3RsaWIucGF0Y2hlcyBpbXBvcnQgQ2lyY2xlLCBQYXRoUGF0Y2gKZnJvbSBtYXRwbG90bGliLnBhdGggaW1wb3J0IFBhdGgKCiMgU2V0IHVwIGdyaWQKeCA9IG5wLmxpbnNwYWNlKC0yLCAyLCA1MDApCnkgPSBucC5saW5zcGFjZSgtMiwgMiwgNTAwKQpYLCBZID0gbnAubWVzaGdyaWQoeCwgeSkKCiMgQ29udmVydCB0byBwb2xhcgpyID0gbnAuc3FydChYKioyICsgWSoqMikKdGhldGEgPSBucC5hcmN0YW4yKFksIFgpCgojIFBhcmFtZXRlcnMKVV9pbmYgPSAxLjAgICAgICAjIEZyZWVzdHJlYW0gdmVsb2NpdHkKUiA9IDAuOCAgICAgICAgICAjIENpcmNsZSByYWRpdXMKbGFtYmRhX3ZhbHMgPSBucC5saW5zcGFjZSgwLCAwLjE1LCAzMCkgICMgSm91a293c2t5IHBhcmFtZXRlciByYW5nZQoKIyBTdGFydGluZyBwb2ludHMgZm9yIHN0cmVhbWxpbmVzICgxMiBwb2ludHMpCnN0YXJ0X3kgPSBucC5saW5zcGFjZSgtMS41LCAxLjUsIDEyKQpzdGFydF9wb2ludHMgPSBucC5hcnJheShbWy0yICogbnAub25lcygxMiksIHN0YXJ0X3ldXSkuVC5yZXNoYXBlKC0xLCAyKQoKIyBKb3Vrb3dza3kgdHJhbnNmb3JtYXRpb24KZGVmIGpvdWtvd3NreSh6LCBsYW1iZGFfKToKICAgIHJldHVybiB6ICsgbGFtYmRhXyoqMiAvIHoKCiMgSW5pdGlhbGl6ZSBmaWd1cmUKZmlnLCBheCA9IHBsdC5zdWJwbG90cyhmaWdzaXplPSg4LCA4KSkKYXguc2V0X3hsaW0oLTIsIDIpCmF4LnNldF95bGltKC0yLCAyKQpheC5zZXRfZmFjZWNvbG9yKCdibGFjaycpCmF4LnNldF94dGlja3MoW10pCmF4LnNldF95dGlja3MoW10pCgojIENpcmNsZSAoaW5pdGlhbCBzaGFwZSkKY2lyY2xlID0gQ2lyY2xlKCgwLCAwKSwgUiwgY29sb3I9JyNGQUY5RjYnLCBmaWxsPVRydWUsIHpvcmRlcj0zKQpheC5hZGRfcGF0Y2goY2lyY2xlKQoKIyBQcmUtY29tcHV0ZSB2ZWxvY2l0eSBmaWVsZHMgZm9yIHRoZSBjaXJjbGUgKHVuY2hhbmdlZCkKVV9yID0gVV9pbmYgKiAoMSAtIChSKioyKSAvIChyKioyKSkgKiBucC5jb3ModGhldGEpClVfdGhldGEgPSAtVV9pbmYgKiAoMSArIChSKioyKSAvIChyKioyKSkgKiBucC5zaW4odGhldGEpClUgPSBVX3IgKiBucC5jb3ModGhldGEpIC0gVV90aGV0YSAqIG5wLnNpbih0aGV0YSkKViA9IFVfciAqIG5wLnNpbih0aGV0YSkgKyBVX3RoZXRhICogbnAuY29zKHRoZXRhKQoKIyBJbml0aWFsIHN0cmVhbWxpbmVzIChjaXJjbGUpCnN0cm0gPSBheC5zdHJlYW1wbG90KFgsIFksIFUsIFYsIGNvbG9yPScjRTYzOTQ2JywgbGluZXdpZHRoPTEuNSwgCiAgICAgICAgICAgICAgICAgICAgIHN0YXJ0X3BvaW50cz1zdGFydF9wb2ludHMsIGFycm93c2l6ZT0wLCB6b3JkZXI9MikKCiMgQWRkIGFycm93cyBtYW51YWxseSAoc2FtZSBhcyB5b3VyIG9yaWdpbmFsKQpmb3IgbGluZSBpbiBzdHJtLmxpbmVzLmdldF9zZWdtZW50cygpOgogICAgYXJyb3dfaW50ZXJ2YWwgPSBtYXgoMSwgKGxlbihsaW5lKS8vOCkqMikKICAgIGZvciBpIGluIHJhbmdlKDAsIGxlbihsaW5lKSAtIGFycm93X2ludGVydmFsLCBhcnJvd19pbnRlcnZhbCk6CiAgICAgICAgcF9zdGFydCwgcF9lbmQgPSBsaW5lW2ldLCBsaW5lW2kgKyAxXQogICAgICAgIGR4LCBkeSA9IHBfZW5kWzBdIC0gcF9zdGFydFswXSwgcF9lbmRbMV0gLSBwX3N0YXJ0WzFdCiAgICAgICAgYXguYXJyb3cocF9zdGFydFswXSwgcF9zdGFydFsxXSwgZHgsIGR5LCBzaGFwZT0nZnVsbCcsIGx3PTEsCiAgICAgICAgICAgICAgICAgaGVhZF93aWR0aD0wLjA1LCBoZWFkX2xlbmd0aD0wLjEsIGNvbG9yPScjRTYzOTQ2Jywgem9yZGVyPTIpCgojIEFuaW1hdGlvbiBmdW5jdGlvbgpkZWYgdXBkYXRlKGZyYW1lKToKICAgIGdsb2JhbCBzdHJtLCBjaXJjbGUKICAgIAogICAgbGFtYmRhXyA9IGxhbWJkYV92YWxzW2ZyYW1lXQogICAgCiAgICAjIENsZWFyIHByZXZpb3VzIHN0cmVhbWxpbmVzIGFuZCBjaXJjbGUKICAgIGZvciBhcnQgaW4gYXguY29sbGVjdGlvbnNbOl06ICAjIFJlbW92ZSBhbGwgc3RyZWFtbGluZXMvYXJyb3dzCiAgICAgICAgYXJ0LnJlbW92ZSgpCiAgICBheC5wYXRjaGVzLnJlbW92ZShjaXJjbGUpCiAgICAKICAgICMgQXBwbHkgSm91a293c2t5IHRyYW5zZm9ybWF0aW9uIHRvIGdyaWQgYW5kIGNpcmNsZQogICAgWiA9IFggKyAxaipZCiAgICBaX3RyYW5zZm9ybWVkID0gam91a293c2t5KFosIGxhbWJkYV8pCiAgICAKICAgICMgVHJhbnNmb3JtIHZlbG9jaXR5IGZpZWxkIChhcHByb3hpbWF0ZTogYXNzdW1lcyBwb3RlbnRpYWwgZmxvdyB0cmFuc2Zvcm1zKQogICAgIyBOb3RlOiBGb3IgZXhhY3QgcGh5c2ljcywgcmVjb21wdXRlIHBvdGVudGlhbCBpbiB0cmFuc2Zvcm1lZCBwbGFuZQogICAgVV90cmFuc2Zvcm1lZCA9IFUuY29weSgpCiAgICBWX3RyYW5zZm9ybWVkID0gVi5jb3B5KCkKICAgIAogICAgIyBVcGRhdGUgc3RyZWFtbGluZXMKICAgIHN0cm0gPSBheC5zdHJlYW1wbG90KFgsIFksIFVfdHJhbnNmb3JtZWQsIFZfdHJhbnNmb3JtZWQsIGNvbG9yPScjRTYzOTQ2JywgCiAgICAgICAgICAgICAgICAgICAgICAgICBsaW5ld2lkdGg9MS41LCBzdGFydF9wb2ludHM9c3RhcnRfcG9pbnRzLCBhcnJvd3NpemU9MCwgem9yZGVyPTIpCiAgICAKICAgICMgQWRkIGFycm93cwogICAgZm9yIGxpbmUgaW4gc3RybS5saW5lcy5nZXRfc2VnbWVudHMoKToKICAgICAgICBhcnJvd19pbnRlcnZhbCA9IG1heCgxLCAobGVuKGxpbmUpLy84KSoyKQogICAgICAgIGZvciBpIGluIHJhbmdlKDAsIGxlbihsaW5lKSAtIGFycm93X2ludGVydmFsLCBhcnJvd19pbnRlcnZhbCk6CiAgICAgICAgICAgIHBfc3RhcnQsIHBfZW5kID0gbGluZVtpXSwgbGluZVtpICsgMV0KICAgICAgICAgICAgZHgsIGR5ID0gcF9lbmRbMF0gLSBwX3N0YXJ0WzBdLCBwX2VuZFsxXSAtIHBfc3RhcnRbMV0KICAgICAgICAgICAgYXguYXJyb3cocF9zdGFydFswXSwgcF9zdGFydFsxXSwgZHgsIGR5LCBzaGFwZT0nZnVsbCcsIGx3PTEsCiAgICAgICAgICAgICAgICAgICAgIGhlYWRfd2lkdGg9MC4wNSwgaGVhZF9sZW5ndGg9MC4xLCBjb2xvcj0nI0U2Mzk0NicsIHpvcmRlcj0yKQogICAgCiAgICAjIERyYXcgdHJhbnNmb3JtZWQgc2hhcGUgKGFpcmZvaWwpCiAgICB0aGV0YV9jaXJjbGUgPSBucC5saW5zcGFjZSgwLCAyKm5wLnBpLCAyMDApCiAgICBjaXJjbGVfcG9pbnRzID0gUiAqIG5wLmV4cCgxaiAqIHRoZXRhX2NpcmNsZSkKICAgIGFpcmZvaWxfcG9pbnRzID0gam91a293c2t5KGNpcmNsZV9wb2ludHMsIGxhbWJkYV8pCiAgICBhaXJmb2lsX3BhdGggPSBQYXRoKG5wLmNvbHVtbl9zdGFjayhbbnAucmVhbChhaXJmb2lsX3BvaW50cyksIG5wLmltYWcoYWlyZm9pbF9wb2ludHMpXSkpCiAgICBhaXJmb2lsID0gUGF0aFBhdGNoKGFpcmZvaWxfcGF0aCwgY29sb3I9JyNGQUY5RjYnLCBmaWxsPVRydWUsIHpvcmRlcj0zKQogICAgYXguYWRkX3BhdGNoKGFpcmZvaWwpCiAgICAKICAgIGF4LnNldF90aXRsZShmIkpvdWtvd3NreSBUcmFuc2Zvcm1hdGlvbiAozrs9e2xhbWJkYV86LjNmfSkiKQogICAgcmV0dXJuIFtdCgojIENyZWF0ZSBhbmltYXRpb24KYW5pID0gRnVuY0FuaW1hdGlvbihmaWcsIHVwZGF0ZSwgZnJhbWVzPWxlbihsYW1iZGFfdmFscyksIGJsaXQ9RmFsc2UsIGludGVydmFsPTEwMCkKCiMgVW5jb21tZW50IHRvIHNhdmU6CiMgYW5pLnNhdmUoJ2pvdWtvd3NreV9haXJmb2lsLmdpZicsIHdyaXRlcj0ncGlsbG93JywgZnBzPTEwKQojIE9yIHNhdmUgZnJhbWVzOgojIGZvciBpIGluIHJhbmdlKGxlbihsYW1iZGFfdmFscykpOgojICAgICB1cGRhdGUoaSkKIyAgICAgcGx0LnNhdmVmaWcoZidmcmFtZV97aTowM2R9LnBuZycsIGRwaT0xNTAsIGZhY2Vjb2xvcj0nYmxhY2snKQoKcGx0LnNob3coKQ==