Sonnenuhr

Dieses Dokument wurde erstellt durch Stefan Kühnel stefan.kuehnel@hm.edu und Konrad Moron k.moron@hm.edu

Inhaltsverzeichnis

1. Abstrakt

Sonnenuhren fangen durch einen fest verankerten Stab Sonnenstrahlen ein und ermöglichen durch die Projektion des Schattens auf am Boden befindliche Zeitlinien, eine präzise Bestimmung der aktuellen Uhrzeit. Doch obwohl das Konzept von Sonnenuhren schon zu Zeiten der alten Ägypter bekannt gewesen ist [Ref: 1], so handelt es sich bei ihrem Aufbau auch heute noch um eine nicht ganz triviale Aufgabe. Ziel des nachfolgenden Papers wird es deshalb sein, eine universelle Sonnenuhr auf Basis der aktuellen Positions-/ und Zeitzonendaten zu definieren, die, sollte sie an einer Südwand angebracht sein, die korrekte aktuelle Uhrzeit anzeigt.

2. Annahmen

Das nachfolgende mathematische Modell trifft einige idealisierte Annahmen:

  1. Die Sonnenuhr wird an einer Südwand angebracht, d.h. der Azimut liegt zwischen $\displaystyle + \frac{\pi}{2}$ und $\displaystyle - \frac{\pi}{2}$.
  2. Die Südwand ist genau senkrecht, d.h. der Höhenwinkel des Normalenvektors ist 0.
  3. Der horizontale Sonnenwinkel (Azimut) wandert pro Stunde etwa 15 Grad weiter.
  4. Der allgemeine Zeigerstab zeigt immer nach Süden und mit der Horizontalen bildet dieser den Breitengrad $\displaystyle \beta$.

3. Mathematisches Modell

Bei der Implementierung der Sonnenuhr kamen vermehrt Themen aus dem Themenkomplex der Linearen Algebra und der Geometrischen, sowie Analytischen Geometrie zum Einsatz. Zuerst war es dabei notwendig, die notwendigen Punkte Zeigerstab, Sonnenposition, Schattenvektor und Normalenvektor der Hauswand über Kugelkoordinaten zu definieren. Im Anschluss wurde durch über den Richtungsvektor der Sonne und den an der Hauswand angebrachten Zeigerstab ein Sonnenstrahl gelegt, dessen Schnittpunkt auf der Wand, den Schattenvektor begrenzte. Durch eine anschließende Abbildung des dreidimensionalen Schattenvektors auf eine zweidimensionale Fläche, war es möglich, die x,y-Koordinaten des Zeigerstabs zu bestimmen, die im finalen Schritt zur Erstellung des Ziffernblattes der Sonnenuhr zum Einsatz kamen.

3.1. Implementierung der Gleichungen für die Sonnenbahn

Die Position der Sonne ist abhängig von einem gegebenen Ort und der dazugehörigen aktuellen Uhrzeit. Die Implementierung der Sonnenbahngleichungen erfolgte gemäß den vorgegebenen Formeln in der deutschsprachigen Wikipedia [Ref: 3]. Der Konstruktoraufruf erfordert die Angabe eines gregorianischen Datums und einer Position (Längen-/ und Breitengrad) auf der Erde. Nach Abschluss aller Berechnungen, liefert die Methode getAngles() ein Listenelement zurück, welches Azimut und Höhenwinkel enthält. Die zurückgegebene Ausgabe wurde darüber hinaus durch das Portal Sun Ephemeris Calculator von Jay Tanner verifiziert. Die marginale Messabweichung von maximal 0.01° stellt für die Simulation und Berechnung der Sonnenuhr allerdings kein Problem dar.

Sollten jedoch Werte mit einer maximalen Abweichung von 0.0003° erforderlich sein, so kann der Algorithmus von Reda und Afshin [Ref: 4] angewendet werden, der für eine Zeitspanne von $-2000$ bis $+6000$ Jahren eine Gültigkeit besitzt.

