Zum Inhalt springen

Projekt:Minkowski Gitterpunktsatz/Animationen

Aus Wikiversity

Die Animationen sind im Rahmen eines Studienprojektes im Bereich der Zahlentheorie bei Prof. Dr. Holger Brenner entstanden.

Alle Animationen basieren auf dem Gitterpunktsatz von Minkowski:

Es sei ein Gitter im mit Grundmasche . Es sei eine konvexe, kompakte, zentralsymmetrische Teilmenge in , die zusätzlich die Volumenbedingung

erfülle. Dann enthält mindestens einen von verschiedenen Gitterpunkt.


Der Beweis zu diesem Satz beruht darauf, dass die Menge entlang verdoppelter Grundmaschen des Gitters in Teilmengen zerschnitten wird, welche dann in eine Grundmasche verschoben werden. Wenn sich in dieser nun mindestens zwei schneiden, was insbesondere garantiert ist, wenn ist, enthält mindestens einen Gitterpunkt, ansonsten nicht. Die folgenden Animationen zeigen einige Beispiele dafür im :

Ein wichtiges Resultat des Gitterpunktsatzes ergibt sich in der Betrachtung von quadratischen Zahlbereichen:

Es sei    eine quadratfreie Zahl, sei der zugehörige quadratische Zahlbereich mit Diskriminante . Es sei    ein Ideal.

Dann gibt es ein , , mit der Eigenschaft


Der gesamte Beweis des Satzes ist hier zu finden.

Für die Animationen ist relevant, wie das Gitter von einem Ideal eines Zahlbereiches dargestellt werden kann, insbesondere also zuerst, wie das Gitter eines Zahlbereiches aussieht. Der größte Unterschied ergibt sich dabei aus dem Rest von modulo 4:

Es sei    eine quadratfreie Zahl und der zugehörige quadratische Zahlbereich.

Dann gilt

und

Das Gitter von in wird also von und bzw. aufgespannt, eine Grundmasche würde sich entsprechend beispielsweise durch das Parallelogramm ergeben, welches von aus durch diese beiden Punkte aufgespannt wird. Ein Ideal kann in einem quadratischen Zahlbereich immer in der Form mit dargestellt werden, das Gitter in wird dann über und bzw. aufgespannt.

Die Idee des Beweises ist nun, dass man eine Menge, die die Bedingungen des Gitterpunktsatzes (konvex, kompakt, zentralsymmetrisch, Volumen größer als das der Grundmasche) definiert, welche dementsprechend einen Gitterpunkt enthält, für die dann die gewünschte Normabschätzung aus Satz 2 vorliegt. Für wählt man dafür einen Kreis um den Nullpunkt mit Radius und für wählt man ein Quadrat mit den Eckpunkten mit .

Um die Unterschiede zwischen zum einen dem orthogonalen Gitter für und dem schiefen Gitter für sowie den unterschiedlichen Formen, die im Beweis genutzt werden, darzustellen, werden in den folgenden Animationen für das Ideal jeweils die Parameter und verwendet. Der Code ist zudem so ausgelegt, dass die Parameter und einfach in Zeile 167 (def construct(...)) verändert werden können und so andere Zahlbereiche und Ideale ausprobiert werden können, zudem werden Ideale - wenn möglich - vereinfacht. Gegebenenfalls muss das Fenster in Zeile 163 allerdings mit angepasst werden.

Python-Skript
from manim import config
from manim import *
import numpy as np

