Profinite Graphs

Profinite Graph

Let \(F: \hat{\ZZ} \to \hat{\ZZ}\) be a function and let \(P\) be a ProfiniteFunction implementing \(F\). This file implements interactive profinite graphs in the class ProfiniteGraph: a user can view arbitrarily high precision approximations of the graph \(\{(x, F(x)) \mid x \in \hat{\ZZ}\}\) of \(F\) by zooming in and out.

The idea of these kind of profinite graphs is taken from the paper [Len2005] (see below), which contains an image of such a graph for the profinite Fibonacci function.

We use the class ProfiniteIntegers as our implementation of \(\hat{\ZZ}\) and by a profinite integer we shall mean an instance of Zhat, i.e. ProfiniteIntegers(QQ). Also by 3 mod 6 we shall mean the profinite integer Zhat(3, 6).

In this file we use the character ‘!’ to mean factorial (e.g. \(4!\) is \(24\)).

Factorial digits

Every \(\alpha \in \hat{\ZZ}\) is uniquely determined by its factorial digits, which are the unique integers \(d_1, d_2, d_3, d_4, ... \in \ZZ\) satisfying \(0 \leq d_k \leq k\) and \(\alpha \equiv \sum_{i=1}^k d_i \cdot i! \mod (k+1)!\hat{\ZZ}\) for every \(k \in \ZZ_{\geq 1}\). Knowing the first \(k\) factorial digits of \(\alpha\) corresponds to knowing \(\alpha\) modulo \((k+1)!\). These factorial digits are implemented in Zhat:

sage: a = Zhat([1, 2, 1, 1]); a
35 mod 120
sage: print(a.str(style='factorial'))
1*1! + 2*2! + 1*3! + 1*4! + O(5!)
sage: a.factorial_digits()
[1, 2, 1, 1]
sage: b = Zhat([1, 2, 1, 1, 0, 0]); b
35 mod 5040
sage: print(b.str(style='factorial'))
1*1! + 2*2! + 1*3! + 1*4! + O(7!)
sage: b.factorial_digits()
[1, 2, 1, 1, 0, 0]

The visualization function

We define the visualization function to be the map \(\phi: \hat{\ZZ} \to [0, 1]\) (where \([0, 1] \subset \RR\) denotes the unit interval) defined by \(\phi(\alpha) = \sum_{i=1}^\infty d_i / (i+1)!\) for \(d_i\) the factorial digits of \(\alpha\).

It is illustrative to verify the equalities \(\phi(1+2\hat{\ZZ}) = [1/2, 1]\) and \(\phi(2+3\hat{\ZZ}) = [1/6, 1/3] \cup [5/6, 1]\). Can you determine \(\phi(8+24\hat{\ZZ})\)?

The visualization function enables us to visualize the ring \(\hat{\ZZ}\) as the unit interval and \(\hat{\ZZ} \times \hat{\ZZ}\) as the unit square. It is implemented in Zhat:

sage: Zhat(1, 2).visual()
(1/2, 1)
sage: Zhat(2, 3).visual()
(1/6, 1)
sage: a.visual()
(53/60, 107/120)
sage: b.visual()
(53/60, 4453/5040)

Graphing a profinite function

Let \(k\) be a positive integer which we call our precision. Then we can draw the graph of a function \(F: \hat{\ZZ} \to \hat{\ZZ}\) at precision \(k\) as follows. For each profinite integer \(x\) of modulus \(k!\), we compute the image of the represented subset of \(x\) under \(F\) and we take all profinite integers \(y\) of modulus \(k!\) whose represented subset intersects this image. Each of these profinite integers \(x\) and \(y\) are mapped by the visualization function to a closed interval. Hence we can draw the points \((x, y)\) as squares in the unit interval. This is precisely what the class ProfiniteGraph does. See the documentation of that class for examples.

REFERENCES:

[Her2021] Mathé Hertogh, Computing with adèles and idèles, master’s thesis, Leiden University, 2021.

[Len2005] Hendrik Lenstra, Profinite Fibonacci numbers, Niew Archief voor Wiskunde, 5/6(4):297-300, december 2005. http://www.nieuwarchief.nl/serie5/pdf/naw5-2005-06-4-297.pdf

This implementation is based on and part of [Her2021]. For details, see Chapter 7 of [Her2021]. The idea of these kinds of graphs is taken from [Len2005].

AUTHORS:

  • Mathé Hertogh (2021-01-15): initial version

class adeles.profinite_graph.ProfiniteGraph(function)

Bases: object

Interactive graph of a function from and to profinite integers

Every ProfiniteFunction can be graphed, as well as callables that behave like such profinite functions (see below for examples).

The graph allows the user to zoom in (and back out) to arbitrary high precisions. Left-click on an area in the graph to zoom into that area. Right-click anywhere on the graph to zoom out.

