{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Animating streamlines of the flow past a rotating Joukowski airfoil ##" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The streamlines of the flow past a Joukowski airfoil are generated following this [chapter](http://brennen.caltech.edu/fluidbook/basicfluiddynamics/potentialflow/Complexvariables/joukowskiairfoils.pdf) from the Internet Book of Fluid Dynamics.\n", "\n", "\n", "Visualization of streamlines is based on the property of the complex flow\n", "with respect to a conformal transformation:\n", " \n", "If w is the complex plane of the airfoil, z is the complex plane of the circle, as the section in a circular cylinder,\n", "and $w=w(z)$ is a conformal tranformatiom mapping the circle to the airfoil,\n", "then the complex flow, $F$, past the airfoil is related to the complex flow, $f$, past the circle(cylinder) by:\n", "$F(w)=f(z(w))$ or equivalently $F(w(z))=f(z)$. \n", "\n", "The streamlines of each flow are defined as contour plots of the imaginary part of the complex flow.\n", "In our case, due to the latter relation, we plot the contours of $Imag{(f)}$ over $w(z)$, where $w(z)$ is the Joukowski transformation, that maps a suitable circle onto the airfoil." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Algorithm to generate a frame:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "m- Choose the center of the circle as in the Internet Book, but with respect to a new coordinate $\\tilde{z}$, $\\tilde{z}=e^{i \\alpha}z$, i.e\n", " $\\tilde{z}_c=\\lambda-R e^{i\\beta}$ . \n", " \n", "- The Joukowski transformation $\\tilde{J}(\\tilde{z})=\\tilde{z}+\\lambda^2/\\tilde{z}=\\tilde{w}$ maps this circle to an airfoil referenced to the coordinate $\\tilde{w}=e^{i \\alpha}w$.\n", " \n", "- The circle center has the coordinate $z_c=e^{-i\\alpha}\\tilde{z}_c$, in the z-complex plane.\n", "\n", "- The Joukowski transformation from the complex plane z, to w, is conjugated via a rotation with the Joukowski tranformation, \n", " $\\tilde{J}$, i.e. $J(z)= e^{-i \\alpha} \\underbrace{\\tilde{J}(e^{i \\alpha}z)}_{\\tilde{w}}$\n", "\n", "Hence, $J(z)=z+\\lambda^2e^{-2 i\\alpha}/z$\n", "\n", "Each animation frame displays the flow streamlines of angle of attack 0 with respect to the z-coordinate.\n", "Due to the inclination of the airfoil the corresponding streamlines are in fact the streamlines of the flow expressed in the $\\tilde{z}$-plane, with angle of attack $\\alpha$." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import plotly.graph_objects as go\n", "import numpy as np\n", "import numpy.ma as ma\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def J(z, lam, alpha):\n", " return z+(np.exp(-1j*2*alpha)*lam**2)/z" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def circle(C, R):\n", " t = np.linspace(0,2*np.pi, 200)\n", " return C + R*np.exp(1j*t)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def deg2radians(deg):\n", " return deg*np.pi/180" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def flowlines(alpha=10, beta=5, V_inf=1, R=1, ratio=1.2):\n", " #alpha, beta are given in degrees\n", " #ratio =R/lam\n", " alpha = deg2radians(alpha)# angle of attack\n", " beta = deg2radians(beta)# -beta is the argument of the complex no (Joukowski parameter - circle center)\n", " if ratio <= 1: #R/lam must be >1\n", " raise ValueError('R/lambda must be > 1')\n", " lam = R/ratio#lam is the parameter of the Joukowski transformation\n", " \n", " center_c = np.exp(-1j*alpha)*(lam-R*np.exp(-1j*beta))\n", "\n", " Circle = circle(center_c,R)\n", " Airfoil = J(Circle, lam, alpha)\n", " X = np.arange(-3, 3, 0.1)\n", " Y = np.arange(-3, 3, 0.1)\n", "\n", " x,y = np.meshgrid(X, Y)\n", " z = x+1j*y\n", " z = ma.masked_where(np.absolute(z-center_c)<=R, z)\n", " w = J(z, lam, alpha)\n", " beta = beta+alpha\n", " Z = z-center_c\n", "\n", " Gamma = -4*np.pi*V_inf*R*np.sin(beta)#circulation\n", " U = np.zeros(Z.shape, dtype=np.complex)\n", " with np.errstate(divide='ignore'):#\n", " for m in range(Z.shape[0]):\n", " for n in range(Z.shape[1]):# due to this numpy bug https://github.com/numpy/numpy/issues/8516\n", " #we evaluate this term of the flow elementwise\n", " U[m,n] = Gamma*np.log((Z[m,n])/R)/(2*np.pi)\n", " c_flow = V_inf*Z + (V_inf*R**2)/Z - 1j*U #the complex flow \n", " \n", " \n", " return w, c_flow.imag, Airfoil" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def get_contours(mplcont):\n", " conts = mplcont.allsegs # get the segments of line computed via plt.contour\n", " xline = []\n", " yline = []\n", "\n", " for cont in conts:\n", " if len(cont) != 0:\n", " for arr in cont: \n", " \n", " xline += arr[:,0].tolist()\n", " yline += arr[:,1].tolist()\n", " xline.append(None) \n", " yline.append(None)\n", "\n", " return xline, yline" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAAMElEQVQ4jWP8//8/A6WAiWITRg2hnSEs+CQZGRng8f//PwMjTV1CFUMYR1PscDcEAENACR3GtB1dAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "levels = np.arange(-3, 3.7, 0.25).tolist()\n", "plt.figure(figsize=(0.05,0.05))\n", "plt.axis('off')\n", "Alpha= list(range(0, 19)) + list(range(17, -19, -1)) + list(range(-17, 1))\n", "\n", "frames = []\n", "\n", "for k, alpha in enumerate(Alpha):\n", " Jz, stream_func, Airfoil = flowlines(alpha=alpha)\n", " #Define an instance of the mpl class contour\n", " cp = plt.contour(Jz.real, Jz.imag, stream_func, levels=levels, colors='blue')\n", " \n", " xline, yline = get_contours(cp)\n", "\n", " frames.append( go.Frame(data=[go.Scatter(x=xline, \n", " y=yline),\n", " go.Scatter(x=Airfoil.real, \n", " y=Airfoil.imag),\n", " ],\n", " traces=[0, 1],\n", " name=f'frame{k}' \n", " ) )" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "data = [go.Scatter( \n", " x=frames[0].data[0].x,\n", " y=frames[0].data[0].y,\n", " mode='lines',\n", " line=dict(color='blue', width=1),\n", " name=''\n", " ),\n", " go.Scatter(\n", " x=frames[0].data[1].x,\n", " y=frames[0].data[1].y,\n", " mode='lines',\n", " line=dict(color='blue', width=2),\n", " name=''\n", " )\n", " ]" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def get_sliders(Alpha, n_frames, fr_duration=100, x_pos=0.0, y_pos=0, slider_len=1.0):\n", " # n_frames= number of frames\n", " #fr_duration=duration in milliseconds of each frame\n", " #x_pos x-coordinate where the slider starts\n", " #slider_len is a number in (0,1] giving the slider length as a fraction of x-axis length \n", " return [dict(steps= [dict(method= 'animate',#Sets the Plotly method to be called when the\n", " #slider value is changed.\n", " args= [ [ f'frame{k}'],#Sets the arguments values to be passed to \n", " #the Plotly method set in method on slide\n", " dict(mode= 'immediate',\n", " frame= dict( duration=fr_duration, redraw= False ),\n", " transition=dict( duration= 0)\n", " )\n", " ],\n", " label=str(alpha)\n", " ) for k, alpha in enumerate(Alpha)], \n", " transition= dict(duration= 0 ),\n", " x=x_pos,\n", " y=y_pos, \n", " currentvalue=dict(font=dict(size=12), \n", " prefix=\"Angle of attack: \", \n", " visible=True, \n", " xanchor= \"center\"\n", " ), \n", " len=slider_len)\n", " ]" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "axis = dict(showline=True, zeroline=False, ticklen=4, mirror=True, showgrid=False)\n", "\n", "\n", "layout = go.Layout(title_text=\"Streamlines of a flow past a rotating Joukowski airfoil\",\n", " title_x=0.5,\n", " font=dict(family='Balto'),\n", " showlegend=False, \n", " autosize=False, \n", " width=600, \n", " height=600, \n", " xaxis=dict(axis, **{'range': [ma.min(Jz.real), ma.max(Jz.real)]}),\n", " yaxis=dict(axis, **{'range':[ma.min(Jz.imag), ma.max(Jz.imag)]}),\n", " \n", " plot_bgcolor='#c1e3ff',\n", " hovermode='closest',\n", " \n", " updatemenus=[dict(type='buttons', showactive=False,\n", " y=1,\n", " x=1.15,\n", " xanchor='right',\n", " yanchor='top',\n", " pad=dict(t=0, l=10),\n", " buttons=[dict(\n", " label='Play',\n", " method='animate',\n", " args=[None, dict(frame=dict(duration=50, redraw=False), \n", " transition=dict(duration=0),\n", " fromcurrent=True,\n", " mode='immediate')])]\n", " )\n", " ])\n", "\n", "layout.update(sliders=get_sliders(Alpha, len(frames), fr_duration=50))\n", "fig = go.Figure(data=data,layout=layout, frames=frames)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import chart_studio.plotly as py\n", "py.iplot(fig, filename='streamlJouk1485353936.44' )" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.core.display import HTML\n", "def css_styling():\n", " styles = open(\"./custom.css\", \"r\").read()\n", " return HTML(styles)\n", "css_styling()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 1 }