{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotly interactive visualization of complex valued functions ##" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this Jupyter Notebook we generate via [Plotly](https://plot.ly), an interactive plot of a complex valued function, $f:D\\subset\\mathbb{C}\\to\\mathbb{C}$. A complex function is visualized using a version of the \n", "[domain coloring method](http://nbviewer.jupyter.org/github/empet/Math/blob/master/DomainColoring.ipynb).\n", "\n", "Compared with other types of domain coloring, the Plotly interactive plot is much more informative. It displays for each point $z$ in a rectangular region of the complex plane, \n", "not only the hsv color associated to $\\arg(f(z)$ (argument of $f(z)$), but also the values $\\arg(f(z)$ and $\\log(|f(z)|)$ (the log modulus). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First we define a Plotly hsv (hue, saturation, value) colorscale, adapted to the range of the numpy functions, `np.angle`, respectively, `np.arctan2`. \n", "We plot a Heatmap over a rectangular region in the complex plane, colored via this colorscale, according to\n", "the values of the $\\arg(f(z))$. Over the Heatmap are plotted a few contour lines of the log modulus $\\log(|f(z)|)$.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import numpy.ma as ma\n", "from numpy import pi\n", "import matplotlib.pyplot as plt\n", "import matplotlib.colors" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def hsv_colorscale(S=1, V=1): \n", " if S < 0 or S > 1 or V < 0 or V > 1:\n", " raise ValueError('Parameters S (saturation), V (value, brightness) must be in [0,1]')\n", " \n", " argument = np.array([-pi, -5*pi/6, -2*pi/3, -3*pi/6, -pi/3, -pi/6, 0, \n", " pi/6, pi/3, 3*pi/6, 2*pi/3, 5*pi/6, pi])\n", " \n", " H = argument/(2*np.pi)+1\n", " H = np.mod(H,1)\n", " Sat = S*np.ones_like(H)\n", " Val = V*np.ones_like(H)\n", " \n", " HSV = np.dstack((H, Sat, Val))\n", " RGB = matplotlib.colors.hsv_to_rgb(HSV) \n", " \n", " colormap = (255* np.squeeze(RGB)).astype(int) \n", " #Define and return the Plotly hsv colorscale adapted to polar coordinates for complex valued functions\n", " step_size = 1 / (colormap.shape[0]-1)\n", " return [[round(k*step_size, 4), f'rgb{tuple(c)}'] for k, c in enumerate(colormap)] " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pl_hsv=hsv_colorscale()\n", "pl_hsv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following two functions compute data needed for visualization:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def evaluate_function(func, re=(-1,1), im=(-1,1), N=100):\n", " # func is the complex function to be ploted\n", " #re, im are the interval ends on the real and imaginary axes, defining the rectangular region in the complex plane\n", " #N gives the number of points in an interval of length 1\n", " l = re[1]-re[0]\n", " h = im[1]-im[0]\n", " resL = int(N*l) #horizontal resolution\n", " resH = int(N*h) #vertical resolution\n", " X = np.linspace(re[0], re[1], resL)\n", " Y = np.linspace(im[0], im[1], resH)\n", " x, y = np.meshgrid(X,Y)\n", " z = x+1j*y\n", " w = func(z)\n", " argument = np.angle(w)\n", " modulus = np.absolute(w)\n", " log_modulus = ma.log(modulus)\n", " return X,Y, argument, log_modulus" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_levels(fmodul, nr=10):\n", " #define the levels for contour plot of the modulus |f(z)|\n", " #fmodul is the log modulus of f(z) computed on meshgrid\n", " #nr= the number of contour lines\n", " mv = np.nanmin(fmodul)\n", " Mv = np.nanmax(fmodul)\n", " size = (Mv-mv)/float(nr)\n", " return [mv+k*size for k in range(nr+1)]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import plotly.graph_objects as go" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We extract the contour lines from an attribute of the `matplotlib.contour.QuadContourSet` object returned by the `matplotlib.pyplot.contour`.\n", "\n", "The function defined in the next cell retrieves the points on the contour lines segments, and defines the corresponding Plotly traces:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plotly_contour_lines(contour_data):\n", " #contour_data is a matplotlib.contour.QuadContourSet object returned by plt.contour\n", " contours=contour_data.allsegs # \n", " if len(contours)==0:\n", " raise ValueError('Something wrong hapend in computing contour lines')\n", " #contours is a list of lists; if contour is a list in contours, its elements are arrays. \n", " #Each array defines a segment of contour line at some level\n", " xl = []# list of x coordinates of points on a contour line\n", " yl = []# y\n", " #lists of coordinates for contour lines consisting in one point:\n", " xp = []# \n", " yp = []\n", "\n", " for k,contour in enumerate(contours):\n", " L = len(contour)\n", " if L!= 0: # sometimes the list of points at the level np.max(modulus) is empty\n", " for ar in contour:\n", " if ar.shape[0] == 1:\n", " xp += [ar[0,0], None]\n", " yp += [ar[0,1], None]\n", " else: \n", " xl += ar[:,0].tolist()\n", " yl += ar[:,1].tolist()\n", " xl.append(None) \n", " yl.append(None)\n", "\n", " lines = go.Scatter(x=xl, \n", " y=yl, \n", " mode='lines', \n", " name='modulus', \n", " line=dict(width=1,\n", " color='#a5bab7', \n", " shape='spline', \n", " smoothing=1),\n", " hoverinfo='skip'\n", " ) \n", " if len(xp) == 0:\n", " return lines\n", " else: \n", " points = go.Scatter(x=xp, y=yp, mode='markers',\n", " marker=dict(size=4, color='#a5bab7'), \n", " hoverinfo='none')\n", " return lines, points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Set the layout of the plot:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def set_plot_layout(title, width=500, height=500):\n", " \n", " return go.Layout(title_text=title,\n", " title_x=0.5,\n", " width=width,\n", " height=height,\n", " showlegend=False,\n", " xaxis_title='Re(z)',\n", " yaxis_title='Im(z)')\n", " \n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define a function that associates to each point $z$ in a meshgrid, the strings containing the values to be displayed when hovering the mouse over that point: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def text_to_display(x, y, argum, modulus):\n", " m, n = argum.shape\n", " return [['z='+'{:.2f}'.format(x[j])+'+' + '{:.2f}'.format(y[i])+' j'+'
arg(f(z))='+'{:.2f}'.format(argum[i][j])+\\\n", " '
log(modulus(f(z)))='+'{:.2f}'.format(modulus[i][j]) for j in range(n)] for i in range(m)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, the function `plotly_plot` calls all above defined functions in order to generate the phase plot \n", "of a given complex function:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plotly_plot(f, re=(-1,1), im=(-1,1), N=50, nr=10, title='', width=500, height=500, **kwargs):\n", " x, y, argument, log_modulus=evaluate_function(f, re=re, im=im, N=N)\n", " \n", " levels = get_levels(log_modulus, nr=nr)\n", " plt.figure(figsize=(0.05,0.05))\n", " plt.axis('off')\n", " cp = plt.contour(x,y, log_modulus, levels=levels)\n", " cl = plotly_contour_lines(cp)\n", " text = text_to_display(x,y, argument, log_modulus.data)\n", " tickvals = [-np.pi, -2*np.pi/3, -np.pi/3, 0, np.pi/3, 2*np.pi/3, np.pi]\n", " \n", " #define the above values as strings with pi-unicode\n", " ticktext=['-\\u03c0', '-2\\u03c0/3', '-\\u03c0/3', '0', '\\u03c0/3', '2\\u03c0/3', '\\u03c0']\n", " \n", " dom = go.Heatmap(x=x, y=y, z=argument, colorscale=pl_hsv, \n", " text=text, hoverinfo='text',\n", " colorbar=dict(thickness=20, tickvals=tickvals, \n", " ticktext=ticktext, \n", " title='arg(f(z))'))\n", " if len(cl) == 2 and isinstance(cl[0], go.Scatter):\n", " data = [dom, cl[0], cl[1]]\n", " else: \n", " data = [dom, cl]\n", " \n", " layout = set_plot_layout(title=title) \n", " fig=go.Figure(data=data, layout =layout)\n", " return fig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As an example we take the function $f(z)=\\sin(z)/(1-cos(z^3))$: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig = plotly_plot(lambda z: np.sin(z)/(1-np.cos(z**3)), re=(-2,2), im=(-2,2), \n", " nr=22, title='$f(z)=\\\\sin(z)/(1-\\\\cos(z^3))$');\n", "fig.update_layout(xaxis_range=[-2,2], yaxis_range=[-2,2])\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import IFrame \n", "url = \"https://chart-studio.plotly.com/~empet/13957\"\n", "IFrame(url, width=700, height=700)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 2, "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()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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 }