The profinite integers are drawn based on the visual() method of profinite integers. So \(\hat{\ZZ} \times \hat{\ZZ}\) is identified with the unit square \([0,1] \times [0,1]\) and a pair of profinite integers (x mod m, y mod n) is identified with a rectangle \([a,b] \times [c,d]\) in the unit square.

The idea of this kind of graph is taken from the paper “Profinite Fibonacci numbers” by Hendrik Lenstra, cf. [Len2005]. Some default settings such as the colors to draw in are taken such that the resulting graph looks like the picture in that paper.

Warning

This class uses the standard Python interface package tkinter for its graphical interface. Make sure that tkinter is installed (which should already be the case on Windows and most Unix systems) and that the command import tkinter works.

EXAMPLES:

We plot the graph of the profinite Fibonacci function:

sage: g = ProfiniteGraph(ProfiniteFibonacci())
sage: g.set_window_sizes(720, 720)
sage: g.set_title("Graph of the profinite Fibonacci function")
sage: g.plot() # optional - tkinter

This produces the following image:

_images/Fibonacci_graph_0mod1x0mod1.png

What do we see here? Lets first look only at the big orange blocks. Above 3 mod 6 on the \(x\)-axis, there are two orange blocks, which on the \(y\)-axis represent 2 mod 6 and 4 mod 6. This means that the image of \(3 + 6 \cdot \hat{\ZZ}\) under the Fibonacci function lies in \((2 + 6 \cdot \hat{\ZZ}) \cup (4 + 6 \cdot \hat{\ZZ})\). It also means that both 2 mod 6 and 4 mod 6 are “hit” by 3 mod 6: \(F(3 + 6 \cdot \hat{\ZZ}) \cap (2 + 6 \cdot \hat{\ZZ}) \neq \emptyset\) and \(F(3 + 6 \cdot \hat{\ZZ}) \cap (4 + 6 \cdot \hat{\ZZ}) \neq \emptyset\).

The orange blocks represent the approximation of the graph of the profinite Fibonacci function of precision 3: it computes the images of the profinite integers of modulus \(3!\). In pink, the approximation of precision 4 is drawn. Hence the pink rectangles all represents subsets of \(\hat{\ZZ} \times \hat{\ZZ}\) of the form \((x+4!\hat{\ZZ}) \times (y+4!\hat{\ZZ})\).

By left clicking we zoom in to the area \((3+6\hat{\ZZ}) \times (2+6\hat{\ZZ})\), i.e. (3 mod 6) x (2 mod 6):

_images/Fibonacci_graph_3mod6x2mod6.png

Now we see that within \(3+6\hat{\ZZ}\), the open subsets \(3+24\hat{\ZZ}\) and \(21+24\hat{\ZZ}\) are actually mapped to \(2+24\hat{\ZZ}\). Apparently, the rest of \(3+6\hat{\ZZ}\) (i.e. \(9+24\hat{\ZZ}\) and \(15+24\hat{\ZZ}\)) is mapped into \(4+6\hat{\ZZ}\). One could see this by right-clicking anywhere on the graph to zoom out and than zooming in to the area (3 mod 6) x (4 mod 6).

Now within the pink squares, we see brown squares. This is the precision 5 approximation. From this picture one can already see that 3 mod 120 is mapped to 2 mod 120 and that 27 mod 120 is mapped to 98 mod 120. But a user can also zoom in again to see this more clearly of course.

Any ProfiniteFunction can be graphed. We can even graph any callable that behaves like a profinite function. Here is an example of how to produce a graph of the square function \(\hat{\ZZ} \to \hat{\ZZ}, x \mapsto x^2\):

sage: graph = ProfiniteGraph(lambda x, des_mod: x*x)
sage: graph.set_title("Square function")
sage: graph.plot()  # optional - tkinter
class View

Bases: object

This class stores the state of the “view” of a ProfiniteGraph.

A ProfiniteGraph allows the user to change the view by zooming in and out to open subsets of \(\hat{\ZZ} \times \hat{\ZZ}\) of the form \((x + p! \cdot \hat{\ZZ}) \times (y + p! \cdot \hat{\ZZ})\), i.e. (x mod p!) x (y mod p!). Here x, y and p are integers. We call p above the “precision”.

plot()

Open the interactive graph in a new window

EXAMPLES:

sage: cube_graph = ProfiniteGraph(lambda x, des_mod: x*x*x)
sage: cube_graph.plot()  # optional - tkinter

Note

Control flow of the program is handed over to the drawing library used (Tkinter) until the user closes the graph-window.

set_colors(approx=None, highlight=None, identity_line=None, axis=None)

Set the colors of the graph

