mixedmath

Explorations in math and programming
David Lowry-Duda



Making plots of modular forms

Inspired by the images and ideas of Elias Wegert, I thought it might be interesting to attempt to implement a version of his colorizing technique for complex functions in sage. The purpose is ultimately to revisit how one plots modular forms in the LMFDB (see lmfdb.org and click around to see various plots — some are good, others are less good).   The challenge is that plotting a function from $\mathbb{C} \longrightarrow \mathbb{C}$ is that the graph is naturally 4-dimensional, and we are very bad at visualizing 4d things. In fact, we want to use only 2d to visualize it. A complex number $z = re^{i \theta}$ is determined by the magnitude ($r$) and the argument ($\theta$). Thus one typical approach to represent the value taken by a function $f$ at a point $z$ is to represent the magnitude of $f(z)$ in terms of the brightness, and to represent the argument in terms of color. For example, the typical complex space would then look like the following.
In [1]:
P = complex_plot(x, [-2, 2], [-2, 2])
P.axes(False)
P.show()
To get an idea for a slightly more complicated plot, we might look at $z^2(z-0.5)(z-0.5i)$
In [2]:
complicated_func = x**2 * (x - 0.5) * (x - 0.5*i)
In [3]:
P = complex_plot(complicated_func, [-2, 2], [-2, 2])
P.axes(False)
P.show()
Sometimes one chooses to plot only the magnitude or the argument. By discarding the argument, one can use cycling colors to represent changing magnitudes. For example, one could have red represent 0, and as magnitude increases the colors might rotate through purple, blue, green, yellow and orange, and eventually cycle to represent more magnitudes.
In [4]:
def normalize(complexval):
    return 2*exp(2 * pi * i * complexval.abs())
In [5]:
f = lambda x: normalize(x)
P = complex_plot(f, [-2, 2], [-2, 2])
P.axes(False)
P.show()
Doing this with our slightly more complicated function gives
In [6]:
f = lambda x: normalize(complicated_func(x))
P = complex_plot(f, [-2, 2], [-2, 2], plot_points=200)
P.axes(False)
P.show()
/home/davidlowryduda/sagebuild/sage2/local/lib/python2.7/site-packages/sage/misc/decorators.py:493: DeprecationWarning: Substitution using function-call syntax and unnamed arguments is deprecated and will be removed from a future release of Sage; you can use named arguments instead, like EXPR(x=..., y=...)
See http://trac.sagemath.org/5930 for details.
  return func(*args, **options)
The fact that there are three zeros in the central red blob is lost. It's not particularly illuminating. We can change this a bit by changing how often the colors cycle. Instead of cycling every time the magnitude increases by a multiple of $2\pi$, we could have it change every time the magnitude increases by a factor of $e$ (or whatever other number).
In [7]:
def normalize_log(complexval):
    return 2*exp(2 * pi * i * log(complexval.abs()))
In [8]:
f = lambda x: normalize_log(x)
P = complex_plot(f, [-2, 2], [-2, 2], plot_points=200)
P.axes(False)
P.show()
In [9]:
f = lambda x: normalize_log(complicated_func(x))
P = complex_plot(f, [-2, 2], [-2, 2], plot_points=200)
P.axes(False)
P.show()
This seems like an improvement.

Alternative Idea

As an alternative, we might consider representing the argument in color and representing magnitude through contour lines. I've done this in a separate file called myplot.spyx. Let's run this plotting function on these two examples
In [10]:
from myplot import ccomplex_plot
In [11]:
f = lambda x: x
P = ccomplex_plot(f, [-2, 2], [-2, 2], plot_points=400)
P.axes(False)
P.show()
In [12]:
P = ccomplex_plot(complicated_func, [-2, 2], [-2, 2], plot_points=400)
P.axes(False)
P.show()
This tells us something about both the argument (the colors) and the magnitude (the contour lines), while retaining lots of interesting information.

Applied to modular forms

To try it out, let's plot a modular form.
In [13]:
import numpy as np