In [1]:
class Sun():
    def __init__(self, y, m, d, l, t):
        """
        Konstruiert eine neue Sonne mit den angegebenen Datum und Koordinaten
        y = gregorianisches Jahr, ganze Zahl
        m = gregorianischer Monat, ganze Zahl
        d = gregorianischer Tag
        l = Längengrad in Dezimaldarstellung
        t = Breitengrad in Dezimaldarstellung
        """
        assert 0 < m < 13,"Der gregorianische Monat muss im Intervall [0,12] liegen."
        assert m in ZZ,"Der gregorianische Monat muss einer ganzen Zahl entsprechen."
        assert y in ZZ,"Das gregorianische Jahr muss einer ganzen Zahl entsprechen."
        assert 0 < d <= 31,"Der gregorianische Tag muss im Intervall [1,31] liegen."
        self.__year = y
        self.__month = m
        self.__day = d
        self.__l = l
        self.__t = t
        angles = self.getAngles(0)
    
    def getAngles(self, T):
        """
        Berechnet den Azimut und Höhenwinkel der Sonne für die gegebene Uhrzeit.
        T = Dezimaldarstellung der Uhrzeit
        """
        assert 0 <= T < 24,"Die Uhrzeit muss der Dezimaldarstellung entsprechen und sich im Intervall [0,24] befinden."
        s = radian(self.__stundenwinkel(T))
        d = radian(self.__deklination(T))
        t = radian(self.__t)
        az = degree(arctan(sin(s)/(cos(s)*sin(t) - tan(d)*cos(t))))
        if (cos(s)*sin(t) - tan(d)*cos(t)) < 0:
            az = az + 180
        hw = degree(arcsin(cos(d)*cos(s)*cos(t) + sin(d)*sin(t)))
        sol = [az, hw]
        return sol
    
    def getLatitude(self):
        """
        Liefert den Breitengrad des Objekts."""
        return self.__t
        
    def __stundenwinkel(self, T):
        """
        Liefert den Stundenwinkel für die im Konstruktor angegebenen Daten
        T = Dezimaldarstellung der Uhrzeit
        """
        return self.__localStundenWinkel(T) - self.__rektaszension(T)
    
    def __rektaszension (self, T):
        """
        Liefert die Rektaszension für die im Konstruktor angegebenen Daten
        T = Dezimaldarstellung der Uhrzeit
        """
        n = (self.__JD(T=T) - 2451545)
        e = 23.439 - 0.0000004*n
        A = self.__eclipticposition(T)
        if cos(radian(A)) > 0:
            result = degree(arctan(cos(radian(e))*tan(radian(A))))
        else:
            result = degree(arctan(cos(radian(e))*tan(radian(A)))) + 4*degree(arctan(1))
        if result < 0:
            result = 360 + result
        return result
    
    def __deklination(self, T):
        """
        Liefert die Deklination für die im Konstruktor angegebenen Daten
        T = Dezimaldarstellung der Uhrzeit
        """
        n = (self.__JD(T=T) - 2451545)
        e = 23.439 - 0.0000004*n
        A = self.__eclipticposition(T)
        return degree(arcsin(sin(radian(e))*sin(radian(A))))
    
    def __eclipticposition (self, T):
        """
        Liefert die ekliptikale Länge für die im Konstruktor angegebenen Daten
        T = Dezimaldarstellung der Uhrzeit
        """
        n = (self.__JD(T=T) - 2451545)
        L = 280.460 + 0.9856474*n
        g = 357.528 + 0.9856003*n
        return L + 1.915*sin(radian(g)) + 0.01997*sin(radian(2*g))
    
    def __localStundenWinkel (self, T):
        """
        Liefert den lokalen Stundenwinkel des Frühlingspunktes für die im Konstruktor angegebenen Daten.
        T = Dezimaldarstellung der Uhrzeit
        """
        tmp = 6.697376 + 2400.05134*self.__T0() + 1.002738*T
        remHours = int(tmp)//24*24
        return (6.697376 + 2400.05134*self.__T0() + 1.002738*T-remHours)*15 + self.__l
    
    def __T0 (self):
        """
        Liefert die bisher vergangenen Julianischen Jahrhunderte ab J2000
        """
        return (self.__JD() - 2451545)/36525
    
    def __JD (self, T=0):
        """
        Liefert die julianische Tageszahl für das hinterlegte Datum und Uhrzeit
        T = Dezimaldarstellung der Uhrzeit
        """
        y = self.__year
        m = self.__month
        d = self.__day + T/24
        if m <= 2:
            y = y - 1
            m = m + 12
        return floor(365.25*(y+4716)) + floor(30.6001*(m+1)) + d + (2 - y//100 + y//400) - 1524.5

3.2. Beschreibung einer allgemeinen Ursprungsebene durch Normalenvektor

Zur Beschreibung der Ursprungsebene wird ein Normalenvektor $\vec{e}$ mit Hilfe von Kugelkoordinaten definiert. Gemäß Zwillinger (S. 589) [Ref: 2] ergibt sich zur Definition eines Vektors durch zwei Winkel folgende Vektordarstellung:

$\displaystyle \tilde{\vec{e}} = \begin{pmatrix}p\cos{\theta}\sin{\phi} \\ p\sin{\theta}\sin{\phi}\\p\cos{\phi}\end{pmatrix}$

Setzt man nun jedoch für den Höhenwinkel $\theta = 0$ und für den Azimuth $\phi = 0$ (für Süden) ein, so erkennt man sehr schnell durch die Ausrichtung des Normalenvektors in z-Richtung, dass diese Art der Darstellung ein wenig realitätsfremd ist, wenn man beachtet, dass eine Hauswand niemals gen Himmel gerichtet sein wird. Aus diesem Grund soll daher eine realistischere Vektordarstellung graphisch ermittelt werden.

Herleitung der Vektordefinition von Kugelkoordianaten

Die nun ermittelte Vektordarstellung richtet den Normalenvektor für $\theta = 0$ und $\phi = 0$ gemäß realistischen Gegebenheiten zur x-Achse hin aus.

Beachte: Die Länge $r$ (Distanz zum Punkt P) des Normalenvektors spielt keine Rolle und wird deshalb auf 1 gesetzt.

$\displaystyle \vec{e} = r \cdot \begin{pmatrix} \cos{\theta}\cos{\phi} \\ \cos{\theta}\sin{\phi} \\ \sin{\theta} \end{pmatrix} = \begin{pmatrix} \cos{\theta}\cos{\phi} \\ \cos{\theta}\sin{\phi} \\ \sin{\theta} \end{pmatrix}$

3.3. Berechnung der x,y-Koordinaten der Schattenspitze auf der Hauswand

Zur Berechnung der Schattenvektors $\vec{s}$ auf der Hauswand ist es erforderlich eine Gerade, beschrieben durch die Position der Sonne am Himmel $\vec{☉}$ und den Zeigerstab $\vec{p}$ zu definieren. Daraus ergibt sich folgende Formeldarstellung:

$\displaystyle \vec{s} = \vec{p} + \lambda\vec{☉}$

Da bekannt ist, dass der die Hauswand beschreibende Normalenvektor $\vec{e}$ und der dazugehörige Schattenvektor $\vec{s}$ zueinander orthogonal $\vec{s} \perp \vec{e}$ sind, folgt daraus, dass auch das Standardskalarprodukt $\langle \vec{s},\vec{e}\rangle = \vec{s} \cdot \vec{e} = 0$ sein muss. Diese Erkenntnis kann zum Auflösung der oben definierten Geradengleichung verwendet werden [Ref: 5].

$\displaystyle \underbrace{\vec{s} \cdot \vec{e}}_{= \, 0} = [\vec{p} + \lambda\vec{☉}] \cdot \vec{e}$

$\displaystyle \therefore \, \lambda = - \frac{\vec{p} \cdot \vec{e}}{\vec{☉} \cdot \vec{e}} = - \frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}$

