Posted on August 28, 2018 by Stéphane Laurent

In a previous post, I have shown how to draw some linked cyclides as unions of circles. In fact it is easy to parametrize the linked cyclides.

We show how to do so with R.

# "inverse" Hopf map
hopfinverse <- function(q, t){
1/sqrt(2*(1+q[3])) * c(q[1]*cos(t)+q[2]*sin(t),
sin(t)*(1+q[3]),
cos(t)*(1+q[3]),
q[1]*sin(t)-q[2]*cos(t))
}
# stereographic projection
stereog <- function(x){
c(x[1], x[2], x[3])/(1-x[4])
}
# rotation in 4D space (right-isoclinic)
rotate4d <- function(alpha, beta, xi, vec){
a = cos(xi)
b = sin(alpha)*cos(beta)*sin(xi)
c = sin(alpha)*sin(beta)*sin(xi)
d = cos(alpha)*sin(xi)
p = vec[1]; q = vec[2]; r = vec[3]; s = vec[4]
c(a*p - b*q - c*r - d*s,
a*q + b*p + c*s - d*r,
a*r - b*s + c*p + d*q,
a*s + b*r - c*q + d*p)
}

phi <- 1 # -pi/2 < phi < pi/2; close to pi/2 <=> big hole

f1 <- function(theta, t){
p <- c(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi))
h <- hopfinverse(p, t)
hr <- rotate4d(pi/2, 0, 1, h)
stereog(hr)
}
f2 <- function(theta, t){
p <- c(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi))
h <- hopfinverse(p, t)
hr <- rotate4d(pi/2, 2*pi/3, 1, h)
stereog(hr)
}
f3 <- function(theta, t){
p <- c(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi))
h <- hopfinverse(p, t)
hr <- rotate4d(pi/2, 4*pi/3, 1, h)
stereog(hr)
}

f1x <- Vectorize(function(u,v) f1(u,v)[1])
f1y <- Vectorize(function(u,v) f1(u,v)[2])
f1z <- Vectorize(function(u,v) f1(u,v)[3])
f2x <- Vectorize(function(u,v) f2(u,v)[1])
f2y <- Vectorize(function(u,v) f2(u,v)[2])
f2z <- Vectorize(function(u,v) f2(u,v)[3])
f3x <- Vectorize(function(u,v) f3(u,v)[1])
f3y <- Vectorize(function(u,v) f3(u,v)[2])
f3z <- Vectorize(function(u,v) f3(u,v)[3])

library(misc3d)
library(rgl)
open3d(windowRect=c(50,50,550,550))
view3d(90,0)
n <- 300
parametric3d(f1x,f1y,f1z, umin=0,umax=2*pi, vmin=0,vmax=2*pi,
n=n,
smooth=TRUE, color="chocolate")
parametric3d(f2x,f2y,f2z, umin=0,umax=2*pi, vmin=0,vmax=2*pi,
n=n,
parametric3d(f3x,f3y,f3z, umin=0,umax=2*pi, vmin=0,vmax=2*pi,
n=n,
smooth=TRUE, color="firebrick4", add=TRUE)

This code generates the following picture:

And here is a three.js drawing:

Finally, here is a beautiful Asymptote rendering:

It is generated by this code:

settings.render = 4;
settings.outformat = "pdf";

// import modules
import three;
import solids;
import palette;

// overall settings
currentprojection = orthographic(20,6,6);
viewportmargin = (10,10);
size(10cm);
currentlight = ((3,3,0));

// "inverse" Hopf map ----------------------------------------------------------
real[] hopfinverse(triple q, real t){
real f = 1/sqrt(2*(1+q.z));
real[] out = {f*(q.x*cos(t)+q.y*sin(t)),
f*sin(t)*(1+q.z),
f*cos(t)*(1+q.z),
f*(q.x*sin(t)-q.y*cos(t))};
return out;
}

// stereographic projection ----------------------------------------------------
triple stereog(real[] x){
return (x[0],x[1],x[2])/(1-x[3]);
}

// rotation in 4D space (right-isoclinic) --------------------------------------
real[] rotate4d(real alpha, real beta, real xi, real[] vec){
real a = cos(xi);
real b = sin(alpha)*cos(beta)*sin(xi);
real c = sin(alpha)*sin(beta)*sin(xi);
real d = cos(alpha)*sin(xi);
real p = vec[0];
real q = vec[1];
real r = vec[2];
real s = vec[3];
real[] out = {a*p - b*q - c*r - d*s,
a*q + b*p + c*s - d*r,
a*r - b*s + c*p + d*q,
a*s + b*r - c*q + d*p};
return out;
}

// -----------------------------------------------------------------------------
real phi = 1.2;

triple f(real theta, real t, real beta){
triple p = (cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi));
real[] h = hopfinverse(p, t);
real[] hr = rotate4d(pi/2, beta, 1, h);
return stereog(hr);
}

triple f1(pair uv){
return f(uv.x, uv.y, 0);
}
triple f2(pair uv){
return f(uv.x, uv.y, 2pi/3);
}
triple f3(pair uv){
return f(uv.x, uv.y, 4pi/3);
}

splinetype[] Notaknot = {notaknot,notaknot,notaknot};