def generate_lattice(D: int, N: int = 4):
    """
    Erzeugen aller nur von der Wahl des Zahlbereichs abhängigen Werte (Determinante, Basis, Punkte und Gitter des Zahlbereichs)
    """

    # Berechnen von Basis und Diskriminante
    if D == 0:
        raise ValueError("D darf nicht 0 sein.")
    if D == 1:
        raise ValueError("D darf nicht 1 sein.")
    if D % 4 in (2, 3):
        # erzeugt ein orthogonales Gitter
        basis = np.array([[1, 0, 0], [0, np.sqrt(abs(D)), 0]])
        disc = 4*D
    elif D % 4 == 1:
        # erzeugt ein schiefes Gitter
        basis = np.array([[1, 0, 0], [0.5, np.sqrt(abs(D)) / 2, 0]])
        disc = D
    else:
        # Wenn D % 4 == 0 gilt, kann D nicht quadratfrei sein und wird dementsprechend ausgeschlossen
        raise ValueError("Unerwarteter D-Wert")

    # Erzeugen des Gitters von A_D
    lines1 = VGroup(*[
        Line(
            start=(-N*2)*basis[1] + k*basis[0],
            end=(N*2)*basis[1] + k*basis[0],
            stroke_opacity=0.3,
            color=GREEN
        )
        for k in range(-N, N+1)
    ])
    lines2 = VGroup(*[
        Line(
            start=(-N*2)*basis[0] + k*basis[1],
            end=(N*2)*basis[0] + k*basis[1],
            stroke_opacity=0.3,
            color=GREEN
        )
        for k in range(-N, N+1)
    ])

    # Alle Gitterpunkte
    points = [
        m * basis[0] + n * basis[1]
        for m in range(-N, N + 1)
        for n in range(-N, N + 1)
    ]
    

    return disc, points, basis, lines1, lines2

def ggT(a,b):
    if a == 0:
        return b
    if b == 0:
        return a 
    if a==b:
        return a
    divider = 1
    for i in range(1, min(abs(a),abs(b))+1):
        if a % i == 0 and b % i == 0:
            divider = i
    return divider

def clean_ideal_points(points, N):
    """
    Löscht die Punkte, die entweder außerhalb des Sichtbereichs liegen oder doppelt vorkommen.
    """
    s = set()
    uniq = []

    for p in points:
        # schränkt die Punkte auf den Sichtbereich ein
        if not (-N/2 <= p[0] <= N/2 and -N/2 <= p[1] <= N/2):
            continue
        
        # löscht die Duplikate
        key = (round(p[0], 9), round(p[1], 9), round(p[2], 9))
        if key not in s:
            s.add(key)
            uniq.append(p)

    return uniq

def ideal(N: int, a: int, alpha: int, beta: int, D: int, basis: np.array =[[1,0,0],[0,1,0]]):
    """
    Erzeugen aller Werte, die von der Wahl des Ideals abhängen (Norm, Basis und Punkte des Ideals), 
    zudem wird geprüft, ob das Ideal ein Hauptideal ist. 
    Da hier nur mit quadratischen Zahlbereichen gearbeitet wird, wird jedes Ideal von maximal 2 Elementen erzeugt.
    """

    # Vorarbeiten für die Berechnungen
    if D % 4 == 1:
        norm_alphabeta = int(abs((alpha+0.5*beta)**2-(beta**2)*D/4))
    else:
        norm_alphabeta = int(abs(alpha**2-(beta**2)*D))

    x_fact = int(norm_alphabeta/ggT(alpha, beta)) # Das kleinste x>0, für das [x,0,0] im Ideal liegt

    is_principal_ideal = False
    if a % x_fact == 0: 
        # Wenn [a,0,0] im von [alpha,beta,0] erzeugten Ideal liegt, ist das Ideal ein Hauptideal
        is_principal_ideal = True
    
    if is_principal_ideal == False:
        if ggT(norm_alphabeta, int(a**2)) == 1:
            # In diesem Fall enthält das Ideal den gesamten Zahlbereich
            ideal_norm = 1
        else:
            ideal_norm = abs(a*beta)
    else:
        ideal_norm = norm_alphabeta

    # Bestimmen der Idealpunkte
    ideal_points = []
    for i in range(-2*N, 2*N+1):
        for j in range(-2*N, 2*N+1):
            ideal_points.append(i*a*basis[0]+j*a*basis[1])
            if D % 4 == 1:
                ideal_points.append((i*alpha+j*beta*(D-1)/4)*basis[0] + (i*beta+j*alpha+j*beta)*basis[1])
            else:
                ideal_points.append((i*alpha+j*beta*D)*basis[0] + (i*beta+j*alpha)*basis[1])
    
    # Bei von zwei Elementen erzeugten Idealen kann es sein,
    # dass nicht alle Punkte des Ideals durch das Multiplizieren mit den einzelnen Basiselementen
    # erfasst werden.
    ideal_helper = []
    for p in ideal_points:
        for n in range(1, int(max(ideal_norm, norm_alphabeta))+1):
            ideal_helper.append(p+[n*a,0,0])
    
    for p in ideal_helper:
        ideal_points.append(p)

    ideal_points = clean_ideal_points(ideal_points, N)

    """
    Berechnen der Idealbasis. 
    Für den Fall, dass a=0 gesetzt wird (also ein Hauptideal vorliegt),
    ist der erste Vektor zwar theoretisch ein Erzeuger von einem Teil des Ideals 
    (aber für das gesamte Ideal eben irrelevant),
    realistisch ist er aber dafür da, um die Grundmasche zu erzeugen.
    """
    ideal_basis = basis.copy()
    if ideal_norm == 1:
        ideal_basis = basis.copy()
    elif a != 0:
        ideal_basis[0] = basis[0]*a
        ideal_basis[1] = basis[0]*alpha+basis[1]*beta
    else:
        ideal_basis[0] = basis[0]*x_fact
        ideal_basis[1] = basis[0]*alpha+basis[1]*beta
    

    return ideal_basis, ideal_norm, ideal_points, is_principal_ideal