Durch Erweiterung, Faktorisierung und Substitution des Ausdrucks für $\lambda$ Appendix 1, kann für den Schattenvektor $\vec{s}$ folgende Vektordarstellung gefunden werden:

$\displaystyle \vec{s} = \frac{1}{e_{1} ☉_{1} + e_{2} ☉_{2} + e_{3} ☉_{3}} \cdot {\begin{bmatrix}p_{1}(☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{1} (p_{2} e_{2} + p_{3} e_{3})\\p_{2} (☉_{1} e_{1} + ☉_{3} e_{3}) - ☉_{2} (p_{1} e_{1} + p_{3} e_{3})\\p_{3} (☉_{1} e_{1} + ☉_{2} e_{2}) - ☉_{3} (p_{1} e_{1} + p_{2} e_{2})\\\end{bmatrix}}$

Um nun die x,y-Koordinaten der Schattenspitze auf der Hauswand zu ermitteln, ist es gemäß Ruhrländer (S. 101 - 109) [Ref: 6] erforderlich, die Ebene mit den daraufliegenden Schatten auf eine zweidimensionale Abbildung zu projizieren. Eine Erleichterung stellt hierbei die Annahme dar, dass die Hauswand immer einen Höhenwinkel $\theta = 0$ besitzen wird.

Durch die Hilfsmethode wall_projection_matrix() wird eine Matrix $\boldsymbol {P}$ mit zwei Richtungsvektoren $\vec{u}$ und $\vec{v}$ der Hauswand und dem dazugehörigen Normalenvektor $\vec{e}$ erzeugt.

$\boldsymbol {P} = \begin{bmatrix}\, \\ \vec{u} & \vec{v} & \vec{e} \\ \,\,\end{bmatrix} \quad \textrm{mit} \quad \vec{u} = \vec{v} \times \vec{e} \quad \textrm{und} \quad \vec{v} = \begin{pmatrix}0\\0\\1\\\end{pmatrix}$

Beachte: $\vec{v} \times \vec{e}$ entspricht dem Kreuzprodukt des zweiten Richtungsvektors $\vec{u}$ mit dem Normalenvektor $\vec{e}$.

Nun kann ein lineares Gleichungssystem aufgestellt werden, wobei $\vec{s}$ dem dreidimensionalen Schattenvektor entspricht.

$\boldsymbol {P} \cdot \vec{x} = \vec{s}$

Löst man dieses Gleichungssystem nun nach $\vec{x}$ auf, so erhält man den auf die Hauswand projizierten zweidimensionalen Schattenvektor $\vec{\hat s}$.

$\vec{\hat s} = \begin{pmatrix}x_{1}\\x_{2}\\\end{pmatrix}$

In [2]:
def radian (d):
    return (d/180)*pi
    
def degree (d):
    return (d/pi)*180
    
def carthesian_coordinate(distance, azimuth, elevation):
    """
    Liefert einen Punkt im dreidimensionalen Raum in karthesischen Koordinaten.
    distance: Die Länge des Vektors. Nicht negativ, nicht 0.
    azimuth: Der horizontale Winkel (Azimut) des zu beschreibenden Objekts.
    elevation: Der Höhenwinkel des zu beschreibenden Objekts.
    """
    azimuth = -radian(azimuth)
    elevation = radian(elevation)
    assert (distance > 0), "Die Länge darf nicht negativ und nicht 0 betragen."
    return vector([
        distance*cos(elevation)*cos(azimuth),
        distance*cos(elevation)*sin(azimuth),
        distance*sin(elevation)
    ])
    
def wall_projection_matrix(normal):
    """
    Gibt eine die Hauswand beschreibende Projektionsmatrix zurück, welche
    den senkrecht auf der Hauswand stehenden Normalenvektor und zwei Richtungsvektoren
    der Hauswand beinhaltet.
    normal: Ein dreidimensionaler Normalenvektor der Hauswand mit Höhenwinkel = 0.
    """
    elevation = normal[2]
    degree_of_normal = normal.degree()
    assert(elevation == 0), "Der Normalenvektor muss einen Höhenwinkel = 0 besitzen."
    assert(degree_of_normal == 3), "Der Normalenvektor muss dreidimensional sein."

    vertical_unit_vector = vector([0, 0, 1])
    wall_direction_vector = vertical_unit_vector.cross_product(normal).normalized()

    x = wall_direction_vector
    y = vertical_unit_vector
    z = normal

    wall_projection_matrix = Matrix([x,y,z]).transpose()

    return wall_projection_matrix

class Sundial():
    def __init__(self, config):
        """
        Initialisiert eine neue Sonnenuhr über ein Konfigurationsdictionary.
        config["year"] = Das aktuelle Jahr in der Form YYYY.
        config["month"] = Der aktuelle Monat in der Form MM im Intervall 1 bis 12.
        config["day"] = Der aktuelle Tag in der Form DD im Intervall 1 bis 31.
        config["longitude"] = Der Längengrad in Grad.
        config["latitude"] = Der Breitengrad in Grad.
        config["day_time"] = Die aktuelle Tageszeit im Intervall 0 bis 23
        config["wall_azimuth"] = Der Azimuth der Wand im Intervall -pi/2 und +pi/2
        config["pointer_length"] = Die Länge des Zeigerstabs > 0
        """
        assert("year" in config), "Bitte übergeben Sie ein Jahr in der Form YYYY"
        assert("month" in config), "Bitte übergeben Sie einen Monat in der Form MM im Intervall 0 bis 13"
        assert("day" in config), "Bitte übergeben Sie einen Tag in der Form DD"
        assert("longitude" in config), "Bitte übergeben Sie einen Breitengrad"
        assert("latitude" in config), "Bitte übergeben Sie einen Längengrad"
        assert("day_time" in config), "Bitte übergeben Sie eine Tageszeit im Intervall 0 bis 23"
        assert("wall_azimuth" in config), "Bitte übergeben Sie den Azimut der Hauswand."
        assert("pointer_length" in config), "Bitte definieren Sie die Länge des Zeigerstabs."
        
        assert(config["year"] in ZZ), "Das Jahr entspricht nicht der Form YYYY"
        assert(config["month"] in ZZ), "Der Monat entspricht nicht der Form MM"
        assert(0 < config["month"] < 13), "Der Monat liegt nicht im Intervall 1 bis 12"
        assert(config["day"] in ZZ), "Der Tag entspricht nicht der Form DD"
        assert(0 < config["day"] < 32), "Der Tag muss im Intervall 1 bis 31 liegen."
        assert(0 <= config["day_time"] <= 23),"Die aktuelle Uhrzeit muss im Intervall 0 bis 23 liegen."
        assert(-pi/2<=radian(config["wall_azimuth"])<=pi/2),"Der Azimut der Südwand muss zwischen -pi/2 und +pi/2 liegen."
        assert(config["pointer_length"] > 0), "Die Länge des Zeigerstabs darf nicht kleiner 0 sein."
            
        self.year = config["year"]
        self.month = config["month"]
        self.day = config["day"]
        self.longitude = config["longitude"]
        self.latitude = config["latitude"]
        self.day_time = config["day_time"]
        self.wall_azimuth = config["wall_azimuth"]
        self.pointer_length = config["pointer_length"]
        
        self.SUN = Sun(self.year, self.month, self.day, self.longitude, self.latitude)
        
    def update_day_time(self, day_time):
        """
        Ermöglicht die dynamische Aktualisierung der Tageszeit.
        day_time = Die aktualisierte Tageszeit in Stunden, die im Intervall 0 bis 23 liegen muss.
        """
        assert(0 <= day_time < 24),"Die aktuelle Uhrzeit muss im Intervall 0 bis 23 liegen."
        self.day_time = day_time
    
    def get_normal(self):
        """
        Liefert einen Normalenvektor, der die nach Süden ausgerichtete senkrechte Hauswand beschreibt.
        """
        distance = 1
        elevation = 0
        return carthesian_coordinate(distance, self.wall_azimuth, elevation)
    
    def get_pointer(self):
        """
        Liefert einen nach Süden ausgerichteten Zeigerstab.
        """
        azimuth = 0
        elevation = -self.SUN.getLatitude()
        return carthesian_coordinate(self.pointer_length, azimuth, elevation)
    
    def get_sun_position(self):
        """
        Liefert den Vektor der Sonne.
        """
        sun = self.SUN
        sun_angles = sun.getAngles(self.day_time)
        distance = 1
        azimuth = sun_angles[0]
        elevation = sun_angles[1]
        return carthesian_coordinate(distance, azimuth, elevation)
    
    def get_shadow(self):
        """
        Liefert den durch den Zeigerstab auf der Hauswand geworfenen Schatten zur entsprechenden Tageszeit.
        """
        o = self.get_sun_position()
        p = self.get_pointer()
        e = self.get_normal()
        
        day_time = self.day_time
        sun_angles = self.SUN.getAngles(day_time)
        elevation = sun_angles[1]
        
        # Überprüfung, ob der Sonnenvektor nicht hinter der Hauswand liegt.
        if (wall_projection_matrix(self.get_normal())\o)[2] <= 0:
            return vector([0, 0, 0])
        
        # Überprüfung, ob die Sonne noch sichtbar ist.
        if (elevation < 0):
            return vector([0, 0, 0])
                
        p1 = p[0]
        p2 = p[1]
        p3 = p[2]

        e1 = e[0]
        e2 = e[1]
        e3 = e[2]
 
        o1 = o[0]
        o2 = o[1]
        o3 = o[2]

        e1o1 = e1*o1
        e2o2 = e2*o2
        e3o3 = e3*o3

        pf = 1/(e1o1 + e2o2 + e3o3)

        s1 = p1*(e2o2 + e3o3) - o1*(p2*e2 + p3*e3)
        s2 = p2*(e1o1 + e3o3) - o2*(p1*e1 + p3*e3)
        s3 = p3*(e1o1 + e2o2) - o3*(p1*e1 + p2*e2)

        return pf * vector([s1, s2, s3])
    
    def get_shadow_on_wall(self):
        """
        Gibt den dreidimensionalen auf die Hauswand projezierten zweidimensionalen Schattenvektor zurück.
        """
        shadow = self.get_shadow().n()
        normal = self.get_normal().n()
        projection_matrix = wall_projection_matrix(normal)
        
        dimension_of_projection_matrix = projection_matrix.dimensions()
        degree_of_shadow = shadow.degree()

        assert(dimension_of_projection_matrix == (3, 3)), "Die Projektionsmatrix der Hauswand muss dreidimensional sein."
        assert(degree_of_shadow == 3), "Der Schattenvektor muss dreidimensional sein."

        projected_shadow = projection_matrix\shadow

        x = projected_shadow[0]
        y = projected_shadow[1]

        shadow_on_wall = vector([x, y])

        return shadow_on_wall
    
    def plot_dial(self):
        """
        Erstellt ein zweidimensionales Ziffernblatt der Sonnenuhr.
        """
        time_window_gmt = range(6, 18)
        dial_shadows = []
        
        for time in time_window_gmt:
            self.update_day_time(time)
            
            scale = 3
            shadow_on_wall = self.get_shadow_on_wall().normalized()
            time_indicator = line([(0, 0), [x for x in scale*shadow_on_wall]])
            
            dial_shadows.append(time_indicator)
            
            # Hinzufügen der Indexe zum Ablesen der Tageszeit für jeden Zeitabschnitt.
            if shadow_on_wall.norm() > 0:
                line_offset = 0.1
                position_scalar = 2
                
                position_x = position_scalar*shadow_on_wall[0] + line_offset
                position_y = position_scalar*shadow_on_wall[1]
                
                time_display = text(time, (position_x, position_y), color="red")
                
                dial_shadows.append(time_display)
            
        plot_dial_shadows = sum(dial_shadows)
        plot_dial_shadows.show(ymin=-2, xmin=-2, xmax=2, aspect_ratio=1, figsize=10)
            
    
    def model_plot3d(self):
        """
        Erstellt eine grafische, dreidimensionale Veranschaulichung des Sonnenuhrenproblems.
        """
        # Setzen des Plotbereichs
        plot_domain = (-5*self.pointer_length, 5*self.pointer_length)
        
        # Sammlung aller für den Plot erforderlichen Vektoren
        normal = self.get_normal().n()
        pointer = self.get_pointer().n()
        sun_position = self.get_sun_position().n()
        shadow = self.get_shadow().n()
        
        # Setzen der Strahlengleichung
        var('k')
        sun_ray_equation = pointer + k*sun_position
        
        # Grafik der südlichen Hauswand
        wall_plot = implicit_plot3d(lambda x,y,z: normal[0]*x + normal[1]*y + normal[2]*z, plot_domain, plot_domain, plot_domain, color='gray', opacity=0.1)
        
        # Grafik des Sonnenstrahls 
        sun_ray_plot = line3d([sun_ray_equation.substitute(k=plot_domain[0]), sun_ray_equation.substitute(k=plot_domain[1])], color='orange') # Plot des Sonnenstrahls durch die Zeigerspitze.
        sun_ray_label = text3d("Sonnenstrahl", (sun_ray_equation.substitute(k=plot_domain[1])[0], sun_ray_equation.substitute(k=plot_domain[1])[1], sun_ray_equation.substitute(k=plot_domain[1])[2]))
        
        # Grafik des Zeigerstabs
        pointer_plot = line3d([(0, 0, 0), [x for x in pointer]], color='black') # Plot des Zeigers.
        pointer_label = text3d("Zeigerstab", (pointer[0], pointer[1], pointer[2]))
        
        # Grafik der Sonne
        sun_position_plot = line3d([(0, 0, 0), [x for x in sun_position]], color='orange') # Plot der Sonne.
        sun_position_label = text3d("Sonne", (sun_position[0], sun_position[1], sun_position[2]))
        
        # Grafik des Schattens
        shadow_plot = line3d([(0, 0, 0), [x for x in shadow]], color='black') # Plot des Schattens.
        shadow_label = text3d("Schatten", (shadow[0], shadow[1], shadow[2]))
        
        # Rückgabe des Plots
        model_plot3d = pointer_plot + pointer_label + sun_position_plot + sun_position_label + shadow_plot + shadow_label + sun_ray_plot + sun_ray_label + wall_plot
        model_plot3d.show(frame=False, axes=True)
    

4. Anwendung und Veranschaulichung

Der nachfolgende Teilbereich wird abschließend auf die Verwendung der einzelnen Klassen, sowie verschiedene unterstützende Veranschaulichungen eingehen.

In [36]:
# Bereitstellung der Basiskonfiguration der Sonnenuhr
config = {
    "year": 2020,
    "month": 5,
    "day": 5,
    "longitude": 11.5451628,
    "latitude": 48.1540728,
    "day_time": 18,
    "wall_azimuth": 72,
    "pointer_length": 6.5
}

# Instanzierung der Sonnenuhr
sundial = Sundial(config)

# Dynamische Aktualisierung der Tageszeit
sundial.update_day_time(18)

4.1. Veranschaulichung der Implementierung des Sonnenschattens

Damit der Sonnenschatten angezeigt und berechnet werden kann, war es erforderlich eine nach Süden hin ausgerichtete und senkrecht stehende Hauswand zu definieren. Diese Hauswand ist im nachfolgenden Plot grau eingefärbt. Der braun dargestellte Zeigerstab wird im nächsten Schritt vom parallel zum Sonnenvektor verlaufenden orangen Sonnenstrahl getroffen und markiert mit dem Schnittpunkt der Hauswand das Ende des schwarz eingefärbten Schattenvektors.

Beachte: Der grüne, rote und blaue Vektor beschreibt die x,y,z-Achse des Koordinatensystems.

In [35]:
sundial.model_plot3d()

4.2. Interaktive Grafik zur Veranschaulichung der Linien des Ziffernblattes zu unterschiedlichen Tageszeiten

Nachfolgend ist eine interaktive Grafik implementiert worden. Diese zeigt jeweils zur vollen Stunde eine neue Linie auf dem Ziffernblatt an. Schiebt man den Stundenzeiger wahlweise nach links oder rechts, so lässt sich die Veränderung des Schattens auf dem Ziffernblatt erkennen.

Beachte: Wenn sich die Sonne hinter der Hauswand befindet, so wird durch die Methode sundial.get_shadow_on_wall() ein Nullvektor zurückgegeben, was bedeutet, dass auf der Wand durch die Abwesenheit der Sonne kein Schatten projiziert wird.

In [34]:
from ipywidgets import interact
import ipywidgets as widgets

def interactive_shadow(day_time):
    sundial.update_day_time(day_time)
    
    projected_shadow = sundial.get_shadow_on_wall()
    
    plot_projected_shadow = plot(projected_shadow)
    plot_projected_shadow.show(xmin=-3, xmax=3, ymin=-3, ymax=0, aspect_ratio=1, figsize=8)

interact(interactive_shadow, day_time=widgets.IntSlider(min=0, max=23, step=1, value=12))
Out[34]:
<function interactive_shadow at 0x6fdb73be730>

4.3. Ausgabe der Koordinaten der Schattenspitze

Nachfolgend wird die Spitze des Schattens, welche durch den Zeigerstab und den Sonnenstrahl auf die senkrecht stehende Hauswand projiziert wird, berechnet.

In [38]:
shadow_on_wall = sundial.get_shadow_on_wall()

print("Schattenvektor: {0}".format(shadow_on_wall))
Schattenvektor: (3.89678976556666, -6.11644255093315)

4.4. Ausgabe des Normalen-, Schatten-, Sonnen- und Zeigervektors

Nachfolgend finden sich die Befehle, um die wichtigesten für die Berechnung verwendeten Vektoren ausgeben zu können:

In [19]:
normal = sundial.get_normal().n()
sun_position = sundial.get_sun_position().n()
pointer = sundial.get_pointer().n()
shadow = sundial.get_shadow().n()

print("Normalenvektor: {0}".format(normal))
print("Sonnenvektor: {0}".format(sun_position))
print("Zeigerstab: {0}".format(pointer))
print("Schattenvektor: {0}".format(shadow))
Normalenvektor: (0.694658370458997, -0.719339800338651, 0.000000000000000)
Sonnenvektor: (0.512931754858803, -0.205540597293651, 0.833459103808722)
Zeigerstab: (4.33634379971919, 0.000000000000000, -4.84211962374299)
Schattenvektor: (1.27169208957638, 1.22805877591486, -9.82185005303688)

4.5. Ausgabe des Ziffernblattes der Sonnenuhr

Nachfolgend wird eine graphische Anzeige des Ziffernblattes generiert. Diese besteht aus allen durch den Zeigerstab auf die Wand geworfenen zweidimensionalen Schattenvektoren zur vollen Stunde. Wenn dieses Ziffernblatt ausgedruckt werden würde, so könnte man anhand der dargestellten Zeitlinien in Kombination mit dem durch den Zeiger auf die Hauswand geworfenen Schatten die aktuelle Tageszeit ablesen.

In [33]:
sundial.plot_dial()

5. Literaturverzeichnis

[1] N.N. (2009): A Walk Through Time - Early Clocks. National Institute of Standards and Technology. Online verfügbar unter https://www.nist.gov/pml/time-and-frequency-division/popular-links/walk-through-time/walk-through-time-early-clocks, zuletzt aktualisiert am 25.06.2019, zuletzt geprüft am 26.04.2020.
[2] Zwillinger, Daniel (2003): CRC Standard Mathematical Tables and Formulas, 31st Edition. 31st ed. Milton: CRC Press (Advances in Applied Mathematics). Online verfügbar unter https://ia802900.us.archive.org/16/items/StandardMathematicalTablesAndFormulae31stEditionZwillinger/Standard%20Mathematical%20Tables%20and%20Formulae%2031st%20Edition%20-%20Zwillinger.pdf, zuletzt geprüft am 26.04.2020.
[3] Sonnenstand (2020). Online verfügbar unter https://de.wikipedia.org/w/index.php?title=Sonnenstand&oldid=195707610, zuletzt aktualisiert am 11.01.2020, zuletzt geprüft am 26.04.2020.
[4] Ibrahim, Reda; Andreas, Afshin (2003): Solar Position Algorithm for Solar Radiation Applications (Revised). National Renewable Energy Laboratory. Online verfügbar unter https://www.nrel.gov/docs/fy08osti/34302.pdf, zuletzt aktualisiert am 23.01.2015, zuletzt geprüft am 26.04.2020.
[5] Smith, James (2017): Projection of a Vector upon a Plane from an Arbitrary Angle, via Geometric (Clifford) Algebra. Online verfügbar unter https://vixra.org/pdf/1712.0524v1.pdf, zuletzt geprüft am 02.05.2020.
[6] Ruhrländer, Michael (2017): Lineare Algebra für Naturwissenschaftler und Ingenieure. Lehr- und Übungsbuch mit MyMathLab, lineare Algebra. Hallbergmoos: Pearson (Mat - Mathematik).

6. Appendix

6.1. HERLEITUNG DER FORMELN FÜR DIE VEKTORDARSTELLUNG DES SCHATTENVEKTORS

Die nachfolgende Herleitung erfolgt nach einer Idee von Smith. [Ref: 5]

Schritt 1: Überführung der Geradengleichung in die Vektordarstellung

$\displaystyle \vec{s} = {\begin{bmatrix} p_{1} + \lambda ☉_{1} \\ p_{2} + \lambda ☉_{2} \\ p_{3} + \lambda ☉_{3} \end{bmatrix}}$

Schritt 2: Substitution des für $\lambda$ ermittelten Ausdrucks

$\displaystyle \lambda = - \frac{\vec{p} \cdot \vec{e}}{\vec{☉} \cdot \vec{e}} = - \frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}$