surface s1=surface(f1,(0,0),(2pi,2pi),35,35,Notaknot,Notaknot);
draw(s1,render(merge=true));
surface s2=surface(f2,(0,0),(2pi,2pi),35,35,Notaknot,Notaknot);
draw(s2,render(merge=true));
surface s3=surface(f3,(0,0),(2pi,2pi),35,35,Notaknot,Notaknot);
draw(s3,render(merge=true));

# Update 2018-12-06

Actually the three cyclides differ from each other by a rotation around the $$y$$-axis. Thus one can calculate the first one and construct the two other ones by rotation. So we can do like this in R:

library(rgl)

# "inverse" Hopf map
hopfinverse <- function(q, t){
1/sqrt(2*(1+q[3])) * c(q[1]*cos(t)+q[2]*sin(t),
sin(t)*(1+q[3]),
cos(t)*(1+q[3]),
q[1]*sin(t)-q[2]*cos(t))
}
# stereographic projection
stereog <- function(x){
c(x[1], x[2], x[3])/(1-x[4])
}
# rotation in 4D space (right-isoclinic)
rotate4d <- function(alpha, beta, xi, vec){
a = cos(xi)
b = sin(alpha)*cos(beta)*sin(xi)
c = sin(alpha)*sin(beta)*sin(xi)
d = cos(alpha)*sin(xi)
p = vec[1]; q = vec[2]; r = vec[3]; s = vec[4]
c(a*p - b*q - c*r - d*s,
a*q + b*p + c*s - d*r,
a*r - b*s + c*p + d*q,
a*s + b*r - c*q + d*p)
}
# cyclide parameterization
f <- function(phi, theta, t){ # -pi/2 < phi < pi/2
p <- c(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi))
h <- hopfinverse(p, t)
hr <- rotate4d(pi/2, 0, 1, h)
stereog(hr)
}
# cyclide as mesh
cyclideMesh <- function(phi, nu, nv){
vs <- matrix(NA_real_, nrow=3, ncol=nu*nv)
u_ <- seq(0, 2*pi, length.out = nu+1)[-1]
v_ <- seq(0, 2*pi, length.out = nv+1)[-1]
for(i in 1:nu){
for(j in 1:nv){
vs[,(i-1)*nv+j] <- f(phi, u_[i], v_[j])
}
}
tris1 <- matrix(NA_integer_, nrow=3, ncol=nu*nv)
tris2 <- matrix(NA_integer_, nrow=3, ncol=nu*nv)
nv <- as.integer(nv)
for(i in 1L:nu){
ip1 <- ifelse(i==nu, 1L, i+1L)
for(j in 1L:nv){
jp1 <- ifelse(j==nv, 1L, j+1L)
tris1[,(i-1)*nv+j] <- c((i-1L)*nv+j,(i-1L)*nv+jp1, (ip1-1L)*nv+j)
tris2[,(i-1)*nv+j] <- c((i-1L)*nv+jp1,(ip1-1L)*nv+jp1,(ip1-1L)*nv+j)
}
}
out <- tmesh3d(
vertices = vs,
indices = cbind(tris1, tris2),
homogeneous = FALSE
)
}

# draw ####
phi <- 1.2
mesh1 <- cyclideMesh(phi, 250, 250)
n <- 3
beta_ <- seq(0, 2*pi , length.out = n+1)[-1][-n]
colors <- viridisLite::viridis(n)
open3d(windowRect=c(50,50,550,550))
view3d(0,90)
for(i in seq_along(beta_)){
shade3d(rotate3d(mesh1, beta_[i], 0, 1, 0), color = colors[i+1])
}

And like this in Asymptote:

settings.render = 4;
// import modules
import three;
import solids;
import palette;
// overall settings
currentprojection = orthographic(0, 5, 0);
viewportmargin = (10,10);
size(10cm);
currentlight = ((3,3,0));

// "inverse" Hopf map ----------------------------------------------------------
real[] hopfinverse(triple q, real t){
real f = 1/sqrt(2*(1+q.z));
real[] out = {f*(q.x*cos(t)+q.y*sin(t)),
f*sin(t)*(1+q.z),
f*cos(t)*(1+q.z),
f*(q.x*sin(t)-q.y*cos(t))};
return out;
}
// stereographic projection ----------------------------------------------------
triple stereog(real[] x){
return (x[0],x[1],x[2])/(1-x[3]);
}
// rotation in 4D space (right-isoclinic) --------------------------------------
real[] rotate4d(real alpha, real beta, real xi, real[] vec){
real a = cos(xi);
real b = sin(alpha)*cos(beta)*sin(xi);
real c = sin(alpha)*sin(beta)*sin(xi);
real d = cos(alpha)*sin(xi);
real p = vec[0];
real q = vec[1];
real r = vec[2];
real s = vec[3];
real[] out = {a*p - b*q - c*r - d*s,
a*q + b*p + c*s - d*r,
a*r - b*s + c*p + d*q,
a*s + b*r - c*q + d*p};
return out;
}
// parameterization ------------------------------------------------------------
real phi = 1.2;
triple f0(real theta, real t){
triple p = (cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi));
real[] h = hopfinverse(p, t);
real[] hr = rotate4d(pi/2, 0, 1, h);
return stereog(hr);
}
triple f(pair uv){
return f0(uv.x, uv.y);
}

// draw ------------------------------------------------------------------------
surface s = surface(f, (0,0), (2pi,2pi), 55, 55, Spline);

pen[] colors = Rainbow();

surface s1 = s;
draw(s3, render(merge=true));