# Einstellen des Fensters
config.frame_width = 14

class MinkowskiZB(Scene):
    # Hier wird sowohl der Zahlbereich, als auch das Ideal ausgewählt
    def construct(self, D=-3, a=2, alpha=1, beta=1):
        

        N=config.frame_width

        # Standartgitter vom Z^2, zur Orientierung        
        grid = NumberPlane(
            x_range=[-N, N, 1],
            y_range=[-N, N, 1],
            background_line_style={"stroke_color": GRAY, "stroke_opacity": 0.25}
        )
        self.add(grid)
        self.wait(0.4)
        

        # Generieren des Zahlbereichs
        disc, points, base_basis, lines1, lines2 = generate_lattice(D, N) 
        
        # Generieren des Ideals
        basis, ideal_norm, ideal_points, is_principal_ideal = ideal(N, a, alpha, beta, D, base_basis)
        # Definieren der Verschiebungsrichtungen
        ups,rights = [UP*basis[1][1]*2+RIGHT*basis[1][0]*2, RIGHT*basis[0][0]*2]

        # Gitter des Zahlbereichs einfügen
        self.add(lines1)
        self.wait(0.4)

        self.add(lines2)
        self.wait(0.4)

        # Punkte des Zahlbereiches einfügen
        lattice = VGroup(*[
            Dot(p, radius=0.07, color=BLUE)
            for p in points
        ])
        self.add(lattice)

        # Beschriften von x- und y-Achse des Standartgitters zur Orientierung
        labels_x = VGroup()
        labels_y = VGroup()
        for i in range(-N, N+1):
            label_x = MathTex(f"{i}", color=WHITE).scale(0.5).next_to([i-0.2, 0, 0], DOWN, buff=0.1)
            labels_x.add(label_x)
            if i != 0:
                label_y = MathTex(fr"{i}\sqrt{{{D}}}", color=WHITE).scale(0.5).next_to([0, i*np.sqrt(abs(D))-0.2, 0], LEFT, buff=0.1)
                labels_y.add(label_y)
        self.add(labels_x, labels_y)
        self.wait(0.4)

        # Hervorheben des Ideals
        ideal_lattice = VGroup(*[
            Dot(p, radius=0.08, color=YELLOW)
            for p in ideal_points
        ])
        self.add(ideal_lattice)
        self.wait(0.4)

        # Erzeugen der Legende
        if ideal_norm == 1:
            if D % 4 != 1:
                legend = VGroup(
                    MathTex(rf"ZB:\, A_{{{D}}}=\mathbb{{Z}}[\sqrt{{{D}}}]", color = BLUE),
                    MathTex(rf"Ideal:\, (1)", color = YELLOW)
                ).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
            else:
                legend = VGroup(
                    MathTex(rf"ZB:\, A_{{{D}}}=\mathbb{{Z}}[\omega],", color = BLUE),
                    MathTex(rf"\omega = \frac{{1+\sqrt{{{D}}}}}{{2}}", color = BLUE),
                    MathTex(rf"Ideal:\, (1)", color = YELLOW)
                ).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
        elif D % 4 != 1:
            if is_principal_ideal == True:
                legend = VGroup(
                    MathTex(rf"ZB:\, A_{{{D}}}=\mathbb{{Z}}[\sqrt{{{D}}}]", color = BLUE),
                    MathTex(rf"Ideal:\, ({alpha}+{beta}\sqrt{{{D}}})", color=YELLOW)
                ).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
            else:
                legend = VGroup(
                    MathTex(rf"ZB:\, A_{{{D}}}=\mathbb{{Z}}[\sqrt{{{D}}}]", color = BLUE),
                    MathTex(rf"Ideal:\, ({a}, {alpha}+{beta}\sqrt{{{D}}})", color=YELLOW)
                ).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
        else:
            if is_principal_ideal == True:
                legend = VGroup(
                    MathTex(rf"ZB:\, A_{{{D}}}=\mathbb{{Z}}[\omega],", color = BLUE),
                    MathTex(rf"\omega = \frac{{1+\sqrt{{{D}}}}}{{2}}", color=BLUE),
                    MathTex(rf"Ideal:\, ({alpha}+{beta}\omega)", color=YELLOW)

                ).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
            else:
                legend = VGroup(
                    MathTex(rf"ZB:\,  A_{{{D}}}=\mathbb{{Z}}[\omega],", color = BLUE),
                    MathTex(rf"\omega = \frac{{1+\sqrt{{{D}}}}}{{2}}", color=BLUE),
                    MathTex(rf"Ideal:\, ({a}, {alpha}+{beta}\omega)", color=YELLOW)
                ).arrange(DOWN).to_corner(UL)
          
        box = SurroundingRectangle(legend, buff=0.2, stroke_color=WHITE, 
                                   fill_color=BLACK, fill_opacity=1)
        self.add(box, legend)

        # Grundmasche einzeichnen
        masche = Polygon(*[
            ORIGIN,
            basis[0],
            basis[0]+basis[1],
            basis[1],
        ], color=RED, fill_opacity=0.05)
        self.add(masche)
        self.wait(1.5)
        
        # Verdoppeln der Grundmasche
        doppelmasche = Polygon(*[
            ORIGIN,
            basis[0]*2,
            (basis[0]+basis[1])*2,
            basis[1]*2,
        ], color=RED, fill_opacity=0.05)
        self.remove(masche)
        self.add(doppelmasche)
        self.wait(1.5)

        # Erzeugen der Form, die geshiftet wird
        if D < 0:
            form = Circle(radius=np.sqrt((2/np.pi)*np.sqrt(abs(disc))*ideal_norm))
        else:
            form = Square(side_length=2*np.sqrt(0.5*np.sqrt(abs(disc))*ideal_norm))
        self.add(form)
        # Hervorheben der Idealpunkte, die durch den Satz nachgewiesei eine wie gefordert kleine Norm haben
        intercepts = []
        if D < 0:
            for p in ideal_points:
                if np.sqrt(p[0]**2+p[1]**2) <= np.sqrt((2/np.pi)*np.sqrt(abs(disc))*ideal_norm):
                    intercepts.append(p)
        else:
            for p in ideal_points:
                if (abs(p[0]) <= np.sqrt(0.5*np.sqrt(abs(disc))*ideal_norm) and abs(p[1]) <= np.sqrt(0.5*np.sqrt(abs(disc))*ideal_norm)):
                    intercepts.append(p)
        intercept_points = VGroup(*[
            Dot(p, radius=0.09, color=RED)
            for p in intercepts
        ])
        self.add(intercept_points)


        # Zerschneiden
        pieces = VGroup()
        shifts = []
        for i in range(-4,4):
            for j in range(-4,4):
                mod = ups*i+rights*j
                piece = Intersection(Polygon(*[
                    ORIGIN+mod, basis[0]*2+mod, (basis[0]+basis[1])*2+mod, basis[1]*2+mod]),
                    form, color=BLUE, fill_opacity=0.2)
                pieces.add(piece)
                shifts.append(-mod)
        
        self.remove(form)
        self.add(pieces)
        self.wait(0.8)

        #  Verschieben in die Grundmasche
        self.play(*(p.animate.shift(s) for p, s in zip(pieces, shifts)), run_time = 2)
        self.wait(3)

Da die Zahlbereiche , und alle faktoriell sind, wird in diesen jedes Ideal von nur einem Element erzeugt (und ist somit ein Hauptideal), so auch das hier verwendete: In und ist das verwendete Ideal sogar der gesamte Zahlbereich, wodurch die Grundmasche des Ideals kleiner wird, die Aussage des Satzes - wie in der Animation sichtbar - aber natürlich bestehen bleibt.