DtoH = lambda x: (-CDF.0*x + 1)/(x - CDF.0)
Htoq = lambda x: exp(2*CDF.pi()*CDF.0*x)
Dtoq = lambda x: Htoq(DtoH(CDF(x)))

f = ModularForms(group=1, weight=12).newforms()[0].q_expansion(100).truncate()
P = ccomplex_plot(lambda x: +Infinity if abs(x) >= 0.99 else f(Dtoq(x)), (-1,1),(-1,1),
             plot_points=500, aspect_ratio = 1, figsize=[5,5])

### slightly complicated nonsense
rgbs = P[0].rgb_data
inds = np.where(np.isnan(rgbs))
rgbs[inds] = 1.
### end complicated nonsense

P.axes(show=False)
P.show()
Compare to default complex plot, to the magnitude-is-linear-color plot, and to the magnitude-is-scale-color-plot
In [14]:
P = complex_plot(lambda x: +Infinity if abs(x) >= 0.99 else f(Dtoq(x)), (-1,1),(-1,1),
             plot_points=500, aspect_ratio = 1, figsize=[5,5])
P.axes(False)
P.show()
In [15]:
ff = lambda x: normalize(Dtoq(x))
P = complex_plot(lambda x: +Infinity if abs(x) >= 0.99 else ff(x), (-1,1),(-1,1),
             plot_points=200, aspect_ratio = 1, figsize=[5,5])
P.axes(False)
P.show()
In [16]:
ff = lambda x: normalize_log(Dtoq(x))
P = complex_plot(lambda x: +Infinity if abs(x) >= 0.99 else ff(x), (-1,1),(-1,1),
             plot_points=200, aspect_ratio = 1, figsize=[5,5])
P.axes(False)
P.show()
That concludes this particular exploration. Please let me know if you have any thoughts or ideas. Thank you.

Leave a comment

Info on how to comment

To make a comment, please send an email using the button below. Your email address won't be shared (unless you include it in the body of your comment). If you don't want your real name to be used next to your comment, please specify the name you would like to use. If you want your name to link to a particular url, include that as well.

bold, italics, and plain text are allowed in comments. A reasonable subset of markdown is supported, including lists, links, and fenced code blocks. In addition, math can be formatted using $(inline math)$ or $$(your display equation)$$.

Please use plaintext email when commenting. See Plaintext Email and Comments on this site for more. Note also that comments are expected to be open, considerate, and respectful.

Comment via email

Comments (3)
  1. 2023-01-01 PD

    I see your plots here. Do you have an idea how to plot precisely this curve?

    I am simulating few mathematical things (I am not a mathematician, only engineer) and would like to identify the coordinates of the points of the curve for making a comparison with tetraetion curves. I know how to start python scripts and was using sometime matplotlib (No clue of Sage).

    No hurry in answering. This is a project I am doing in parallel to my life.

  2. 2023-01-02 DLD

    I think the easiest (where easy here means writing the least new amount of code) would be to use a library or program that implements the Dedekind eta function or the Jacobi theta functions $\theta_i$. For example, these are all in sage.

    In pure python, these are both done in mpmath.

    As a cautionary note: evaluating $\lambda(\tau)$ is nontrivial when the imaginary part of tau is very small. Or equivalently, evaluating $\lambda(iy)$ is very hard when $y$ is very small. It's possible to be clever about this (using functional equations for lambda or for the theta functions or the eta function, which are all ultimately equivalently useful here). But many implementations won't do this by default — mpmath also doesn't do this cleverly.

    Concretely, here is one implementation.

    import mpmath
    
    def mplambda(tau):
        inner = 2**.5 * mpmath.eta(tau/2) * (mpmath.eta(2*tau))**2 / (mpmath.eta(tau)**3)
        return inner**8
    
    xs = [x/300 for x in range(1, 1000)]
    ys = [float(real(mplambda(x * 1j))) for x in xs]
    scatterplot(xs, ys)  # adjusted to the plotting library you use
    

    This makes a plot roughly identical to the wikipedia plot. I bet you could adjust it from there.

  3. 2023-01-02 PD

    Thanks for the quick reply and your code! I was working already with few libs.