Interactive Graphics - Python and Plotly

A completely different graphics engine for Python is called Plotly.   Aside from being a very slick package, the library makes the transition to using your graphics on-line as seamless as possible.  The plot below is an interactive rendering of a parametric surface in R3, a torus knot. You can zoom and rotate with your mouse.  The code to generate the plot was written in Python.  The advantage of Plotly is that when you run the code, your rendering (really, your code) is sent up to the Plotly server where it can be distributed to other user's web-browsers. You can embed the rendering in any html page as an I-frame.   Depending on how speedy the end-user's computer is, they may find the rendering below more or less pleasant. 

The big advantage of Plotly is that you can seamlessly push your graphics onto the internet.  Indeed -- the plotly site sends your entire Python app to the end-users web-browser and it will be run on the end-user's machine.  This can be slow, but it is effective for basic applications.  It also allows the end-user to not rely on your data -- their computer can compile data in real-time by ripping it from a website, for example. 

One downside to Plotly is you rely on their web-service to host your code.  You can host public code free of charge, but if you wish to share your graphics privately you will have to pay for the service.  Overall I find this quite impressive.  

One other downside is it appears Plotly lacks some flexibility -- it takes quite a bit more effort to specify how a parametric surface is coloured.  The default "colormap" option maps the colours directly from the z-coordinate. 

Plotly has an "offline" setting where you can view your graphics locally in your i-python notebook.  You can use this as much as you like without signing up for their service. 

A table of 3d-rendered (p,q)-torus knots. 

 

Comments


import sympy as sp


## symbols we need to describe p,q-torus knots
## t time parameter. p,q indexes the torus knot
## r minor radius, R major radius
spt, spp, spq, spr, spR = sp.symbols("t p q r R", real=True)

c = sp.Matrix([(spR+spr*sp.cos(2*sp.pi*spq*spt))*sp.cos(2*sp.pi*spp*spt), 
     (spR+spr*sp.cos(2*sp.pi*spq*spt))*sp.sin(2*sp.pi*spp*spt), 
      spr*sp.sin(2*sp.pi*spq*spt)])
dc = sp.Matrix([sp.diff(x,spt) for x in c]) # derivative
ldc = sp.sqrt(sum( [ x**2 for x in dc ] )).simplify() # speed
udc = dc/ldc

## 2nd order
kc = sp.Matrix([sp.diff(x,spt) for x in udc]) # curvature vector
ks = sp.sqrt(sum( [ x**2 for x in kc])) # curvature scalar
ukc = kc/ks # unit curvature vector

## bi-normal
bnc = udc.cross(ukc) # cross of unit tangent and unit curvature.

## the parametrization of the boundary of the width w tubular neighbourhood

spw, spu = sp.symbols("w, u", real=True) ## width of torus knot, and meridional parameter
kp = 3 ## these are the "p" and
kq = 2 ## "q" of our (p,q) torus knot.

tSurf = c + spw*sp.cos(2*sp.pi*(spu+kp*kq*spt))*ukc + spw*sp.sin(2*sp.pi*(spu+kp*kq*spt))*bnc

## Let's have a visualization routine that takes as input a curve and a framing of the curve.  
##  We will then plot things like a tubular neighbourhood of the curve, together with some
##  decoration on the boundary. 

import numpy as np
import itertools as it

## (a) lambdify with numpy.  This returns a 3-element list.
knotSnp = sp.lambdify((spt, spp, spq, spr, spR, spw, spu), tSurf, "numpy" )

## (b) ufuncify
from sympy.utilities.autowrap import ufuncify
knotSuf = [ufuncify([spt, spp, spq, spr, spR, spw, spu], tSurf[i]) for i in range(3)]

## (c) theano
from sympy.printing.theanocode import theano_function
knotSth = theano_function([spt,spp,spq,spr,spR,spw,spu], [tSurf],
                          dims={spt:0, spp:0, spq:0, spr:0, spR:0, spw:0, spu:0})