INPUT:

  • approx – non-empty list of strings (optional, default is keep the current setting); colors of the different approximations

  • highlight – string (optional, default is keep the current setting); color of the transparent box that follows the mouse

  • identity_line – string (optional, default is keep the current setting); color of the identity line, see also set_identity_line()

  • axis – string (optional, default is keep the current setting); color of the axis and the corresponding coordinate labels (e.g. “1/6”, “2/3”)

Strings to denote a color should be formatted in one of the following ways:

  • A hexadecimal string specifying the red, green and blue portions of the color, in that order: “#rrggbb”. Examples: “#00ff00” is green, “#00ffff” is cyan and “#ff0077” is pink.

  • A locally defined standard color name. The colors “white”, “black”, “red”, “green”, “blue”, “cyan”, “yellow”, and “magenta” are always available. Usually more colors work, like “brown”, but this depends on your local installation.

EXAMPLES:

sage: graph = ProfiniteGraph(lambda x, des_mod: Zhat(0, 0))
sage: graph.set_colors(approx=["blue", "green", "yellow", "red"])
sage: graph.set_colors(highlight="#efefef", identity_line="#0033aa")
sage: graph.set_colors(axis="black")
set_identity_line(draw)

Set whether or not to draw the identity line

By the identity line we mean de graph of the identity function on \(\hat{\ZZ}\), i.e. “\(x = y\)”. We draw this as an actual line (with zero surface area). This can be useful to find fixed points of the function you are graphing. This is for example done in the paper [Len2005] for the graph of the Fibonacci function.

INPUT:

  • \(draw\) – boolean; whether or not to draw the identity line

EXAMPLES:

sage: graph = ProfiniteGraph(lambda x, des_mod: x*x)
sage: graph.set_identity_line(draw=False)

See also

To set the color of the identity line, see set_colors().

set_minimal_axis_distance(min_axis_distance)

Set the minimal axis distance

We usualy draw axes at two different precisions. But if you zoom in very far, the axes with the highest precision start taking up (almost) the whole graph. Hence if two such consecutive axes are less than a certain number of pixels apart, we don’t draw them any more; we only draw the lowest precision axis. This minimal number of pixels that these axes should be apart we call the “minimal axis distance”.

INPUT:

  • min_axis_distance – integer; the new minimal axis distance

EXAMPLES:

sage: graph = ProfiniteGraph(ProfiniteFibonacci())
sage: graph.set_minimal_axis_distance(10)

See also

To set the color of the axis, see set_colors().

set_minimal_draw_area(min_area)

Set the minimal draw area of this graph to min_area

We only draw approximations up to a precision such that we can still see the rectangles drawn for pairs \((x, y)\) in \(\hat{\ZZ} \times \hat{\ZZ}\). The minimal area in pixels a rectangle representing such a point \((x, y)\) must have to be drawn is called the “minimal draw area”. This method sets this minimal draw area.

INPUT:

  • min_area – integer; the new minimal draw area, in pixels

EXAMPLES:

sage: graph = ProfiniteGraph(lambda x, des_mod: x)
sage: graph.set_minimal_draw_area(25)
set_title(title)

Set the title of the graph-window to title

INPUT:

  • title – string; the new title

EXAMPLES:

sage: graph = ProfiniteGraph(lambda x, des_mod: x*x)
sage: graph.set_title("Graph of square-function")

TESTS:

sage: graph = ProfiniteGraph(lambda x, des_mod: x*x)
sage: graph.set_title(79)
Traceback (most recent call last):
...
TypeError: title should be a string
set_window_sizes(width=None, height=None, border=None, footer=None)

Set the sizes in pixels of the graph window

We define sizes in pixels of parts of the window, as depicted in ASCII-art below.

-----------------------------------------------------------
|            ^                                            |
|            |border                1 mod 2               |
|         0  v              1/2                 1         |
|        1-------------------|-------------------1        |
| border  |     ^                               | border  |
|<------->|     |            width              |<------->|
|         |<----------------------------------->|         |
|         |     |                               | 1 mod 2 |
|         |     |                               |         |
|         |     |                               |         |
|      1/2|     |                               |1/2      |
|         |     |                               |         |
|         |     |height                         |         |
| 0 mod 2 |     |                               | 0 mod 2 |
|         |     |                               |         |
|         |     v                               |         |
|        0-------------------|-------------------0        |
|         0  ^              1/2                 1         |
|            |border                1 mod 2               |
|   _   _   _v  _   _   _   _   _   _   _   _   _   _   _ |
|            ^                                            |
|            |footer    currenty viewing: ...             |
|            v                                            |
-----------------------------------------------------------

Note that width and height are not the width and height of the full window.

For the best drawing results, make sure k! divides width and height, for a “high” value of k (at least 4!, but strive for 5!).

EXAMPLES:

sage: graph = ProfiniteGraph(lambda x, des_mod: Zhat(2)*x)
sage: graph.set_window_sizes(720, 720, 30, 15)