10

I would like to know if there is an (easy?) way of plotting the Cantor function (devil's staircase) using LaTeX.

Doing it manually with TikZ seems like madness somehow and my knowledge of plotting using TikZ is also very limited. With that being said, I have seen it plotted many times in math scripts so how is it done? Does anyone know?

Here is a reference to the Cantor function.

14

I advise for an external solution, too. But surely, it is possible in TeX. It just takes a bit of time.

As this is a recursive solution, it might take more time than a non-recursive, but the recursive one is relative easy to implement.

If one defines cantor 2 edge/.style={move to} the diagonal part will not be drawn. (It's not an edge in an TikZ path operator kind of way.)

You start your path as usual with \draw and whatever options you want and then insert as another option:

cantor start={<lower x>}{<upper x>}{<lower y>}{<upper y>}{<level>}

There are the value keys

  • /tikz/lower cantor and /tikz/upper cantor, as well as
  • /tikz/y cantor.

I don't know how much sense the y cantor value makes so I added it as a “fun” definition. In the proper staircase definition y cantor equals 0.5. (However, then I’d use the definition marked as such.)

Code

\documentclass[tikz]{standalone}
\tikzset{
  if/.code n args=3{\pgfmathparse{#1}\ifnum\pgfmathresult=0
    \pgfkeysalso{#3}\else\pgfkeysalso{#2}\fi},
  lower cantor/.initial=.3333, upper cantor/.initial=.6667, y cantor/.initial=.5,
  declare function={
    cantor_l(\lowerBound,\upperBound)=
      (\pgfkeysvalueof{/tikz/lower\space cantor})*(\upperBound-\lowerBound)+\lowerBound;
    cantor_u(\lowerBound,\upperBound)=
      (\pgfkeysvalueof{/tikz/upper\space cantor})*(\upperBound-\lowerBound)+\lowerBound;
    cantor(\lowerBound,\upperBound)=% fun definition
      (\pgfkeysvalueof{/tikz/y\space cantor})*(\upperBound-\lowerBound)+\lowerBound;},
  cantor start/.style n args=5{%
    insert path={(#1,#3)},
    cantor={#1}{#2}{#3}{#4}{#5}{0},
    insert path={to[every cantor edge/.try, cantor 1 edge/.try] (#2,#4)}},
  cantor/.style n args=6{%
    /utils/exec=%
      \pgfmathsetmacro\lBx{cantor_l(#1,#2)}%
      \pgfmathsetmacro\uBx{cantor_u(#1,#2)}%
%      \pgfmathsetmacro\y{.5*(#3+#4)},% proper definition
      \pgfmathsetmacro\y{cantor(#3,#4)},% fun
    style/.expanded={
      if={#6<#5}{cantor={#1}{\lBx}{#3}{\y}{#5}{#6+1}}{},
      insert path={
        to[every cantor edge/.try, cantor 1 edge/.try] (\lBx,\y)
        to[every cantor edge/.try, cantor 2 edge/.try] (\uBx,\y)},
      if={#6<#5}{cantor={\uBx}{#2}{\y}{#4}{#5}{#6+1}}{}}}}
\begin{document}
\foreach \level in {0,...,5}{
\begin{tikzpicture}[line join=round] % cantor 1 edge/.style={move to}
  \useasboundingbox[draw, scale=6, help lines]
    (0,0) grid[xstep=1/9, ystep=.25] (1,1);
  \draw[thick, cantor start={0}{6}{0}{6}{\level}{0}];
\end{tikzpicture}}
\foreach \val[evaluate={\lc=1/\val;\uc=(\val-1)/\val;}] in {2,...,9}{
\begin{tikzpicture}[line join=round, lower cantor=\lc, upper cantor=\uc]
%  \useasboundingbox[draw, scale=6, help lines]
%    (0,0) grid[xstep=\lc*\lc, ystep=.25] (1,1);
  \draw[thick, cantor start={0}{6}{0}{6}{6}{0}];
  \node [anchor=north west] at (0,6) {$\frac1\val$};
\end{tikzpicture}}
\foreach \val in {1,...,9}{
\begin{tikzpicture}[line join=round, y cantor=.\val, cantor 1 edge/.style={move to}]
  \draw[thick, cantor start={0}{6}{0}{6}{6}{0}];
  \node[anchor=north west] at (0,6) {$.\val$};
\end{tikzpicture}}
\end{document}

Output

enter image description here enter image description hereenter image description here

  • @Jake Thanks. I added an updated version. Though, looking at the definition of the Cantor function on Wikipedia it seems my addition is not useful in any way. – Qrrbrbirlbel May 1 '15 at 21:35
11

I'd love to see someone do this directly in LaTeX, but in the meantime, it's probably easiest to generate the data using some other method and plot the resulting data file.

Here's a Python script that uses the cantor function from https://stackoverflow.com/a/17810389/1456857 to generate the data file:

def cantor(n):
    return [0.] + cant(0., 1., n) + [1.]

def cant(x, y, n):
    if n == 0:
        return []

    new_pts = [2.*x/3. + y/3., x/3. + 2.*y/3.]
    return cant(x, new_pts[0], n-1) + new_pts + cant(new_pts[1], y, n-1)

x = np.array(cantor(5))
y = np.cumsum( np.ones(len(x))/(len(x)-2) ) - 1./(len(x)-2)
y[-1] = 1

np.savetxt('cantor.dat', np.vstack([x,y]).T)

The cantor.dat file can then be plotted using PGFPlots:

\documentclass{standalone}
\usepackage{pgfplots}
\begin{document}
    \begin{tikzpicture}
        \begin{axis}
        \addplot [const plot] table {cantor.dat};
        \end{axis}
    \end{tikzpicture}
\end{document}

9

Here is a first attempt with MetaPost, with a number of iterations n to be chosen at will, 10 on this example.

It surely needs refinement, but it shows anyway it can be done with it. To be typeset with LuaLaTeX.

Edit A second, recursive implementation, much more refined.

vardef cantor(expr A, B, n) =
  if n = 0 : A -- B
  else: 
    save C, D; pair C, D; 
    C = (1/3[xpart A, xpart B], .5[ypart A, ypart B]); 
    D = (2/3[xpart A, xpart B], ypart C);
    cantor(A, C, n-1) -- cantor(D, B, n-1)
  fi
enddef;

Now n is taken equal to 15 in the subsequent application, but I guess it could be a deal more with this implementation.

Edit bis For better presentation I have added a grid similar to Qrrbrbirlbel's one.

\documentclass[border=2mm]{standalone}
\usepackage{amsmath, luamplib}
  \mplibtextextlabel{enable}
  \mplibnumbersystem{double}
\begin{document}
  \begin{mplibcode}
    u  := 10cm;
    vardef cantor(expr A, B, n) =
      if n = 0 : A -- B
      else: 
        save C, D; pair C, D; 
        C = (1/3[xpart A, xpart B], .5[ypart A, ypart B]); 
        D = (2/3[xpart A, xpart B], ypart C);
        cantor(A, C, n-1) -- cantor(D, B, n-1)
      fi
    enddef;
    beginfig(1);
      % Grid and axes
      for i = 1 upto 9: draw (i*u/9, 0) -- (i*u/9, u) withcolor .8white; endfor
      for j = 1 upto 4: draw (0, j*u/4) -- (u, j*u/4) withcolor .8white; endfor
      drawarrow origin -- (1.1u, 0); drawarrow origin -- (0, 1.1u);
      % The function
      draw cantor(origin, (1, 1), 15) scaled u;
      % labels
      label.llft("$O$", origin); label.bot("$1$", (u, 0)); label.lft("$1$", (0, u));
      label.bot("$x$", (1.1u, 0)); label.lft("$y$", (0, 1.1u));
      label.bot("$\dfrac{1}{3}$", (1/3u, 0)); label.bot("$\dfrac{2}{3}$", (2/3u, 0));
      label.lft("$\dfrac{1}{2}$", (0, .5u));
    endfig;
  \end{mplibcode}
\end{document}

enter image description here

  • As a complementary information, I have just managed to make this code run for n=23. So the smallest “staircase step” of this graph is 3^-23 of length and 2^-23 of heigth. It took a few hours (!) for my iMac Intel Core i5 (2.7 GHz) to produce the result. The PDF file weighs 25 Mo. It do not seem very sensible to go further than this :-) – Franck Pastor May 2 '15 at 18:47
7

A sagetex solution using the (free) SageMathCloud for access to the computer algebra system Sage rather than installing Sage locally on your computer. There's probably a simpler and better way to implement it; I'm not known for my programming skills. I get up to 9 iterations of construction; Sage is literally plotting a line segment at the appropriate place. The code's long, too, because I wanted it integrated with a "nice" Cartesian plane (that can be easily modified). Just copy/paste the following code into a LaTeX file on Sagemath Cloud:

\documentclass{standalone}
\usepackage{sagetex}
\usepackage[usenames,dvipsnames]{xcolor}
\usepackage{pgfplots}
\pagestyle{empty}
\begin{document}
\begin{sagesilent}
LowerX = 0
UpperX = 1
LowerY = 0
UpperY = 1
step = .01
Scale = 1.2
xscale=1
yscale=1

output = r""
output += r"\begin{tikzpicture}[scale=%f]"%(Scale)
output += r"\begin{axis}["
output += r"    grid = none,"
output += r"minor tick num=4,"
output += r"every major grid/.style={Red!30, opacity=1.0},"
output += r"every minor grid/.style={ForestGreen!30, opacity=1.0},"
output += r"height= %f\textwidth,"%(yscale)
output += r"width = %f\textwidth,"%(xscale)
output += r"thick,"
output += r"black,"
output += r"scale=%f,"%(Scale)
output += r"axis lines=center,"
output += r"domain=%f:%f"%(LowerX,UpperX)
output += r"samples=500,"
output += r"line join=bevel,"
output += r"xmin=%f,xmax=%f,ymin= %f,ymax=%f]"%(LowerX,UpperX,LowerY, UpperY)
N=[1,2]
elements = 2
fx = .5
den = 3
output += r"\addplot[NavyBlue,domain=%s:%s]{%s};"%(N[0]/den,N[1]/den,fx)
Iter = 9
for i in range(2,Iter):
    for j in range(elements/2, elements,2):
        if elements==2:
            N += [den*N[j]+1]
            N += [(den)*N[j]+2]
        else:
            N += [den*N[j]-2]
            N += [den*N[j]-1]
            N += [(den)*N[j+1]+1]
            N += [(den)*N[j+1]+2]
    elements *= 2
    fx = fx/2
    for k in range(0,len(N)-1,2):
        output += r"\addplot[NavyBlue,domain=%s:%s]{%s};"%(N[k]/(den^i),N[k+1]/(den^i),(k+1)*fx)
output += r"\end{axis}"
output += r"\end{tikzpicture}"
\end{sagesilent}
\sagestr{output}
\end{document}

Which results in this output: enter image description here

5

An Asymptote MWE:

// Cantor.asy    

settings.tex="pdflatex";
import graph;
real w=8cm,h=w; size(w,h);
import fontsize;defaultpen(fontsize(9pt));
texpreamble("\usepackage{lmodern}");     
xaxis(0,1,RightTicks(Step=0.20,step=0.1));
yaxis(0,1,LeftTicks (Step=0.20,step=0.1));
real eps=1e-10;
real fn (real x,int n){
  real u;
  if(n>0){
    if(0   <=x && x<=1/3) u=0.5fn(3x,n-1);
    if(1/3 < x && x<=2/3) u=0.5;
    if(2/3 < x && x<=1  ) u=0.5*(1+fn(3x-2,n-1));
  }else u=x;
  return u;
}
real f (real x){
  real u, v; int n=1;
  u=fn(x,0); v=fn(x,n);
  while(abs(u-v)>eps){ ++n; u=v; v=fn(x,n);}
  return u;
}
draw(graph(f,0,1,n=1000),darkblue+0.6bp+linejoin(0));
shipout(bbox(Fill(paleyellow)));

// To get Cantor.pdf, run
// asy Cantor.asy
//

enter image description here

2

Another strategy is to generate directly tikz code e.g. in Python (and to use symbolic variables rather than floating point ones for a nicer source). First generate the data:

import sympy as sy
S = sy.S

def single_patch(points, values):
    basis_points = [S('0'), S('1/3'), S('2/3'), S('1')]
    basis_values = [S('0'), S('1/2'), S('1/2'), S('1')]

    new_points = [points[0] + t * (points[1] - points[0]) for t in basis_points]
    new_values = [values[0] + t * (values[1] - values[0]) for t in basis_values]
    return new_points, new_values

def apply_patches(prev_points, prev_values):
    new_points = []
    new_values = []
    n = len(prev_points)+1
    for k in range(0, n//2):
        dp, dv = single_patch(prev_points[2*k:2*k+2], prev_values[2*k:2*k+2])
        new_points = new_points + dp
        new_values = new_values + dv
    return new_points, new_values

def cantor(n):
    p = [S('0'), S('1')]
    v = [S('0'), S('1')]
    for k in range(n):
        p, v = apply_patches(p, v)
    return(p, v)

p, v = cantor(3)

In Jupyter notebook one can plot this using

%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(p,v)

Once the data is generated we can create latex source (with some additional decorations, which can also be partially automated):

tex = r"""\documentclass[tikz,margin=2pt]{standalone}
\usetikzlibrary{decorations.pathreplacing}
\begin{document}
\begin{tikzpicture}[scale=6]
\draw[-latex] (0,0) -- (1,0) node[above left]{$x$};
\draw[-latex] (0,0) -- (0,1) node[below right]{$y$};
"""
tmp1 = r"\draw[%(style)s] (%(x)s,%(fx)s) -- (%(y)s,%(fy)s);" + '\n'
tmp2 = \
r"""\draw [decorate,decoration={brace,amplitude=1.5pt,mirror,raise=1pt}]
(%(x)s,%(fx)s) -- (%(y)s,%(fx)s) node[midway,yshift=-10pt]{$\frac{1}{3^n}$};
\draw [decorate,decoration={brace,amplitude=1.5pt,mirror,raise=1pt}]
(%(y)s,%(fx)s) -- (%(y)s,%(fy)s) node[midway,xshift=10pt]{$\frac{1}{2^n}$};
"""

for k in range(len(p)-1):
    style = 'blue' if k % 2 == 0 else 'gray, dotted'
    var = {'x': p[k], 'fx': v[k], 'y': p[k+1], 'fy': v[k+1], 'style': style}
    tex += (tmp1 % var)
    if k==6:
        tex += (tmp2 % var)

tex += \
r"""\end{tikzpicture}
\end{document}
"""

with open('output.tex', 'w') as f:
    f.write(tex)

Here is an example of the resulting image:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.