tR = 1.6 # major torus radius
tr = 0.6 # minor torus radius. 
kt = (np.pi*tr) / (4*kp) # knot radial thickness 2*pi*tr is circumf, and kp strands pass through so this
## should be around 2*pi*tr  would be 2*kp*kt for the knot to fill the surface, i.e kt = pi*tr / 4*kp
## make bigger or smaller depending on how much empty space one wants to see.

seg = kp*300 ## segments along length of pq torus knot. kp*120 gives a fairly smooth image.
segm = 40 ## meridional segmentation of pq torus knot. 60 is fairly smooth. 

def surf1(i,j): ## sympy raw
    return np.array(tSurf.evalf(subs={spt:float(i)/seg, spu:float(j)/segm, 
                                       spp:kp, spq:kq, spr:tr, spR:tR, spw:kt}) )
def surf2(i,j): ## lambdify
    return np.array(knotSnp(float(i)/seg, kp, kq, tr, tR, kt, float(j)/segm)).ravel()
def surf3(i,j): ## ufuncify
    return np.array([knotSuf[k](float(i)/seg, kp, kq, tr, tR, kt, float(j)/segm) for k in range(3)])
def surf4(i,j): ## theano
    return knotSth(float(i)/seg, kp, kq, tr, tR, kt, float(j)/segm).ravel()

Surf = [surf1, surf2, surf3, surf4]
SurfLabel = ["sympy.evalf", "sympy.lambdify", "ufuncify", "theano"]

k = 2 # determines which method we use to cast sympy expressions to a callable function.
surf = Surf[k]

import time as ti
start=ti.time()
xyz = np.ndarray( (seg+1, segm+1, 3) )
for i,j in it.product( range(seg+1), range(segm+1) ):
    ## put the affine reparametrization here. 
    xyz[i,j] = surf(i,j)
end=ti.time()
print(SurfLabel[k]+" mesh generation: "+str(end-start)+" seconds.", flush=True)

x = np.ndarray((seg+1,segm+1))
y = np.ndarray((seg+1,segm+1))
z = np.ndarray((seg+1,segm+1))

for i,j in it.product( range(seg+1), range(segm+1) ):
    x[i,j] = xyz[i,j,0]
    y[i,j] = xyz[i,j,1]
    z[i,j] = xyz[i,j,2]

import plotly as py
import plotly.graph_objs as go
py.offline.init_notebook_mode()## disable this to upload to webpage

MI = int(0.2*(segm+1)) ## index for meridional color segmentation
surface1 = go.Surface(x=x[0:seg+1:1, 0:MI+1:1], y=y[0:seg+1:1, 0:MI+1:1], 
                      z=z[0:seg+1:1, 0:MI+1:1], 
                     colorscale=[[0.0, 'rgb(220,0,0)'], [1.0, 'rgb(255,0,0)']])
surface2 = go.Surface(x=x[0:seg+1:1, MI:segm+1:1], y=y[0:seg+1:1, MI:segm+1:1], 
                      z=z[0:seg+1:1, MI:segm+1:1], 
                      colorscale=[[0.0, 'rgb(230,230,80)'], [1.0, 'rgb(240,240,140)']]
                     )

## todo: data=[surface1, surface2]
data = [surface1, surface2]

layout = go.Layout(
    title='({},{}) torus knot'.format(kp,kq),
    scene=dict(
        xaxis=dict(
            gridcolor='rgb(255, 255, 255)',
            zerolinecolor='rgb(255, 255, 255)',
            showbackground=True,
            backgroundcolor='rgb(230, 230,230)'
        ),
        yaxis=dict(
            gridcolor='rgb(255, 255, 255)',
            zerolinecolor='rgb(255, 255, 255)',
            showbackground=True,
            backgroundcolor='rgb(230, 230,230)'
        ),
        zaxis=dict(
            gridcolor='rgb(255, 255, 255)',
            zerolinecolor='rgb(255, 255, 255)',
            showbackground=True,
            backgroundcolor='rgb(230, 230,230)'
        )
    )
)

fig = go.Figure(data=data, layout=layout)
#py.plotly.plot(fig, filename='{}{}_torusknot'.format(kp,kq))
# py.offline.plot(fig) # renders in another window
py.offline.iplot(fig)