Durch Ersetzen von $\lambda$ mit dem obigen Ausdruck erhält man:

$\displaystyle \vec{s} = {\begin{bmatrix} p_{1} - \frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}} ☉_{1} \\ p_{2} - \frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}} ☉_{2} \\ p_{3} - \frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}} ☉_{3} \end{bmatrix}}$

Schritt 3: Vereinfachung der Schattenvektorkomponenten

x-Komponente:

$\displaystyle s_{1} = p_{1} - {\frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}} ☉_{1}$

$\displaystyle \,\,\,\,\,\, = p_{1} - {\frac{(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})☉_{1}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{1}(☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{1}(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{\require{cancel}\cancel{p_{1} ☉_{1} e_{1}} + p_{1} ☉_{2} e_{2} + p_{1} ☉_{3} e_{3} - \cancel{p_{1} ☉_{1} e_{1}} + p_{2} ☉_{1} e_{2} + p_{3} ☉_{1} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{1}(☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{1}(p_{2} e_{2} + p_{3} e_{3})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

y-Komponente:

$\displaystyle s_{2} = p_{2} - {\frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}} ☉_{2}$

$\displaystyle \,\,\,\,\,\, = p_{2} - {\frac{(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})☉_{2}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{2}(☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{2}(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{2} ☉_{1} e_{1} + \require{cancel}\cancel{p_{2} ☉_{2} e_{2}} + p_{2} ☉_{3} e_{3} - p_{1} ☉_{2} e_{1} + \require{cancel}\cancel{p_{2} ☉_{2} e_{2}} + p_{3} ☉_{2} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{2}(☉_{1} e_{1} + ☉_{3} e_{3}) - ☉_{2}(p_{1} e_{1} + p_{3} e_{3})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

z-Komponente:

$\displaystyle s_{3} = p_{3} - {\frac{p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}} ☉_{3}$

$\displaystyle \,\,\,\,\,\, = p_{3} - {\frac{(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})☉_{3}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{3}(☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{3}(p_{1} e_{1} + p_{2} e_{2} + p_{3} e_{3})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{3} ☉_{1} e_{1} + p_{3} ☉_{2} e_{2} + \require{cancel}\cancel{p_{3} ☉_{3} e_{3}} - p_{1} ☉_{3} e_{1} + p_{2} ☉_{3} e_{2} + \require{cancel}\cancel{p_{3} ☉_{3} e_{3}}}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

$\displaystyle \,\,\,\,\,\, = {\frac{p_{3}(☉_{1} e_{1} + ☉_{2} e_{2}) - ☉_{3}(p_{1} e_{1} + p_{2} e_{2})}{☉_{1} e_{1} + ☉_{2} e_{2} + ☉_{3} e_{3}}}$

Schritt 4: Aufstellen des Schattenvektors

Die x,y,z-Komponenten $s_{1}$, $s_{2}$ und $s_{3}$ bilden nun den Schattenvektor $\vec{s}$, welcher wie folgt definiert wird:

$\displaystyle \vec{s} = \frac{1}{e_{1} ☉_{1} + e_{2} ☉_{2} + e_{3} ☉_{3}} \cdot {\begin{bmatrix}p_{1}(☉_{2} e_{2} + ☉_{3} e_{3}) - ☉_{1} (p_{2} e_{2} + p_{3} e_{3})\\p_{2} (☉_{1} e_{1} + ☉_{3} e_{3}) - ☉_{2} (p_{1} e_{1} + p_{3} e_{3})\\p_{3} (☉_{1} e_{1} + ☉_{2} e_{2}) - ☉_{3} (p_{1} e_{1} + p_{2} e_{2})\\\end{bmatrix}}$

6.2. AUFGABENVERTEILUNG UNTER DEN TEAMMITGLIEDERN:

Implementierung der Gleichungen der Sonnenbahn

Bearbeitung durch Konrad Moron

Beschreibung einer allgemeinen Ursprungsebene durch Normalenvektor

Bearbeitung durch Stefan Kühnel

Definition und Implementierung des Zeigerstabs

Bearbeitung durch Stefan Kühnel

Berechnung der x,y-Koordinaten der Schattenspitze auf der Hauswand
  • Stefan Kühnel: Definition, Herleitung und Implementierung der Schattenvektorgleichung.
  • Konrad Moron: Recherche über Projektion eines dreidimensionalen Vektors auf eine zweidimensionale Ebene und anschließende Implementierung des entsprechenden Algorithmus.
Generierung des Ziffernblatts mit Schattenlinien zu jeder vollen Stunde

Bearbeitung durch Konrad Moron

Copyright (c) 2020 Stefan Kühnel; Konrad Moron, Alle Rechte vorbehalten, soweit nicht ausdrücklich anders vermerkt.