import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Circle, Polygon
# Parameters
U_INF = 1.0
R = 0.8
LAMBDA = 0.15
CIRCLE_CENTER = -0.1 + 0.1j
GRID_RESOLUTION = 500
N_STREAMLINES = 12
# Grid setup
x = np.linspace ( -2 , 2 , GRID_RESOLUTION)
y = np.linspace ( -2 , 2 , GRID_RESOLUTION)
X, Y = np.meshgrid ( x, y)
Z = X + 1j*Y
# Velocity field (circle)
radius = np.sqrt ( ( X - np.real ( CIRCLE_CENTER) ) **2 + ( Y - np.imag ( CIRCLE_CENTER) ) **2 )
theta = np.arctan2 ( Y - np.imag ( CIRCLE_CENTER) , X - np.real ( CIRCLE_CENTER) )
U_r = U_INF * ( 1 - ( R**2 ) / ( radius**2 ) ) * np.cos ( theta)
U_theta = -U_INF * ( 1 + ( R**2 ) / ( radius**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)
# Joukowsky transform
def joukowsky( z, lambda_= LAMBDA) :
return z + lambda_**2 / z
theta_circle = np.linspace ( 0 , 2 *np.pi , 200 )
circle_points = R * np.exp ( 1j * theta_circle) + CIRCLE_CENTER
airfoil_points = joukowsky( circle_points)
# Streamlines
start_points = np.column_stack ( [
-2 * np.ones ( N_STREAMLINES) ,
np.linspace ( -1.5 , 1.5 , N_STREAMLINES)
] )
# Animation
fig, ax = plt.subplots ( figsize= ( 10 , 6 ) )
ax.set_xlim ( -2 , 2 )
ax.set_ylim ( -2 , 2 )
ax.set_facecolor ( 'black' )
ax.set_xticks ( [ ] )
ax.set_yticks ( [ ] )
STREAMLINE_COLOR = '#E63946'
def update( frame) :
ax.clear ( )
ax.set_xlim ( -2 , 2 )
ax.set_ylim ( -2 , 2 )
lambda_ = LAMBDA * ( frame / 30 )
transformed_points = joukowsky( circle_points, lambda_)
# Fixed Circle constructor
if lambda_ == 0 :
shape = Circle( ( np.real ( CIRCLE_CENTER) , np.imag ( CIRCLE_CENTER) ) , R,
fc= '#FAF9F6' , ec= 'none' , zorder= 3 )
else :
shape = Polygon( np.column_stack ( [ np.real ( transformed_points) , np.imag ( transformed_points) ] ) ,
fc= '#FAF9F6' , ec= 'none' , zorder= 3 )
ax.add_patch ( shape)
strm = ax.streamplot ( X, Y, U, V, color= STREAMLINE_COLOR, linewidth= 1.5 ,
start_points= start_points, arrowsize= 0 , zorder= 2 )
for path in strm.lines .get_segments ( ) :
for i in range ( 0 , len ( path) - 8 , 8 ) :
p_start, p_end = path[ i] , path[ i+1 ]
ax.arrow ( p_start[ 0 ] , p_start[ 1 ] , p_end[ 0 ] -p_start[ 0 ] , p_end[ 1 ] -p_start[ 1 ] ,
shape= 'full' , lw= 1 , head_width= 0.05 , head_length= 0.1 ,
color= STREAMLINE_COLOR, zorder= 2 )
ax.set_title ( f"Joukowsky Transformation (λ={lambda_:.2f})" , color= 'white' )
ani = FuncAnimation( fig, update, frames= 30 , interval= 100 , blit= False )
plt.tight_layout ( )
plt.show ( )
aW1wb3J0IG51bXB5IGFzIG5wCmltcG9ydCBtYXRwbG90bGliLnB5cGxvdCBhcyBwbHQKZnJvbSBtYXRwbG90bGliLmFuaW1hdGlvbiBpbXBvcnQgRnVuY0FuaW1hdGlvbgpmcm9tIG1hdHBsb3RsaWIucGF0Y2hlcyBpbXBvcnQgQ2lyY2xlLCBQb2x5Z29uCgojIFBhcmFtZXRlcnMKVV9JTkYgPSAxLjAKUiA9IDAuOApMQU1CREEgPSAwLjE1CkNJUkNMRV9DRU5URVIgPSAtMC4xICsgMC4xagpHUklEX1JFU09MVVRJT04gPSA1MDAKTl9TVFJFQU1MSU5FUyA9IDEyCgojIEdyaWQgc2V0dXAKeCA9IG5wLmxpbnNwYWNlKC0yLCAyLCBHUklEX1JFU09MVVRJT04pCnkgPSBucC5saW5zcGFjZSgtMiwgMiwgR1JJRF9SRVNPTFVUSU9OKQpYLCBZID0gbnAubWVzaGdyaWQoeCwgeSkKWiA9IFggKyAxaipZCgojIFZlbG9jaXR5IGZpZWxkIChjaXJjbGUpCnJhZGl1cyA9IG5wLnNxcnQoKFggLSBucC5yZWFsKENJUkNMRV9DRU5URVIpKSoqMiArIChZIC0gbnAuaW1hZyhDSVJDTEVfQ0VOVEVSKSkqKjIpCnRoZXRhID0gbnAuYXJjdGFuMihZIC0gbnAuaW1hZyhDSVJDTEVfQ0VOVEVSKSwgWCAtIG5wLnJlYWwoQ0lSQ0xFX0NFTlRFUikpClVfciA9IFVfSU5GICogKDEgLSAoUioqMikgLyAocmFkaXVzKioyKSkgKiBucC5jb3ModGhldGEpClVfdGhldGEgPSAtVV9JTkYgKiAoMSArIChSKioyKSAvIChyYWRpdXMqKjIpKSAqIG5wLnNpbih0aGV0YSkKVSA9IFVfciAqIG5wLmNvcyh0aGV0YSkgLSBVX3RoZXRhICogbnAuc2luKHRoZXRhKQpWID0gVV9yICogbnAuc2luKHRoZXRhKSArIFVfdGhldGEgKiBucC5jb3ModGhldGEpCgojIEpvdWtvd3NreSB0cmFuc2Zvcm0KZGVmIGpvdWtvd3NreSh6LCBsYW1iZGFfPUxBTUJEQSk6CiAgICByZXR1cm4geiArIGxhbWJkYV8qKjIgLyB6Cgp0aGV0YV9jaXJjbGUgPSBucC5saW5zcGFjZSgwLCAyKm5wLnBpLCAyMDApCmNpcmNsZV9wb2ludHMgPSBSICogbnAuZXhwKDFqICogdGhldGFfY2lyY2xlKSArIENJUkNMRV9DRU5URVIKYWlyZm9pbF9wb2ludHMgPSBqb3Vrb3dza3koY2lyY2xlX3BvaW50cykKCiMgU3RyZWFtbGluZXMKc3RhcnRfcG9pbnRzID0gbnAuY29sdW1uX3N0YWNrKFsKICAgIC0yICogbnAub25lcyhOX1NUUkVBTUxJTkVTKSwKICAgIG5wLmxpbnNwYWNlKC0xLjUsIDEuNSwgTl9TVFJFQU1MSU5FUykKXSkKCiMgQW5pbWF0aW9uCmZpZywgYXggPSBwbHQuc3VicGxvdHMoZmlnc2l6ZT0oMTAsIDYpKQpheC5zZXRfeGxpbSgtMiwgMikKYXguc2V0X3lsaW0oLTIsIDIpCmF4LnNldF9mYWNlY29sb3IoJ2JsYWNrJykKYXguc2V0X3h0aWNrcyhbXSkKYXguc2V0X3l0aWNrcyhbXSkKClNUUkVBTUxJTkVfQ09MT1IgPSAnI0U2Mzk0NicKCmRlZiB1cGRhdGUoZnJhbWUpOgogICAgYXguY2xlYXIoKQogICAgYXguc2V0X3hsaW0oLTIsIDIpCiAgICBheC5zZXRfeWxpbSgtMiwgMikKICAgIAogICAgbGFtYmRhXyA9IExBTUJEQSAqIChmcmFtZSAvIDMwKQogICAgdHJhbnNmb3JtZWRfcG9pbnRzID0gam91a293c2t5KGNpcmNsZV9wb2ludHMsIGxhbWJkYV8pCiAgICAKICAgICMgRml4ZWQgQ2lyY2xlIGNvbnN0cnVjdG9yCiAgICBpZiBsYW1iZGFfID09IDA6CiAgICAgICAgc2hhcGUgPSBDaXJjbGUoKG5wLnJlYWwoQ0lSQ0xFX0NFTlRFUiksIG5wLmltYWcoQ0lSQ0xFX0NFTlRFUikpLCBSLCAKICAgICAgICAgICAgICAgICAgICAgICBmYz0nI0ZBRjlGNicsIGVjPSdub25lJywgem9yZGVyPTMpCiAgICBlbHNlOgogICAgICAgIHNoYXBlID0gUG9seWdvbihucC5jb2x1bW5fc3RhY2soW25wLnJlYWwodHJhbnNmb3JtZWRfcG9pbnRzKSwgbnAuaW1hZyh0cmFuc2Zvcm1lZF9wb2ludHMpXSksIAogICAgICAgICAgICAgICAgICAgICAgIGZjPScjRkFGOUY2JywgZWM9J25vbmUnLCB6b3JkZXI9MykKICAgIGF4LmFkZF9wYXRjaChzaGFwZSkKICAgIAogICAgc3RybSA9IGF4LnN0cmVhbXBsb3QoWCwgWSwgVSwgViwgY29sb3I9U1RSRUFNTElORV9DT0xPUiwgbGluZXdpZHRoPTEuNSwKICAgICAgICAgICAgICAgICAgICAgICAgIHN0YXJ0X3BvaW50cz1zdGFydF9wb2ludHMsIGFycm93c2l6ZT0wLCB6b3JkZXI9MikKICAgIAogICAgZm9yIHBhdGggaW4gc3RybS5saW5lcy5nZXRfc2VnbWVudHMoKToKICAgICAgICBmb3IgaSBpbiByYW5nZSgwLCBsZW4ocGF0aCkgLSA4LCA4KToKICAgICAgICAgICAgcF9zdGFydCwgcF9lbmQgPSBwYXRoW2ldLCBwYXRoW2krMV0KICAgICAgICAgICAgYXguYXJyb3cocF9zdGFydFswXSwgcF9zdGFydFsxXSwgcF9lbmRbMF0tcF9zdGFydFswXSwgcF9lbmRbMV0tcF9zdGFydFsxXSwKICAgICAgICAgICAgICAgICAgICBzaGFwZT0nZnVsbCcsIGx3PTEsIGhlYWRfd2lkdGg9MC4wNSwgaGVhZF9sZW5ndGg9MC4xLCAKICAgICAgICAgICAgICAgICAgICBjb2xvcj1TVFJFQU1MSU5FX0NPTE9SLCB6b3JkZXI9MikKICAgIAogICAgYXguc2V0X3RpdGxlKGYiSm91a293c2t5IFRyYW5zZm9ybWF0aW9uICjOuz17bGFtYmRhXzouMmZ9KSIsIGNvbG9yPSd3aGl0ZScpCgphbmkgPSBGdW5jQW5pbWF0aW9uKGZpZywgdXBkYXRlLCBmcmFtZXM9MzAsIGludGVydmFsPTEwMCwgYmxpdD1GYWxzZSkKcGx0LnRpZ2h0X2xheW91dCgpCnBsdC5zaG93KCk=