Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • p.anaguano/informatik2022
  • a.engelke/informatik2022
  • p.schleinzer/informatik2022
  • n.rosenkranz/informatik2022
  • w.weber/informatik2022
  • xuhao.zhang/informatik2022
  • h.el-menuawy/informatik2022
  • saifalla.ibrahim/informatik2022
  • d.krause/informatik2022
  • p.schilling/informatik2022
  • j.tolke/informatik2022
  • f.luckau/informatik2022
  • danish.ahmad/informatik2022
  • emir.sagdani/informatik2022
  • d.griedel/informatik2022
  • j.mahnke/informatik2022
  • l.poehler/informatik2022
  • christoph.wrede/informatik2022
  • y.kummert/informatik2022
  • alexander.reisch/informatik2022
  • t.dickel/informatik2022
  • ni.petersen/informatik2022
  • markus.werner/informatik2022
  • s.mouammar/informatik2022
  • j.jahns/informatik2022
  • m.figueroa-castillo/informatik2022
  • b.hannan/informatik2022
  • v.lapschiess/informatik2022
  • j.hegner/informatik2022
  • g.paraschiv/informatik2022
  • e.abkaimov/informatik2022
  • l.krogmann/informatik2022
  • d.mizyed/informatik2022
  • h.almasri/informatik2022
  • a.mickan/informatik2022
  • f.shikh-alshabab/informatik2022
  • j.feldbausch/informatik2022
  • l.abdel-kader/informatik2022
  • jan.seibt/informatik2022
  • e.goekmen/informatik2022
  • nathanael.schenk/informatik2022
  • r.reksius/informatik2022
  • edmont.schulze/informatik2022
  • a.singh/informatik2022
  • p.christensen/informatik2022
  • m.woidt/informatik2022
46 results
Show changes
%% Cell type:markdown id:6b8a8d99 tags:
# <font color='blue'>**Übung 11 - Primfaktorzerlegung und Speichern von objektorientierten Datenstrukturen**
%% Cell type:markdown id:297a4311 tags:
In dieser Übung soll eine objektorientierte Umgebung zur Berechnung von Primzahlzerlegungen aufgebaut werden. Dies wiederholt Konzepte der objektorientierten Programmierung und die Umsetzung von formulierten Abläufen in Code. Die Objekte werden anschließend in Dateien gespeichert und wieder eingelesen.
### <font color='blue'>**Vorkenntnisse - Notebooks**
* Übung 1
* Übung 2
* Übung 3
* Übung 10
* Grundlagen Python
* Grundlagen Objektorientierte Programmierung
* Grundlagen Dateien
### <font color='blue'>**Notebooks, die helfen können**
* Zusatznotebook "Dateien"
### <font color='blue'>**Lernziele**
* Wiederholung OOP
* Umsetzung mathematischer Abläufe in Algorithmen
* Lesen und Schreiben von hierarchischen Strukturen (OOP) in Dateien
%% Cell type:markdown id:71332cc2 tags:
## <font color='blue'>**Problemstellung 1: Primfaktorzerlegung**
<font color='blue'>**Aufgabenstellung**
Es soll ein objektorientiertes Programm erstellt werden, mit dem sich die Primfaktorzerlegung von beliebigen Ganzzahlen berechnen lässt. Die bereits berechneten Zerlegungen sollen gespeichert werden.
<font color='blue'>**Vorüberlegung**
*Hintergrund Primfaktorzerlegung*
Jede Ganzzahl größer $2$ lässt sich in Faktoren aus Primzahlen zerlegen. $40$ setzt sich zum Beispiel aus $2^3\cdot5$ zusammen, $100$ aus $2\cdot2\cdot5\cdot5 = 2^2\cdot5^2$. Für jede Zahl gibt es exakt eine Primfaktorzerlegung. Die Primfaktorzerlegung wird z.B. genutzt, wenn der größte gemeinsame Teiler (ggT) oder das kleinste gemeinsame Vielfache (kgV) zweier Zahlen gesucht wird.
Den ggT benötigt man z.B., um einen Bruch zu kürzen. Man erhält den ggT, indem man die gemeinsamen Primfaktoren multipliziert. Bei $40 = 2^3\cdot5^1$ und $100 = 2^2\cdot5^2$ ist das $2^2\cdot5^1 = 20$. Wir können also den Bruch $\frac{40}{100}$ mit $20$ kürzen: $\frac{2}{5}$.
Das kgV benötigt man zum Beispiel zum Addieren von Brüchen mit unterschiedlichem Nenner. Die Brüche können nur addiert werden, wenn sie auf einen gemeinsamen Nenner (von denen das kgV die kleinste Möglichkeit ist) gebracht werden. Das kgV erhält man, indem alle auftretenden Primfaktoren in ihrer Höchsten Potenz multipliziert werden. Bei den Beispielzahlen 40 und 100 entspricht das $2^3\cdot5^2 = 200$. 40 müsste also mit 50, 100 mit 2 erweitert werden.
*Berechnung Primfaktorzerlegung*
Die Primfaktorzerlegung kann berechnet werden, indem wir die Zahl durch die kleinste Primzahl teilen, bei der die Division ganzzahlig aufgeht. Diese Primzahl ist ein Primfaktor. Mit dem Ergebnis der Division wird wieder Verfahren, wie mit der Zahl selbst. Wenn die verbleibende Zahl selbst eine Primzahl ist (und dann als letzter Faktor eingeht), bzw. nur durch sich selbst geteilt werden kann und 1 ergibt, sind alle Primfaktoren gefunden. Ausführliches Bsp:
$100 : 2 = 50$. Neuer Primfaktor 2, neue Zahl 50, Primfaktoren: $2^1$\
$50 : 2 = 25$. Neuer Primfaktor 2, neue Zahl 25, Primfaktoren: $2^2$\
$25 : 2 = $ (keine Ganzzahl), nächsthöhere Primzahl testen\
$25 : 3$ -> (keine Ganzzahl), nächsthöhere Primzahl testen\
$25 : 5 = 5$ Neuer Primfaktor 5, neue Zahl 5, Primfaktoren: $2^2\cdot5^1$\
5 ist selbst Primzahl. Primfaktoren: $2^2\cdot5^2$
*Programm*
Diesen Algorithmus wollen wir gleich in einem Programm umsetzen. Wir benötigen dafür eine Liste mit allen Primzahlen, die als Primfaktor in Frage kommen (also alle Primzahlen bis zur Zahl selbst, da sie im Extremfall, wenn sie selbst Primzahl ist, der einzige Primfaktor ist). Darum kümmern wir uns später und nehmen die Liste nun erstmal als gegeben an.
Wir wollen die Primfaktorzerlegung als Liste von Zahlen (den Primfaktoren) ausgeben. Eine Anforderung aus der Problemstellung ist, dass bereits berechnete Zerlegungen gespeichert werden sollen. Hierfür bietet sich ein Dictionary an. Als Key verwenden wir die Zahl, die zerlegt wird, und als Wert speichern wir die Liste mit der Primfaktorzerlegung.
Das Programm soll objektorient sein, um die Daten zu strukturieren. Wir stellen eine Klasse als Werkzeug für die Primfaktorzerlegung zusammen, die folgende Elemente hat:
````class Primfaktor_Werkzeug````
* Attribute:
* Eine Liste mit lückenlos aufsteigenden Primzahlen bis zur aktuellen Grenze ````prim_liste````
* Ein Dictionary mit allen bisher berechneten Zerlegungen in der Form {Zahl:Liste_mit_Primfaktoren} ````zerlegungen````
* Der Kern ist eine Methode, die:
* die Primfaktorzerlegung für eine gegebene Zahl berechnet, ausgibt, und in das Dictionary einträgt ````zerlegung(zahl)````
* Hilfsmethoden sind:
* Der Konstruktor ````__init__()````
* get-Methoden für die Attribute, um außerhalb der Klasse auf Kopien der Attribute zuzugreifen ````get_prim_liste()````,````get_zerlegungen()````
*Primzahl-Liste*
Wir benötigen eine Liste mit allen Primzahlen bis zur aktuell angefragten Zahl (s.o.). Wenn wir diese manuell eingeben, könnte es immer passieren, dass die Liste nicht weit genug geht. Es ist also besser, diese Liste vom Programm bis zur aktuell benötigten höchsten Zahl erstellen zu lassen. Wir könnten diese Funktionalität zwar auch in die Klasse ````Primfaktor_Werkzeug```` integrieren, aber eigentlich geht es in der Klasse um Primfaktorzerlegungen, und nicht um Primzahlen selbt. In der OOP versucht man immer, den Umfang der Klassen so klein wie möglich zu halten, sodass sie nur eine Kernaufgabe haben. Das steigert die Wiederverwendbarkeit und Wartbarkeit. Eine Liste von Primzahlen könnte auch in diversen anderen Programmen nützlich sein, sodass wir eine neue Klasse ````Primzahl_Liste```` anlegen. Im Kern hat diese Klasse eine Liste mit Zahlen als Attribut. Wir statten sie jedoch mit Methoden aus, die dafür sorgen, dass es sich um eine erweiterbare, aufsteigende Liste mit Primzahlen handelt. Die Primzahl-Liste in der ````Primfaktor_Werkzeug```` Klasse wird dann ein Objekt der Klasse ````Primzahl_Liste```` sein.
Unsere neue Klasse ````Primzahl_Liste```` erhält folgende Elemente:
* Attribute:
* Eine Liste mit lückenlos aufsteigenden Primzahlen bis zur aktuellen Grenze ````primzahlen````
* Methoden:
* Die Kernmethode, die die Liste lückenlos bis zu einer gegebenen Zahl erweitert ````liste_erweitern_bis(zahl)````
* Den Konstruktor ````__init__()````
* Eine get-Methode, um außerhalb der Klasse auf eine Kopie der Liste zuzugreifen ````get_primzahlen()````
* Zwei interne Methode, die prüfen, ob eine Zahl eine Primzahl ist ````_ist_primzahl(zahl)````, und ob eine Zahl durch eine andere teilbar ist ````_ist_teilbar(zahl, teiler)```` (das ist nicht zwingend nötig, allerdings sorgt das Auslagern dieser Funktionen für deutlich verständlicheren Code)
Bevor wir mit der Implementierung beginnen, noch kurz die Überlegung zur Erweiterung der Liste:
Wenn die Liste bis zu einer übergebenen Zahl erweitert werden soll, sollten wir zunächst prüfen, ob die Liste bereits groß genug ist. Wenn nicht, müssen wir jede Zahl zwischen dem Ende der Liste und dem Ziel der Erweiterung darauf testen, ob sie eine Primzahl ist. Falls ja, wird sie der Liste angehängt. Falls nicht, wird die nächste Zahl getestet.
Ob die Zahl eine Primzahl ist, finden wir heraus, indem wir die Zahl mit allen bekannten Primzahlen aus der Liste testen. Ist sie durch keine der Primzahlen ganzzahlig teilbar, ist sie selbst eine Primzahl. Sonst nicht.
Ob eine Zahl durch einen Teiler ganzzahlig teilbar ist, finden wir heraus, indem wir die Zahl mit dem Modulo-Operator (% - Division mit Rest) und dem Teiler testen. Kommt bei der Division Rest 0 heraus, ist die Zahl teilbar, ansonsten nicht.
Anhand dieser Beschreibung lässt sich vielleicht schon nachvollziehen, warum es sinnvoll ist, das Testen, ob eine Zahl eine Primzahl ist, und ob eine Zahl durch eine andere Zahl ganzzahlig teilbar ist, auszulagern. So bleibt jede Methode kompakt beschreibbar und übersichtlich.
%% Cell type:markdown id:06c38e96 tags:
<font color='blue'>**Umsetzung**
Wir beginnen mit der Klasse ````Primzahl_Liste````, da wir diese dann später für die Zerlegung bereits verwenden können. Wir werden die Klasse zunächst implementieren und für sich testen, damit wir sicher sein können, dass dieser Baustein funktioniert und im weiteren Verlauf verwendet werden kann. Aufgrund des kompakten Codes mit relativ aussagekräftigen Namen, und der Erklärung in dieser Vorbesprechung ist relativ wenig im Code selbst kommentiert.
**Generell sollte Code, bzw. die Namen, so aussagekräftig sein und fein aufgegliedert sein, dass man wenig Kommentare benötigt.**
*Konstruktor*
Es wäre sinnvoll, dem Konstruktor bereits eine Zahl, bis zu der die Liste erstellt werden soll, zu übergeben. Der Konstruktor kann auch die klasseneigene Methode zum Erweitern der Liste aufrufen. Dazu braucht es nur eine Startliste mit den ersten Primzahlen. Diese Liste könnte ````[2,]```` sein, oder ````[2,3]````. Die 3 wird eigentlich bereits automatisch von unserem Algorithmus für das Erweitern gefunden, aber nach meinem persönlichen Geschmack sieht die Liste mit nur einem Element weniger vollständig aus, als mit zwei Elementen. Daher entscheide ich mich für ````[2,3]````.
*Liste erweitern bis*
Diese Methode benötigt auch die Zahl, bis zu der erweitert werden soll. Als erstes benötigen wir den Startwert. Dieser ist der letzte Eintrag in der aktuellen Primzahlliste. Falls der Zielwert kleiner ist als der Startwert, müssen wir gar nichts mehr machen. Die Liste ist dann ja bereits lang genug. Ansonsten können wir mit dem Erweitern beginnen. Dazu gehen wir in einer Schleife jede Zahl vom Startwert bis zum Endwert durch (bei Verwendung von ````range()```` müssen wir dran denken, dass die obere Grenze exklusiv ist - den Wert also nicht mehr verwendet). Wir nutzen die interne Funktion ````_ist_primzahl(zahl)````, um zu prüfen, ob die Zahl eine Primzahlen ist. Ist sie das, hängen wir sie an die Primzahlliste an. (Falls sie keine ist, müssen wir ncihts weiter unternehmen. Es wird dann direkt der nächsten Schleifendurchlauf mit der nächsten Zahl begonnen).
*_ist_primzahl*
Diese ausgelagerte Methode testet, ob eine Zahl eine Primzahl ist. Dazu benötigen wir eine Schleife über alle bekannten Primzahlen, und testen, ob die Zahl ganzzahlig durch eine dieser Primzahlen teilbar ist. Finden wir einen möglichen teilbar, können wir die Funktion bereits mit ````False```` beenden. Wurden alle Primzahlen getestet, ohne dass ein Teiler gefunden wurde, geben wir ````True```` zurück.
*_ist_teilbar*
Diese Methode teilt die Zahl mittels Ganzzahldivision mit Rest (Modulo, Operator %) durch den Teiler. Gibt es keinen Rest (Rest 0), so ist die Zahl teilbar und wir geben ````True```` zurück. Ansonsten ````False````. Da der Ausdruck in der Abfrage bereits den Wahrheitswert als Ergebnis hat, können wir diesen auch direkt zurückgeben, ganz ohne if-Abfrage. (````return ausdruck```` statt ````if ausdruck: return true else: return false```` )
Das ist ein gutes Beispiel, wie das feine Aufgliedern in kleine Funktionen die Übersichtlichkeit erhöhen kann. Dadurch, dass die Funktion den Namen ````_ist_teilbar```` erhält, können wir sie in der Primzahlprüfung nutzen, und es ist sofort ersichtlich, was in der Zeile passiert (````if self._ist_teilbar(zahl, teiler):````). Die gleiche Zeile könnten wir ohne die Funktion schreiben als (````if zahl%teiler == 0:````). Das hat zwar weniger Zeichen, aber es ist viel schlechter ersichtlich, dass hier eine Zahl auf Teilbarkeit geprüft wird. Dazu müsste man nämlich erstmal nachvollziehen, was bei der Modulo-Operation passiert, und was es bedeutet, wenn dabei 0 herauskommt.
*get_primzahlen*
Die Methode gibt eine Kopie der Liste zurück. Dafür kann ein Slicing über alle Elemente genutzt werden.
%% Cell type:code id:b31782a6 tags:
``` python
class Primzahl_Liste:
def __init__(self, ziel):
self.primzahlen = [2,3]
self.liste_erweitern_bis(ziel)
def liste_erweitern_bis(self, ziel):
start = self.primzahlen[-1]
if ziel > start:
for zahl in range(start, ziel+1):
if self._ist_primzahl(zahl):
self.primzahlen.append(zahl)
def _ist_primzahl(self, zahl):
for teiler in self.primzahlen:
if self._ist_teilbar(zahl, teiler):
return False
return True
def _ist_teilbar(self, zahl, teiler):
return zahl%teiler == 0
def get_primzahlen(self):
return self.primzahlen[:]
```
%% Cell type:markdown id:2c7d1857 tags:
Wir testen die neue Klasse:
%% Cell type:code id:bc4c2eb5 tags:
``` python
print("Test der Klasse Primzahl_Liste:")
# Erstellt die Primzahlen bis 30 als Liste
primes = Primzahl_Liste(30)
print(primes.get_primzahlen())
# Erweitert bis 200
primes.liste_erweitern_bis(200)
print(primes.get_primzahlen())
```
%% Output
Test der Klasse Primzahl_Liste:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]
%% Cell type:markdown id:764ba627 tags:
Nachdem wir nun eine funktionierende Primzahl_Liste haben, können wir uns dem eigentlichen Ziel der Aufgabe widmen, der Primfaktorzerlegung.
Wir haben die Grundstruktur der Klasse bereits entworfen und überlegen nun die Details der Implementierung.
*Konstruktor*
Die erste Frage ist, ob wir der ````__init__()````-Methode Parameter übergeben müssen. Wir könnten zwar eine erste Zahl zur Zerlegung übergeben, müssen aber nicht. Wir werden daher in dieser Klasse auf Parameter bei der Objekterstellung (Instanziierung) verzichten. Die init Methode legt die beiden Attribute an: eine Primzahlliste, die wir zum Beispiel mit 5 initialisieren und ein leeres Dictionary, in dem wir später Ergebnisse ablegen können.
*zerlegung*
Das ist die eigentliche Kernmethode. Sie bekommt die zu zerlegende Zahl übergeben. Wir müssen zunächst überprüfen, ob die eingegebene Zahl kleiner als 2 ist, da dann keine Primfaktorzerlegung gemacht werden kann. Wir könnten einen Fehler erzeugen (das haben wir aber bisher nicht in Übungen behandelt), belassen es aber hier dabei, eine 1 zurückzugeben.
Dann müssen wir sicherstellen, dass die Primzahlliste weit genug reicht. Dazu erweitern wir sie (mit ihrer Methode) bis zur eingegebenen Zahl.
Da wir im folgenden Algorithmus die Zahl verändern, speichern wir die Zahl als ````save_key```` ab, damit wir später beim beschreiben des Dictionaries noch wissen, welche Zahl zerlegt wurde.
Wir benötigen eine leere Liste, die wir mit Primfaktoren füllen.
Nach diesen Vorbereitungen beginnt die eigentliche Berechnung nach dem Schema, das in der Vorüberlegung erklärt wurde. Dabei teilen wir die Zahl immer so oft durch eine Primzahl, wie möglich und gehen dann zur nächsthöheren, bis wir alle Primzahlen getestet haben. Programmtechnisch setzen wir das so um, dass wir als erstes eine Schleife über alle Primzahlen der Primzahlliste starten. Nun prüfen wir, ob die eingegebeneZahl ganzzahlig durch ide Primzahl teilbar ist. Hierbei müssen wir bedenken, dass wir noch nicht wissen, wie oft die Zahl teilbar ist. Würden wir eine if-Abfrage verwenden, könnten wir die Zahl nur ein Mal teilen (oder sooft, wieviele if Abfragen wir aneinander reihen). Daher ist das ein idealer Anwendungsfall für eine While-Schleife. Nur wenn die Bedingung erfüllt ist, wird der Inhalt abgearbeitet (wie bei if), aber danach wird geprüft, ob die Bedingung immer noch gültig ist, und dann gegebenfalls wiederholt. Analog zur Primzahlliste lagern wir die Methode ````_ist_teilbar```` aus, um den Quellcode verständlich zu halten. In der Schleife teilen wir die Zahl durch die Primzahl und nutzen dafür den kombinierten Zuweisungsoperator ````/=````. Außerdem fügen wir die Primzahl der Liste von Primfaktoren hinzu.
Diese Schleife führt bereits die gesamte Primzahlzerlegung durch. Am Ende verbleibt die Zahl bei 1. 1 kann gar nicht ganzzahlig geteilt werden und so werden zwar alle Primzahlen, die noch nicht dran waren getestet, aber es passiert nichts weiter. Man könnte noch überlegen, Abbruchbedingungen einzubauen, um aufzuhören, wenn die Zahl bei 1 angekommen ist. Wir verzichten hier allerdings darauf.
Zuletzt soll das Ergebnis in dem Dictionary gespeichert werden. Dazu legen wir einen Eintrag an und verwenden als Key die zwischengespeicherte Zahl und als Wert die Liste der Primfaktoren. Diese Liste geben wir auch mit return zurück.
*get_zerlegungen*
Diese Methode gibt eine Kopie des Dictionaries zurück. Wir verwenden die .copy() Methode.
*get_prim_liste*
Diese Methode gibt die Primzahlliste zurück. Wir nutzen dazu die entsprechende Methode der Primzahlliste und geben das Ergebnis lediglich weiter.
%% Cell type:code id:56edc06e tags:
``` python
class Primfaktor_Werkzeug:
def __init__(self):
self.prim_liste = Primzahl_Liste(5)
self.zerlegungen = {}
def zerlegung(self, zahl):
if zahl < 2:
return 1
self.prim_liste.liste_erweitern_bis(zahl)
save_key = zahl
primfaktoren = []
for teiler_primzahl in self.get_prim_liste():
while self._ist_teilbar(zahl, teiler_primzahl):
zahl/=teiler_primzahl
primfaktoren.append(teiler_primzahl)
self.zerlegungen[save_key]=primfaktoren[:]
return primfaktoren
def _ist_teilbar(self, zahl, teiler):
return zahl%teiler == 0
def get_zerlegungen(self):
return self.zerlegungen.copy()
def get_prim_liste(self):
return self.prim_liste.get_primzahlen()
```
%% Cell type:markdown id:9a6f752c tags:
Auch diese Klasse testen wir. Zunächst mit den beiden Beispielen aus der Vorüberlegung. Anschließend verwenden wir eine Schleife, um alle Zerlegungen bis z.B. 20 zu ermitteln und lassen uns das Ergebnis Dictionary ausgeben.
%% Cell type:code id:7fc572b9 tags:
``` python
# Test der Klasse Primfaktorzerlegung
pf_werkzeug = Primfaktor_Werkzeug()
print(pf_werkzeug.zerlegung(40))
print(pf_werkzeug.zerlegung(100))
# Alle Zerlegungen bis einschliesslich 20
for zahl in range(21):
pf_werkzeug.zerlegung(zahl)
# Ausgabe der gespeicherten Zerlegungen
print(pf_werkzeug.get_zerlegungen())
```
%% Output
[2, 2, 2, 5]
[2, 2, 5, 5]
{40: [2, 2, 2, 5], 100: [2, 2, 5, 5], 2: [2], 3: [3], 4: [2, 2], 5: [5], 6: [2, 3], 7: [7], 8: [2, 2, 2], 9: [3, 3], 10: [2, 5], 11: [11], 12: [2, 2, 3], 13: [13], 14: [2, 7], 15: [3, 5], 16: [2, 2, 2, 2], 17: [17], 18: [2, 3, 3], 19: [19], 20: [2, 2, 5]}
%% Cell type:markdown id:a5090804 tags:
## <font color='blue'>**Problemstellung 2: Speichern in Dateiformaten**
<font color='blue'>**Aufgabenstellung**
Damit andere Programme die berechneten Primzahlen und Primfaktorzerlegungen nutzen können, sowie neue Primfaktor_Werkzeug Objekte ohne Berechnung wieder mit den Daten gefüllt werden können, soll eine Datei- Ein- und Ausgabe vorbereitet werden.
* Es soll eine Methode implementiert werden, mit der die Daten eines Primfaktor_Werkzeug Objekts in einer Zeichenkette formatiert werden (die man dann in eine Datei schreiben könnte). Das Format ist frei wählbar, soll aber Menschenlesbar und verständlich sein.
* Ebenso wird eine Methode benötigt, die ein neues Primfaktor_Werkzeug Objekt erstellt und mit den Daten aus der Zeichenkette füllt.
* Zur einfacheren Übertragung innerhalb Pythons soll eine Speicherung mittels ````pickle```` (Binärdatei) ermöglicht werden.
<font color='blue'>**Vorüberlegung**
Für den ersten Teil der Aufgabe wollen wir eins der im Grundlagennotebook vorgestellten Standardformate nutzen und stellen die Möglichkeiten gegenüber. Wir haben eine leicht hierarchische Datenstruktur: Auf der ersten Ebene eine Liste mit Primzahlen und ein Dictionary mit den Primzahlzerlegungen. Auf der zweiten Ebene haben wir die einzelnen Primzahlzerlegungen als Listen mit beliebiger Länge.
Die Daten sind daher nicht sehr geeignet für **csv**, wobei man auch für **csv** Lösungen finden kann.
*(Für Interessierte: z.B. eine Tabelle mit Spalten für alle Primzahlen der Primzahl-Liste und Zeilen für die einzelnen Primfaktorzerlegungen nach Zahl. In der Zellen könnte dann für den entsprechenden Primfaktor der Exponent eingetragen werden. Eine solche Tabelle würde auch alle Daten enthalten, die Umwandlung in das Format wäre aber etwas aufwendiger, als wir es hier in der Augabe machen wollen)*
Wir können nun also zwischen **json** und **xml** wählen. Beides würde funktionieren und die Anforderungen erfüllen. Wir entscheiden uns hier für **json**, da es etwas einfacher aufzubauen ist.
Wir werden dazu eine Funktion definieren, die ein Objekt der Klasse Primfaktor_Werkzeug erhält. Sie legt dann ein Dictionary mit den Einträgen für die Primzahlliste und die Primzahlzerlegungen an. Als Wert für die Primzahlzerlegungen wird wieder ein Dictionary verwendet. Wir können dieses direkt aus dem Objekt kopieren. und müssen es nicht weiter umformatieren. Zuletzt nutzen wir das ````json```` Modul, um das Gesamt-Dictionary nach json zu formatieren.
Für das Einlesen definieren wir eine weitere Funktion, die eine Zeichenkette im json-Format erhalten soll, in der die zuvor definierte Dictionary-Struktur enthalten ist. Die Funktion erstellt ein neues Primzahl_Werkzeug Objekt und greift direkt auf die Attribute zu, um die in der json-Zeichenkette enthaltenen Informationen einzutragen, ohne dass etwas berechnet werden muss.
Achtung: Wir greifen hier direkt auf die Attribute zu. Das ist ursprünglich in der Idee der Datenkapselung bei Objektorientierter Programmierung nicht vorgesehen. Unsere Klasse ist so aufgebaut, dass in der Primzahlliste wirklich nur Primzahlen lückenlos und in aufsteigender Reihenfolge stehen können (da sie selbst berechnet werden). Die Primfaktorzerlegung ist darauf angewiesen, dass die Liste genau diese Bedingungen erfüllt. Ansonsten liefert sie falsche Ergebnisse. Mit dem direkten Zugriff auf die Primzahlliste könnten wir aber theoretisch eintragen, was wir wollen. Das sollte man beachten.
Der letzte Teil der Aufgabenstellung ist simpel zu lösen. Das Modul ````pickle```` übernimmt fast alle Schritte. Wir müssen nur eine Datei öffnen und mit ````pickle.dump()```` das Objekt darin speichern, bzw. mit ````pickle.load()```` wieder laden. Wir verpacken diese Funktionalität auch in Funktionen.
%% Cell type:markdown id:327071f5 tags:
<font color='blue'>**Umsetzung**
Wir beginnen mit der Funktion ````pfw_objetct_to_json_string()````. Sie bekommt ein Objekt der Klasse Primfaktor_Werkzeug. Wir legen ein Dictionary ````json_dict```` an und tragen zunächst unter einem Key "Primzahlen" die Primzahlliste ein, die wir über die Methoden des Objekts erhalten. Anschließend legen wir einen weiteren Key "gespeicherte Zerlegungen" an und tragen analog das Dictionary der Zerlegungen ein, wie es vom Objekt ausgegeben wird. Damit ist das Dateiformat vorbereitet und wir können im letzten Schritt mit ````json.dumps()```` die Datenstruktur in das json Format umwandeln.
Wir testen es durch vollständige Ausgabe des Json-Strings, der aus dem oben getesteten Objekt erzeugt wird.
%% Cell type:code id:e135839b tags:
``` python
import json
def pfw_object_to_json_string(pfw_object):
json_dict = {"Primzahlen": pfw_object.get_prim_liste()}
json_dict["gespeicherte Zerlegungen"] = pfw_object.get_zerlegungen()
return json.dumps(json_dict, indent=4)
print(pfw_object_to_json_string(pf_werkzeug))
```
%% Output
{
"Primzahlen": [
2,
3,
5,
7,
11,
13,
17,
19,
23,
29,
31,
37,
41,
43,
47,
53,
59,
61,
67,
71,
73,
79,
83,
89,
97
],
"gespeicherte Zerlegungen": {
"40": [
2,
2,
2,
5
],
"100": [
2,
2,
5,
5
],
"2": [
2
],
"3": [
3
],
"4": [
2,
2
],
"5": [
5
],
"6": [
2,
3
],
"7": [
7
],
"8": [
2,
2,
2
],
"9": [
3,
3
],
"10": [
2,
5
],
"11": [
11
],
"12": [
2,
2,
3
],
"13": [
13
],
"14": [
2,
7
],
"15": [
3,
5
],
"16": [
2,
2,
2,
2
],
"17": [
17
],
"18": [
2,
3,
3
],
"19": [
19
],
"20": [
2,
2,
5
]
}
}
%% Cell type:markdown id:da5bfbeb tags:
Als nächstes das Einlesen: ````json_string_to_pfw_object````. Hier gehen wir sozusagen rückwärts vor. Die Methode erhält die Zeichenkette. Wir erstellen ein Objekt der Klasse Primfaktor_Werkzeug. Wir nutzen ````json.loads()```` um die Zeichenkette in ein Python Dictionary umzuwandeln. Dann greifen wir direkt auf die Attribute des neuen Objekts zu und tragen mittels der oben definierten Keys die entsprechenden Inhalte ein. Dabei müssen wir Besonderheit beachten: Die Dictionaries in json können als Key nur Zeichen haben. Wir verwenden aber eigentlich Ganzzahlen. Statt also "gespeicherte Zerlegungen" sofort in das Attribut "zerlegungen" einzutragen, iterieren wir über alle Keys in "gespeicherte Zerlegungen" und legen im Attribut "zerlegungen" jeden Eintrag einzeln an, indem wir den Key zu int konvertieren und dann verwenden, und als Wert den Inhalt aus "gespeicherte Zerlegungen" am entsprechenden Key eintragen. Zum Schluss wird das neue Objekt zurückgegeben.
Wir testen es, indem wir zunächst das oben getestete Objekt zu json konvertieren, und dann ein neues Objekt mit den Daten einlesen.
%% Cell type:code id:6ba1d95c tags:
``` python
def json_string_to_pfw_object(json_string):
pfw_object = Primfaktor_Werkzeug()
json_dict = json.loads(json_string)
pfw_object.prim_liste = json_dict["Primzahlen"]
# Keys in json sind immer str. Wir benoetigen aber int. Deswegen legen wir die Antraege in der Schleife einzeln an, anstatt das Attribut analog zur Liste direkt zu ueberschreiben.
for key in json_dict["gespeicherte Zerlegungen"]:
pfw_object.zerlegungen[int(key)] = json_dict["gespeicherte Zerlegungen"][key]
return pfw_object
#Test
save_string = pfw_object_to_json_string(pf_werkzeug)
imported_pfz = json_string_to_pfw_object(save_string)
print(imported_pfz.zerlegungen[20])
```
%% Output
[2, 2, 5]
%% Cell type:markdown id:8641deed tags:
Zuletzt ````pickle````, mit dem wir recht einfach ein Objekt zur späteren Verwendung in Python in einer Binärdatei ablegen können. Allerdings darf sich nicht die Python Version ändern und die Klassendefinition muss beim Laden der Pickledatei bekannt und unverändert sein.
Wir erstellen zwei Funktionen. Die Funktion zum Speichern erhält ein Objekt und einen Dateinamen. Die Funktion zum Laden benötigt nur einen Dateinamen. In der Funktion öffnen wir wie im Grundlagennotebook die Datei im entsprechenden Modus (````wb````,````rb````) und nutzen ````dump```` oder ````load```` um das Objekt in der Datei abzulegen, oder auszulesen. Wir testen das analog zu json:
%% Cell type:code id:8c336583 tags:
``` python
import pickle
def pfw_object_to_pickle(pfw_object, filename):
with open(filename, "wb") as f:
pickle.dump(pfw_object, f)
def pfw_object_from_pickle(filename):
with open(filename, "rb") as f:
return pickle.load(f)
#Test:
pfw_object_to_pickle(pf_werkzeug, "Primfaktoren.pkl")
object_from_pickle = pfw_object_from_pickle("Primfaktoren.pkl")
print(object_from_pickle.zerlegungen[20])
```
%% Output
[2, 2, 5]
%% Cell type:markdown id:35dd9ca7 tags:
## <font color='blue'> **Schlussbemerkung**
In dieser Übung haben wir noch einmal für eine neue Problemstellung eine Klassenstruktur entwickelt und mathematische Berechnungsvorschriften aus einer Beschreibung als Algorithmus in Code umgesetzt. Wir haben versucht, den Code so nachvollziehbar wie möglich zu gestalten, indem wir nicht viele Schleifen und Abfragen verschachtelt haben, sondern die Berechnung in kleinere, abgetrennte Bausteine mit aussagekräftigem Namen zerlegt haben. Anschließend haben wir diese hierarchische Struktur in geeignete Dateiformate für verschiedene Zwecke überführt.
Das schließt die Inhalte des ersten Semesters ab. Das Ziel war, möglichst anwendungsorientiert die Herangehensweise an die Erstellung von Skripten näherzubringen und diverse nützliche Pakete vorzustellen und zu sehen, wie man diese einsetzen kann. Alle Pakete bieten mehr, als wir im Rahmen der Übung zeigen konnten. Wir sind sicher, dass euch diese Inhalte oder Teile davon noch auf die ein- oder andere Weise nützlich sein können. Ihr habt nun viel gesehen, und könnt viele der Begriffe einordnen, die euch noch/wieder begegnen werden.
## <font color='blue'> **Ausblick**
In der letzten Übung beschäftigen wir uns nach dem Testat mit einem kleinen Extra: Einer einfachen 3D-Animation. Das entsprechende Python-Paket (vpython) macht sehr viel selbst, sodass es deutlich einfacher zu benutzen ist, als man vielleicht bei dem Begriff 3D_Animation denken würde.
## <font color='blue'> **Aufgaben zum selbst probieren**
* Erstelle Methoden für die Primfaktor_Werkzeug-Klasse, die den größten gemeinsamen Teiler (ggT) und das kleinste gemeinsame Vielfache (kgV) zweier Zahlen ausgeben soll. In den Methoden müssen für beide Zahlen die Primfaktorzerlegungen gemacht, verglichen und ausgewertet werden. Die Berechnung von ggT und kgV ist in der Vorüberlegung von Problemstellung 1 beschrieben.
* Die Berechnung der Primzahlzerlegung ist relativ simpel implementiert, aber nicht sehr rechenschritt-effizient (unnötige Berechnungen). Versuche, die Effizienz der Berechnung zu verbessern. Anhaltspunkte hierfür sind: Müssen wir wirklich bei dem Test, ob eine Zahl eine Primzahl ist, alle Primzahlen testen? Können wir die bereits abgespeicherten Zerlegungen oder Primzahlen zu unserem Vorteil nutzen und uns Rechenschritte sparen? Nutze die in Übung 9 gezeigte Methode, um die Rechenzeiten zu bestimmen. (Hinweise: Hohe Zahlen zeigen die Unterschiede deutlicher, die Zeiten sind z.B. wegen der gespeicherten Primzahlliste auch immer abhängig von den zuvor berechneten Werten)
* Erstelle eine Bruch-Klasse, die die entwickelten Methoden nutzt, um den Bruch zu Kürzen und Brüche zu addieren (und Subtrahieren). Nutze dazu die Magic-Methods (Vgl. Grundlagen OOP) und ergänze Multiplikation und Division.
* Baue eine export und import Funktion für xml. Orientiere dich dabei an dem Vorgehen im "Grundlagen Dateien" Notebook, und dem Zusatznotebook "Dateien".
Füge deiner Kopie des Notebooks beliebig viele Codezellen hinzu, um die Aufgaben zu lösen.
%% Cell type:code id:7d710a72 tags:
``` python
```
%% Cell type:markdown id:049cfa99 tags:
# <font color='blue'>**Zusatz - "Dateien"**</font>
Im Grundlagennotebook zu Dateien wurde anhand von 2 Beispielen gezeigt, wie json und xml Dateien aussehen können. Anhand des kompakten ersten Beispiels wurde auch die Umsetzung in Python gezeigt. In diesem Zusatznotebook sind die Quellcodes zu finden, mit denen die Beispieldateien für das komplexere Beispiel (Bibliothek) erzeugt werden können. Dabei wird ein objektorientierter Ansatz genutzt. Wir verzichten hier auf das Abspeichern als Datei und beschränken uns darauf, die Zeichenketten zu erstellen, die dann noch in die Dateien geschrieben werden könnten.
#### <font color='blue'>**Objektorientierte Struktur der Daten**</font>
Die Datenstruktur ist hier noch mal als Grafik dargestellt:
%% Cell type:markdown id:7fe5e474 tags:
![Hierarchisch_klein.PNG](attachment:Hierarchisch_klein.PNG)
%% Cell type:markdown id:679d13fd tags:
Wir bauen nun erst eine simple objektorientierte Struktur auf. Wir erzeugen dazu Klassen für die jeweiligen Ebenen:
* Bibliothek
* Buch
* Zeitschrift
* Ausgabe
* Artikel
Jede dieser Klassen hat als Attribute die Werte, die in der Darstellung angegeben sind. Ist das Attribut im Plural, z.B. Bücher in Bibliothek, wird es als Liste von Buch-Objekten gespeichert. Die ````__init__()````- Methoden stellen jeweils alle diese Werte ein. Wir verzichten aus Übersichtlichkeit hier auf jegliche Datenkapselung (get- und set- Methoden), die normalerweise im OOP-Kontext viel eingesetzt wird. Hier greifen wir stattdessen direkt auf die Attribute zu und nutzen die Klassen nur zur Strukturierung.
%% Cell type:code id:5b8f3668 tags:
``` python
class Bibliothek:
def __init__(self, buecher, zeitschriften):
self.buecher = buecher
self.zeitschriften = zeitschriften
class Buch:
def __init__(self, titel, autor, jahr):
self.titel = titel
self.autor = autor
self.jahr = jahr
class Zeitschrift:
def __init__(self, titel, verlag, ausgaben):
self.titel = titel
self.verlag = verlag
self.ausgaben = ausgaben
class Ausgabe:
def __init__(self, nr, artikel):
self.nr = nr
self.artikel = artikel
class Artikel:
def __init__(self, titel, autor):
self.titel = titel
self.autor = autor
```
%% Cell type:markdown id:265e27ca tags:
Nun sind die Klassen angelegt. Im nächsten Codeblock werden Objekte erstellt, die genau die Daten aus der Abbildung repräsentieren. Da wir beim Anlegen eines Objekts am besten schon die ganze Liste an Unterobjekten übergeben, legen wir die Objekte sozusagen "von unten nach oben" an. Wir beginnen also mit den Artikeln, damit wir sie direkt beim Erstellen einer Ausgabe einsetzen können usw. Natürlich könnte man die Listen aber auch nachträglich mit ````.append()```` erweitern.
%% Cell type:code id:67cb0b96 tags:
``` python
# Artikel
art_1 = Artikel("Editorial", "A. Top")
art_2 = Artikel("Untersuchung von xy", "C. Schlau")
# Ausgaben
ausg_1 = Ausgabe("1/2023",[art_1,art_2])
ausg_2 = Ausgabe("2/2023",[])
# Zeitschrift
zeitschr_1 = Zeitschrift("Die beste Zeitschrift", "TOPVerlag", [ausg_1, ausg_2])
# Buecher
buch_1 = Buch("Das erste Buch", "M. Muster", 2013)
buch_2 = Buch("Ein Beispielbuch", "E. Beispiel", 2022)
#Bibliothek
OO_bib = Bibliothek([buch_1, buch_2], [zeitschr_1,])
```
%% Cell type:markdown id:035fc658 tags:
Nun sind die Daten objektorientiert im Programm innerhalb des Bibliothek-Objekts "OO_bib" vorhanden. Wir können uns beispielhaft Daten ausgeben lassen, wie z.B. die Nummer der ersten Ausgabe der ersten Zeitschrift:
%% Cell type:code id:2313f17e tags:
``` python
print(OO_bib.zeitschriften[0].ausgaben[0].nr)
```
%% Output
1/2023
%% Cell type:markdown id:0bd2636d tags:
#### <font color='blue'>**Speichern als json**</font>
Nun werden wir eine Funktion erstellen, die ein Bibliothek-Objekt bekommt und die Bibliothek zu einem json-string konvertiert. Wir benötigen also verschachtelte Listen und Dictionaries mit den Attribut-Namen und Werten. Das erreichen wir, indem wir alle Listen mit Objekten durchgehen und die Informationen in ein Dictionary schreiben. Dieses Dictionary fügen wir dann einer Liste hinzu. Die Liste kommt dann wiederum als Wert in das Dictionary des übergeordneten Objekts. Zum Beispiel gehen wir also die Liste mit Büchern durch und erstellen für jedes Buch ein Dictionary mit Titel, Autor und Jahr entsprechend der Objektattribute. Dieses Dictionary fügen wir einer Liste mit Büchern hinzu. Wenn die Liste mit den Daten aller Bücher gefüllt ist, wird sie in dem Dictionary der Bibliothek unter dem key "Bücher" als Wert eingespeichert. Im Code sieht das so aus:
%% Cell type:code id:1a48d9e2 tags:
``` python
import json
def bibliothek_to_json(bib):
# vorbereiten der noetigen Listen
buecher_list = []
zeitschriften_list = []
#Fuellen der Buecherliste.
for buch in bib.buecher:
buecher_list.append({"Titel": buch.titel, "Autor": buch.autor, "Jahr": buch.jahr})
#Fuellen der zeitschriftenliste
for zeitschrift in bib.zeitschriften:
# Analog dazu Ausgaben und Artikellisten. Wir setzen wieder "rueckwaerts" ein:
ausgaben_list = []
for ausgabe in zeitschrift.ausgaben:
artikel_list = []
for ein_artikel in ausgabe.artikel:
artikel_list.append({"Titel": ein_artikel.titel, "Autor": ein_artikel.autor})
# In diesem Dictionary verwenden wir .copy fuer die Liste, da fuer die naechste ausgabe die Liste wieer geleert wird
ausgaben_list.append({"Nr": ausgabe.nr, "Artikel":artikel_list.copy()})
zeitschriften_list.append({"Titel": zeitschrift.titel, "Verlag": zeitschrift.verlag, "Ausgaben": ausgaben_list.copy()})
return json.dumps({"Buecher": buecher_list, "Zeitschriften": zeitschriften_list}, indent = 4)
```
%% Cell type:markdown id:47b06bc3 tags:
Test der Funktion:
%% Cell type:code id:9d396c48 tags:
``` python
print(bibliothek_to_json(OO_bib))
```
%% Output
{
"Buecher": [
{
"Titel": "Das erste Buch",
"Autor": "M. Muster",
"Jahr": 2013
},
{
"Titel": "Ein Beispielbuch",
"Autor": "E. Beispiel",
"Jahr": 2022
}
],
"Zeitschriften": [
{
"Titel": "Die beste Zeitschrift",
"Verlag": "TOPVerlag",
"Ausgaben": [
{
"Nr": "1/2023",
"Artikel": [
{
"Titel": "Editorial",
"Autor": "A. Top"
},
{
"Titel": "Untersuchung von xy",
"Autor": "C. Schlau"
}
]
},
{
"Nr": "2/2023",
"Artikel": []
}
]
}
]
}
%% Cell type:markdown id:7cfb14b3 tags:
#### <font color='blue'>**Speichern als xml**</font>
Nun werden wir die gleiche Funktionalität mit xml Bereitstellen. Die entsprechende Funktion wird sehr ähnlich aufgebaut. Wir erzeugen mit dem xml.ElementTree Knoten für jedes Attribut. Wenn das Attribut eine Liste ist, erstellen wir Unterknoten für jedes der Listenobjekte. Da wir mehr Knoten als bei json anlegen und benennen, sowie eine extra Zeile für das Definieren des Werts benötigen, wirkt der Code womöglich zunächst sehr lang und unübersichtlich. Das Prinzip ist allerdings relativ simpel (da wir nicht rückwärts einsetzen müssen, evtl. sogar simpler als bei json) und nicht groß anders als bei dem Beispiel mit den Karts aus dem Grundlagennotebook. Wir haben nur etwas mehr Ebenen, in denen wir die Elemente durchgehen müssen. Zum Schluss wird eine Methode verwendet, mit der man auch in Python Versionen unter 3.9 eine ansehnlich formatierte Ausgabe enthält. Da es ab Python 3.9 nicht nötig ist, wird diese nicht detailliert erklärt.
%% Cell type:code id:3f26cd8c tags:
``` python
import xml.etree.ElementTree as ET
# folgender import nur fuer das Einruecken in Python <3.9 noetig
from xml.dom import minidom
def bibliothek_to_xml(bib):
root = ET.Element("Bibliothek")
buecher_node = ET.SubElement(root, "Buecher")
zeitschriften_node = ET.SubElement(root, "Zeitschriften")
for buch in bib.buecher:
buch_node = ET.SubElement(buecher_node, "Buch")
buch_titel = ET.SubElement(buch_node, "Titel")
buch_titel.text = buch.titel
buch_autor = ET.SubElement(buch_node, "Autor")
buch_autor.text = buch.autor
buch_jahr = ET.SubElement(buch_node, "Jahr")
buch_jahr.text = str(buch.jahr)
for zeitschrift in bib.zeitschriften:
zeitschrift_node = ET.SubElement(zeitschriften_node, "Zeitschrift")
zeitschrift_titel = ET.SubElement(zeitschrift_node, "Titel")
zeitschrift_titel.text = zeitschrift.titel
zeitschrift_verlag = ET.SubElement(zeitschrift_node, "Verlag")
zeitschrift_verlag.text = zeitschrift.verlag
zeitschrift_ausgaben_node = ET.SubElement(zeitschrift_node, "Ausgaben")
for ausgabe in zeitschrift.ausgaben:
ausgabe_node = ET.SubElement(zeitschrift_ausgaben_node, "Ausgabe")
ausgabe_nr = ET.SubElement(ausgabe_node, "Nr")
ausgabe_nr.text = ausgabe.nr
ausgabe_alle_artikel_node = ET.SubElement(ausgabe_node, "AlleArtikel")
for ein_artikel in ausgabe.artikel:
artikel_node = ET.SubElement(ausgabe_alle_artikel_node, "Artikel")
artikel_titel = ET.SubElement(artikel_node, "Titel")
artikel_titel.text = ein_artikel.titel
artikel_autor = ET.SubElement(artikel_node, "Autor")
artikel_autor.text = ein_artikel.autor
orig_string = ET.tostring(root, 'utf-8')
# orig_string koennte bereits als Ergebnis mit return ausgegeben werden. Der folgende Befehl macht es ansehnlicher
pretty_string = minidom.parseString(orig_string).toprettyxml(indent=" ")
return pretty_string
```
%% Cell type:markdown id:1c22717c tags:
Auch dieses testen wir:
%% Cell type:code id:9c435ff2 tags:
``` python
print(bibliothek_to_xml(OO_bib))
```
%% Output
<?xml version="1.0" ?>
<Bibliothek>
<Buecher>
<Buch>
<Titel>Das erste Buch</Titel>
<Autor>M. Muster</Autor>
<Jahr>2013</Jahr>
</Buch>
<Buch>
<Titel>Ein Beispielbuch</Titel>
<Autor>E. Beispiel</Autor>
<Jahr>2022</Jahr>
</Buch>
</Buecher>
<Zeitschriften>
<Zeitschrift>
<Titel>Die beste Zeitschrift</Titel>
<Verlag>TOPVerlag</Verlag>
<Ausgaben>
<Ausgabe>
<Nr>1/2023</Nr>
<AlleArtikel>
<Artikel>
<Titel>Editorial</Titel>
<Autor>A. Top</Autor>
</Artikel>
<Artikel>
<Titel>Untersuchung von xy</Titel>
<Autor>C. Schlau</Autor>
</Artikel>
</AlleArtikel>
</Ausgabe>
<Ausgabe>
<Nr>2/2023</Nr>
<AlleArtikel/>
</Ausgabe>
</Ausgaben>
</Zeitschrift>
</Zeitschriften>
</Bibliothek>
%% Cell type:markdown id:93f15900 tags:
Die erste Zeile wird durch das umformatieren hinzugefügt. Ein Tag mit <? tag ?> wird als Kommentar interpretiert. Typischerweise gibt die erste Zeile in xml-Dateien mittels Kommentar Hinweise auf die Version.
%% Cell type:markdown id:5d641f11 tags:
#### <font color='blue'>**Einlesen**</font>
Wir können nun noch Funktionen schreiben, die die Objekte anhand der erstellten Zeichenketten anlegt und ein Bibliothek Objekt ausgibt. Vom Prinzip müssen wir wieder mit verschachtelten Schleifen durch alle Ebenen gehen und die Objekte anlegen. In diesem Notebook sind verkürzte Beispiele enthalten, die nur die Bücher anlegen. Wenn du üben möchtest, erweitere diese Funktionen so, dass auch alle Zeitschriften mit ihren Ausgaben und Artikeln ausgelesen werden. Das Prinzip ist wie bei den Büchern, es gibt nur mehr Ebenen.
%% Cell type:code id:3359e50a tags:
``` python
def buecher_from_json_to_bibliothek(json_string):
bib_dict = json.loads(json_string)
# Die Daten sind nun in bib_dict
# Liste fuer die Buch-Objekte anlegen
buecher_list = []
# Alle Dictionaries aus der Liste mit buechern im bib_dict durchgehen
for buch in bib_dict["Buecher"]:
# Anlegen und zur Liste hinzufuegen eines Objekts fuer das entsprechende Buch mit den Attributen, die dem dictionary entnommen werden
buch_objekt = Buch(buch["Titel"], buch["Autor"], int(buch["Jahr"]))
buecher_list.append(buch_objekt)
# Hier koennte man noch die Zeitschriften einlesen
# Erstellung eines Bibliothek-Objekts, wir uebergeben dabei die gefuellte Buecherliste und eine leere Zeitschriftenliste. Das Objekt wird direkt von der Funktion zurueckgegeben.
return Bibliothek(buecher_list, [])
def buecher_from_xml_to_bibliothek(xml_string):
root = ET.fromstring(xml_string)
# Die Daten sind nun in root
# Liste fuer die Buch-Objekte anlegen
buecher_list = []
# Alle Subknoten ("Buch") des Subknotens "Buecher" des Baums durchgehen:
for buch in root.find("Buecher").findall("Buch"):
# Anlegen und zur Liste hinzufuegen eines Objekts fuer das entsprechende Buch mit den Attributen, die den Subknoten entnommen werden
buch_objekt = Buch(buch.find("Titel").text, buch.find("Autor").text, int(buch.find("Jahr").text))
buecher_list.append(buch_objekt)
# Hier koennte man noch die Zeitschriften einlesen
# Erstellung eines Bibliothek-Objekts, wir uebergeben dabei die gefuellte Buecherliste und eine leere Zeitschriftenliste. Das Objekt wird direkt von der Funktion zurueckgegeben.
return Bibliothek(buecher_list, [])
```
%% Cell type:markdown id:50943020 tags:
Und noch der Test am Beispiel der Jahreszahlen der beiden Bücher:
%% Cell type:code id:b80b127b tags:
``` python
json_string_bib = bibliothek_to_json(OO_bib)
bib_from_json = buecher_from_json_to_bibliothek(json_string_bib)
xml_string_bib = bibliothek_to_xml(OO_bib)
bib_from_xml = buecher_from_xml_to_bibliothek(xml_string_bib)
print(bib_from_json.buecher[0].jahr)
print(bib_from_xml.buecher[1].jahr)
```
%% Output
2013
2022
%% Cell type:markdown id:b26cfd92 tags:
# <font color='blue'>**Übung 12 - Extra: 3D-Animation der Ergebnisse aus der Freifall-Simulation**
Zum Schluss des Kurses wollen wir noch eine weitere Möglichkeit der vielen verfügbaren Python-Pakete zeigen. Wir werden ein Paket verwenden, das es erlaubt, mit sehr einfachen Mitteln 3D Animationen zu erzeugen. Dabei übernimmt das Paket alle internen Berechnungen und die Bedienung, sodass wir nicht mehr selbst machen müssen, als ein entsprechendes Fenster zu öffnen, Objekte hinzuzufügen und mit einer Schleife die Position der Objekte zu verändern. Im Gegensatz zu anderen Übungen gibt es hierzu kein Grundlagennotebook, sondern wir erarbeiten die benötigten Features direkt in der Übung.
%% Cell type:markdown id:df2bffd4 tags:
## <font color='blue'>**Problemstellung**
Die Ergebnisse der 2D/3D - Fallsimulation aus Übung 10 sollen besser visualisiert werden, indem statt statischen 2D-Plots, eine 3D Animation erzeugt werden soll. Das Python paket ````vpython```` ist dazu hervorragend geeignet.
#### <font color='blue'>**Vorüberlegungen**
Vermutlich denkt man zunächst, eine 3D Animation aufzubauen wäre sehr kompliziert. Wir müssen allerdings bei weitem nicht bei 0 anfangen. Es gibt ein Python Paket ````vpython````, das alle nötigen perspektivischen Berechnungen und die Kamerasteuerung mit der der Maus bereits beherrscht. Mit diesem Paket ist es mit sehr wenig Code möglich, einfache 3D-Szenen aufzubauen. Um vpython zu benutzen, muss es zunächst mit ````pip3 install vpython```` in der Konsole in die genutzte Python Installation hinzugefügt werden.
Wir werden jetzt Schritt für Schritt einzelne Elemente von vpython durchgehen, sodass wir am Ende in der Lage sind, die Animation für unsere Freifall-Simulationsergebnisse zu erstellen. Dazu benötigen wir Kenntnis über
* das Erstellen eines Fensters für die 3D Animation
* das Hinzufügen von Objekten (wir wollen später eine Kugel für das fallende Objekt und eine sehr flache Box für den Boden verwenden)
* das Verändern der Position von Objekten
* das automatische Aktualisieren der Position in der Zeit
Über die Simulation/Berechnung der zeitabhängigen Positionen müssen wir uns keine großen Gedanken mehr machen. Diese Informationen stehen bereits mit dem Code aus Übung 10 zur Verfügung.
%% Cell type:markdown id:6d0293fd tags:
#### <font color='blue'>**Erarbeiten der nötigen vpython Kenntnisse**
Wie jedes Paket müssen wir vpython zunächst importieren. Wir verwenden hier die Abkürzung ````vp```` für den namespace.
Der erste Schritt ist, ein Fenster zu erzeugen, in dem unsere 3D-Objekte angeordnet werden können. Der Befehl dafür lautet in ````vp.canvas()````. Canvas heißt Leinwand und ist zu vergleichen mit ````plt.figure()```` aus matplotlib. Dieses Canvas speichern wir unter einem Namen ab, um später Attribute verändern zu können. Das ist generell ein typisches Vorgehen in vpython. Die Objekte der 3D-Umgebung sind auch im Programmiersinn Objekte, die wir mit dem Konstruktor anlegen und später Attribute verändern können.
Damit wir auch etwas zu betrachten haben, fügen wir zunächst einen Kasten hinzu. Das ist ein Objekt mit der Klasse ````vp.box````. In diesem ersten Schritt nutzen wir ausschließlich die Standardparameter. Beim Ausführen des Codes sollte ein Bild mit der 3D-"Welt" auftauchen. Man kann mittels Mausrad hinein- und herauszoomen, mit der gedrückten rechten Maustaste und Ziehen den Betrachtungswinkel ändern und mit Shift (Großstelltaste) + linke Maustaste grdürckt halten und ziehen das Bild verschieben. Probiere diese Kamerasteuerung zunächst aus.
%% Cell type:code id:f3124ac8 tags:
``` python
import vpython as vp
scene = vp.canvas()
first_box = vp.box()
```
%% Cell type:markdown id:cc91ae6a tags:
Als nächstes wollen wir uns ein paar Attribute ansehen und verändern. Die Änderungen werden immer im bereits geöffneten Fenster aktualisiert (Es öffnet sich also kein neues Fenster, solange wir nicht eine neue ````vp.canvas```` erstellen).
Wollen wir zum Beispiel die Hintergrundfarbe verändern, können wir das Attribut ````background```` unseres ````vp.canvas```` Objekts verändern. vpython hat ein paar voreingestellte Farben, die unter ````vp.colors.```` *+ englischer Name der Farbe* aufzurufen sind. Stellen wir also den Hintergrund auf weiß ein:
%% Cell type:code id:f0579d68 tags:
``` python
scene.background = '?'
```
%% Cell type:markdown id:cadf330e tags:
Genau so können wir die Farbe des Kastens ändern. Das entsprechende Attribut des ````vp.box```` Objekts heißt ````color````. Stellen wir die Farbe zum Beispiel blau ein.
%% Cell type:code id:adf44a31 tags:
``` python
first_box.'?' = '?'
```
%% Cell type:markdown id:f249942e tags:
Der Kasten hat noch mehr Eigenschaften/Attribute als nur die Farbe. Die Abmessungen sind unter ````width````,````length```` und ````height```` gespeichert. Wir testen dies, indem wir eine der Abmessungen auf 2 stellen.
%% Cell type:code id:7d6288ef tags:
``` python
'?'
```
%% Cell type:markdown id:c0f00fff tags:
Das zeigt uns auch, das die Standardparameter für die Box überall 1 sind.
Als nächstes werden wir eine Kugel hinzufügen. Die entsprechende Klasse heißt ````vp.sphere```` und hat anstelle der 3 Abmessungen der Box den Parameter ````radius````. Wir verwenden außerdem nun die Möglichkeit, direkt im Konstruktor diese Parameter zu setzen. Wir erzeugen eine rote Kugel mit dem Radius 0.7.
%% Cell type:code id:f820d54c tags:
``` python
first_ball = '?'('?')
```
%% Cell type:markdown id:22c147b1 tags:
Die Kugel wurde erzeugt, sie befindet sich aber innerhalb der Box. Das liegt daran, dass beide Objekte mit dem Mittelpunkt an der Standardposition, nämlich dem Ursprung (0, 0, 0) erstellt wurden. Die Position der 3D-Objekte ist in dem Attribut ````pos```` gepseichert. Dieses Attribut benötigt ein Objekt der Klasse ````vp.vector````. Ein solches Objekt erhält beim Anlegen die Koordinaten (x, y, z) als Parameter. Das Koordinatensystem in vpython ist so orientiert, dass die y-Achse in die Höhe zeigt, und die x-z Ebene den Boden beschreibt. Das müssen wir bedenken, da bei uns sonst häufig die x-y - Ebene der Boden ist, und die z-Achse die Höhe.
Wir verändern nun also die Position der Kugel so, dass die Kugel neben der Box sichtbar ist. (z.B. auf (1.5, 0, 0))
%% Cell type:code id:9980b244 tags:
``` python
'?'
```
%% Cell type:markdown id:608b9296 tags:
Wir haben bereits die ersten 3 Punkte der Anforderungsliste abgearbeitet. Wir können die nötigen Objekte in beliebiger Größe und Farbe an beliebiger Position in 3D Anzeigen lassen.
Es fehlt nur noch die Animation. Eine Animation ist das Aktualisieren von Objektattributen in einem zeitlichen Verlauf.
Manuell könnten wir zum Beispiel die y-Koordinate der Kugel etwas anheben. Dann könnten wir uns das Bild angucken und danach wieder etwas anheben. Je schneller wir die Position aktualisieren, desto mehr sieht es nach Bewegung, also Animation aus. Natürlich ist es reichlich unpraktisch, das manuell durchzuführen. Kinofilme haben Aktualisierungsraten von 24 Bildern pro Sekunde, das schaffen wir manuell natürlich nicht. Das Prinzip einer Animation lässt sich so aber gut erklären.
Nun wollen wir also das Aktualisieren der Darstellung automatisieren. Das können wir mit einer Schleife machen. Wir erstellen also eine Liste mit y-Koordinaten, die nach und nach eingestellt werden sollen. Zum Beispiel von 0 bis 10 mit 100 Stützstellen. Mithilfe von ````numpy.linspace()```` lässt sich das sehr gut machen.
Nun müssen wir nur noch mit einer for-Schleife jeden Eintrag der Liste durchgehen und die Position der Kugel mit diesem Eintrag aktualisieren.
%% Cell type:code id:8744a792 tags:
``` python
import numpy as np
y_history = '?'
for '?' in '?':
first_ball.'?' = '?'('?')
```
%% Cell type:markdown id:ed60e7cc tags:
Wenn man nun das 3D-Bild ansieht, wird man höchstwahrscheinlich (abhängig von der Rechnergeschwindigkeit) die Kugel bereits an der Endposition, also 10 Einheiten über dem Startpunkt antreffen. Das liegt daran, dass der Computer die Animation so schnell aktualisiert, wie er kann. Wir können das mithilfe einer Vpython Funktion abbremsen. Die Funktion heißt ````vp.rate(fps)````. FPS steht für "Bilder pro Sekunde", auch "framerate" genannt. In einer Schleife sorgt diese Funktion dafür, dass die Schleife maximal so oft pro Sekunde durchläuft, wie in ````fps```` angegeben. Sie hat allerdings keinen Effekt, falls die Berechnung eines Bildes ohnehin länger dauert.
Wir können uns am Kino orientieren und begrenzen die Framerate auf 24. Nun sollte man die Kugel einmal mit gleichmäßoger Geschwindigkeit nach oben fliegen sehen.
%% Cell type:code id:28fa48ca tags:
``` python
for '?' in '?':
'?'
'?'
```
%% Cell type:markdown id:81ae48a6 tags:
Wenn die Schleife durchlaufen ist, ist die Animation vorbei. Wir können sie aber unbegrenzt in Schleife laufen lassen, indem wir sie in eine weitere Schleife mit einer Endlosbedingung (z.B. ````while true````) einbringen. Die Animation würde dann bis in alle Ewigkeit (bzw. bis wir mithilfe des Stopp-Zeichens im Notebook die Ausführung abbrechen oder den Browser schließen etc.) laufen. Alternativ könnte man auch eine Schleife mit vorgebeber Durchlaufzahl, z.B. 5 verwenden.
%% Cell type:code id:8c0f2a93 tags:
``` python
for '?' in range('?'):
for '?' in '?':
'?'
'?'
```
%% Cell type:markdown id:bdc2f6d8 tags:
Damit haben wir nun alle Kenntnisse für vpython, die wir für die Animation unserer Simulationsergebnisse benötigen, zusammen.
Weitere Attribute und Möglichkeiten (z.B. Rotationen der Objekte oder weitere Formentypen) für andere Visualisierung findet man in der Dokumentatation.
%% Cell type:markdown id:69314a61 tags:
#### <font color='blue'>**Umsetzung**
Zunächst kopieren wir den Code aus Übung 10 hierher. Alternativ könnten wir auch die Ergbenisse aus einer csv-Datei einlesen. Die Erzeugung der Ergebnisse ist jedenfalls nicht Thema dieser Übung.
%% Cell type:code id:7a8d0120 tags:
``` python
# Dieser Code wird in UE10 besprochen. Wir brauchen ihn hier nur, um die Daten zu erzeugen. Alternativ koennten wir sie auch auch einer Datei einlesen.
import scipy.integrate
#Konstanten
g = 9.81
m = 1
rho = 1.225
C_W = 0.18
A = 0.05
vx_0, vz_0 = 70, 10
x_0, z_0 = 0, 500
y_0 = [vx_0, vz_0, x_0, z_0]
t_span = [0,20]
def func_2D(t,y,g,m,rho,C_W,A):
vx = y[0]
vz = y[1]
vektor_v = np.array([vx,vz])
betrag_v = np.linalg.norm(vektor_v)
F_W = 0.5*rho*vektor_v*betrag_v*C_W*A
ax = -F_W[0] /m
az = (-F_W[1] - m*g)/m
return [ax, az, vx, vz]
sol_2d = scipy.integrate.solve_ivp(func_2D, t_span, y_0, max_step = 0.25, args=(g, m, rho, C_W, A))
```
%% Cell type:markdown id:2710494f tags:
In Übung 10 haben wir für die 2D_Plots akzeptiert, dass die Zietschrittweite nicht konstant ist. In unserer jetzt zu erstellenden 3D-Animation wäre eine konstante Zeitspanne pro Bild aber wünschenswert. Ausßerdem möchten wir vielleicht für eine flüssige Darstellung mehrere Schritte anzeigen lassen, als berechnet wurden. Ein Ansatz dafür ist es, die Ergebnisse zu interpolieren. Dadurch können wir die Position zu jedem beliebigen Zeitpunkt aus den in der Simulation berechneten Zeitpunkten annähern. Dafür verwenden wir die scipy-Funktion und erzeugen uns somit Funktionen für die interpolierte Strecke und Höhe.
%% Cell type:code id:f1896355 tags:
``` python
import scipy.interpolate
x_interpolation = scipy.interpolate.interp1d(sol_2d["t"],sol_2d["y"][2])
y_interpolation = scipy.interpolate.interp1d(sol_2d["t"],sol_2d["y"][3])
```
%% Cell type:markdown id:42563ad1 tags:
Nun haben wir alle nötigen Informationen zusammen.
Wir sollten aber noch ein paar Überlegungen anstellen, um eine gute Animation zu erzeugen.
Zunächst können wir der ````vp.canvas```` mittels Attribut ````title```` eine Überschrift geben. Dann sollten wir uns Gedanken über die Farben und Objekttypen machen. Ich schlage folgendes vor: Der Hintergrund symbolisiert den Himmel - cyan. Das fallende Objekt soll auffällig sein: Rote Kugel. Den Boden stellen wir als sehr flache aber lange und breite grüne (Rasen) Box dar.
Dann müssen wir überlegen, wo die "Bodenbox" sein soll, und wie groß sie und die Kugel sein sollen. Hier muss man vielleicht etwas experimentieren, bis man eine gute Abstimmung gefunden hat. Anhaltspunkte können aber die darzustellenden Ergebnisse geben. Wir wissen zum Beispiel (Plots aus Übung 10), dass die x-Koordinate der Kugel zwischen 0 und 300m annimmt. Unser Boden sollte größer sein, als diese Spanne, da es sonst nach einer Ziellandung aussehen würde. Ich schlage 800m x 800m vor. Damit die Kugel sich etwa mittig bewegt, verschieben wir diese Fläche noch um 200m in Richtung x. Der Boden ist flach. Die Kugel fällt aus 500m Höhe. Eine Höhe der Box von 1m sollte dünn genug sein, um als Ebene zu wirken. Wenn wir der Kugel den Radius aus der Simulation geben würden (Querschnitt deutlich unter 1m), so würden wir sie kaum sehen können, da dies viel kleiner ist als die zurückgelegte Distanz. Wir werden also die Kugel deutlich größer darstellen. Bei Radius 20m ist sie etwa 10% der Fallhöhe breit und sollte gut zu sehen sein.
Die letzte Überlegung betrifft die Zeit. Im Beispiel oben haben wir (dort für y) Anfangs- und Endwert und die Anzahl der Stützstellen definiert. Das könnten wir hier natürlich auch tun. Wir könnten aber auch Anfangs- und Endwert, sowie eine Schrittweite angeben (````np.arange````). Das ermöglicht es uns, festzulegen, mit welcher Geschwindigkeit die Animation laufen soll. Soll sie in Echtzeit laufen, tragen wir für die Schrittweite das Inverse der Framerate ein. Also z.B. $\frac{1}{24}$. Unsere Simulation dauert aber 20 Sekunden, was etwas lang ist. Wir beschleunigen es daher, um das Zehnfache und nehmen $\frac{10}{24} \approx 0.4$
Dieser ganze Absatz hatte nicht mehr viel mit Programmierung zu tun, eher mit der Gestaltung der Animation. Nun können wir diese Überlegungen in Code umsetzen, alle Bestandteile haben wir bereits besprochen. Wir nutzen intensiv die Möglichkeit, Attribute mithilfe des Konstruktors zu initialisieren, und denken daran, dass die Höhe bei vpython die y Koordinate ist. Die z-Koordinate ist für unsere 2D-Simualtion dann immer 0.
%% Cell type:code id:e2d8caa6 tags:
``` python
# Anordnen der Elemente
scene = '?'
floor = '?'
falling_object = '?'
# Animation der zeitlichen Bewegung
for '?' in '?':
for '?' in '?':
'?'
falling_object.'?' = '?'(x_interpolation('?'), '?')
```
%% Cell type:markdown id:a0f3f6b4 tags:
Damit haben wir eine gute Visualisierung für unsere Simulationsergebnisse in 7 Zeilen eigenem Code erstellt. Ich hoffe, dieser kurze Exkurs gibt einen Ausblick auf die vielfältigen Möglichkeiten mit Python, und, wie mächtig und nützlich die Pakete sein können.
%% Cell type:markdown id:6d9200a2 tags:
# <font color='blue'>**Übung 12 - Extra: 3D-Animation der Ergebnisse aus der Freifall-Simulation**
Zum Schluss des Kurses wollen wir noch eine weitere Möglichkeit der vielen verfügbaren Python-Pakete zeigen. Wir werden ein Paket verwenden, das es erlaubt, mit sehr einfachen Mitteln 3D Animationen zu erzeugen. Dabei übernimmt das Paket alle internen Berechnungen und die Bedienung, sodass wir nicht mehr selbst machen müssen, als ein entsprechendes Fenster zu öffnen, Objekte hinzuzufügen und mit einer Schleife die Position der Objekte zu verändern. Im Gegensatz zu anderen Übungen gibt es hierzu kein Grundlagennotebook, sondern wir erarbeiten die benötigten Features direkt in der Übung.
%% Cell type:markdown id:d4c554bb tags:
## <font color='blue'>**Problemstellung**
Die Ergebnisse der 2D/3D - Fallsimulation aus Übung 10 sollen besser visualisiert werden, indem statt statischen 2D-Plots, eine 3D Animation erzeugt werden soll. Das Python paket ````vpython```` ist dazu hervorragend geeignet.
#### <font color='blue'>**Vorüberlegungen**
Vermutlich denkt man zunächst, eine 3D Animation aufzubauen wäre sehr kompliziert. Wir müssen allerdings bei weitem nicht bei 0 anfangen. Es gibt ein Python Paket ````vpython````, das alle nötigen perspektivischen Berechnungen und die Kamerasteuerung mit der der Maus bereits beherrscht. Mit diesem Paket ist es mit sehr wenig Code möglich, einfache 3D-Szenen aufzubauen. Um vpython zu benutzen, muss es zunächst mit ````pip3 install vpython```` in der Konsole in die genutzte Python Installation hinzugefügt werden.
Wir werden jetzt Schritt für Schritt einzelne Elemente von vpython durchgehen, sodass wir am Ende in der Lage sind, die Animation für unsere Freifall-Simulationsergebnisse zu erstellen. Dazu benötigen wir Kenntnis über
* das Erstellen eines Fensters für die 3D Animation
* das Hinzufügen von Objekten (wir wollen später eine Kugel für das fallende Objekt und eine sehr flache Box für den Boden verwenden)
* das Verändern der Position von Objekten
* das automatische Aktualisieren der Position in der Zeit
Über die Simulation/Berechnung der zeitabhängigen Positionen müssen wir uns keine großen Gedanken mehr machen. Diese Informationen stehen bereits mit dem Code aus Übung 10 zur Verfügung.
%% Cell type:markdown id:24216354 tags:
#### <font color='blue'>**Erarbeiten der nötigen vpython Kenntnisse**
Wie jedes Paket müssen wir vpython zunächst importieren. Wir verwenden hier die Abkürzung ````vp```` für den namespace.
Der erste Schritt ist, ein Fenster zu erzeugen, in dem unsere 3D-Objekte angeordnet werden können. Der Befehl dafür lautet in ````vp.canvas()````. Canvas heißt Leinwand und ist zu vergleichen mit ````plt.figure()```` aus matplotlib. Dieses Canvas speichern wir unter einem Namen ab, um später Attribute verändern zu können. Das ist generell ein typisches Vorgehen in vpython. Die Objekte der 3D-Umgebung sind auch im Programmiersinn Objekte, die wir mit dem Konstruktor anlegen und später Attribute verändern können.
Damit wir auch etwas zu betrachten haben, fügen wir zunächst einen Kasten hinzu. Das ist ein Objekt mit der Klasse ````vp.box````. In diesem ersten Schritt nutzen wir ausschließlich die Standardparameter. Beim Ausführen des Codes sollte ein Bild mit der 3D-"Welt" auftauchen. Man kann mittels Mausrad hinein- und herauszoomen, mit der gedrückten rechten Maustaste und Ziehen den Betrachtungswinkel ändern und mit Shift (Großstelltaste) + linke Maustaste grdürckt halten und ziehen das Bild verschieben. Probiere diese Kamerasteuerung zunächst aus.
%% Cell type:code id:4442acc1 tags:
``` python
import vpython as vp
scene = vp.canvas()
first_box = vp.box()
```
%% Cell type:markdown id:fdfef700 tags:
Als nächstes wollen wir uns ein paar Attribute ansehen und verändern. Die Änderungen werden immer im bereits geöffneten Fenster aktualisiert (Es öffnet sich also kein neues Fenster, solange wir nicht eine neue ````vp.canvas```` erstellen).
Wollen wir zum Beispiel die Hintergrundfarbe verändern, können wir das Attribut ````background```` unseres ````vp.canvas```` Objekts verändern. vpython hat ein paar voreingestellte Farben, die unter ````vp.colors.```` *+ englischer Name der Farbe* aufzurufen sind. Stellen wir also den Hintergrund auf weiß ein:
%% Cell type:code id:089122d5 tags:
``` python
scene.background = vp.color.white
```
%% Cell type:markdown id:915d5218 tags:
Genau so können wir die Farbe des Kastens ändern. Das entsprechende Attribut des ````vp.box```` Objekts heißt ````color````. Stellen wir die Farbe zum Beispiel blau ein.
%% Cell type:code id:a16d4f6b tags:
``` python
first_box.color = vp.color.blue
```
%% Cell type:markdown id:d560a0ef tags:
Der Kasten hat noch mehr Eigenschaften/Attribute als nur die Farbe. Die Abmessungen sind unter ````width````,````length```` und ````height```` gespeichert. Wir testen dies, indem wir eine der Abmessungen auf 2 stellen.
%% Cell type:code id:9535fc32 tags:
``` python
first_box.width = 2 # es gibt auch noch length und height
```
%% Cell type:markdown id:88b31b63 tags:
Das zeigt uns auch, das die Standardparameter für die Box überall 1 sind.
Als nächstes werden wir eine Kugel hinzufügen. Die entsprechende Klasse heißt ````vp.sphere```` und hat anstelle der 3 Abmessungen der Box den Parameter ````radius````. Wir verwenden außerdem nun die Möglichkeit, direkt im Konstruktor diese Parameter zu setzen. Wir erzeugen eine rote Kugel mit dem Radius 0.7.
%% Cell type:code id:c70ff587 tags:
``` python
first_ball = vp.sphere(radius = 0.7, color = vp.color.red)
```
%% Cell type:markdown id:09da26c2 tags:
Die Kugel wurde erzeugt, sie befindet sich aber innerhalb der Box. Das liegt daran, dass beide Objekte mit dem Mittelpunkt an der Standardposition, nämlich dem Ursprung (0, 0, 0) erstellt wurden. Die Position der 3D-Objekte ist in dem Attribut ````pos```` gepseichert. Dieses Attribut benötigt ein Objekt der Klasse ````vp.vector````. Ein solches Objekt erhält beim Anlegen die Koordinaten (x, y, z) als Parameter. Das Koordinatensystem in vpython ist so orientiert, dass die y-Achse in die Höhe zeigt, und die x-z Ebene den Boden beschreibt. Das müssen wir bedenken, da bei uns sonst häufig die x-y - Ebene der Boden ist, und die z-Achse die Höhe.
Wir verändern nun also die Position der Kugel so, dass die Kugel neben der Box sichtbar ist. (z.B. auf (1.5, 0, 0))
%% Cell type:code id:8433fb34 tags:
``` python
first_ball.pos = vp.vector(1.5, 0, 0)
```
%% Cell type:markdown id:619a312e tags:
Wir haben bereits die ersten 3 Punkte der Anforderungsliste abgearbeitet. Wir können die nötigen Objekte in beliebiger Größe und Farbe an beliebiger Position in 3D Anzeigen lassen.
Es fehlt nur noch die Animation. Eine Animation ist das Aktualisieren von Objektattributen in einem zeitlichen Verlauf.
Manuell könnten wir zum Beispiel die y-Koordinate der Kugel etwas anheben. Dann könnten wir uns das Bild angucken und danach wieder etwas anheben. Je schneller wir die Position aktualisieren, desto mehr sieht es nach Bewegung, also Animation aus. Natürlich ist es reichlich unpraktisch, das manuell durchzuführen. Kinofilme haben Aktualisierungsraten von 24 Bildern pro Sekunde, das schaffen wir manuell natürlich nicht. Das Prinzip einer Animation lässt sich so aber gut erklären.
Nun wollen wir also das Aktualisieren der Darstellung automatisieren. Das können wir mit einer Schleife machen. Wir erstellen also eine Liste mit y-Koordinaten, die nach und nach eingestellt werden sollen. Zum Beispiel von 0 bis 10 mit 100 Stützstellen. Mithilfe von ````numpy.linspace()```` lässt sich das sehr gut machen.
Nun müssen wir nur noch mit einer for-Schleife jeden Eintrag der Liste durchgehen und die Position der Kugel mit diesem Eintrag aktualisieren.
%% Cell type:code id:f398c8e6 tags:
``` python
import numpy as np
y_history = np.linspace(0, 10, 100)
for y in y_history:
first_ball.pos = vp.vector(1.5, y, 0)
```
%% Cell type:markdown id:d4480413 tags:
Wenn man nun das 3D-Bild ansieht, wird man höchstwahrscheinlich (abhängig von der Rechnergeschwindigkeit) die Kugel bereits an der Endposition, also 10 Einheiten über dem Startpunkt antreffen. Das liegt daran, dass der Computer die Animation so schnell aktualisiert, wie er kann. Wir können das mithilfe einer Vpython Funktion abbremsen. Die Funktion heißt ````vp.rate(fps)````. FPS steht für "Bilder pro Sekunde", auch "framerate" genannt. In einer Schleife sorgt diese Funktion dafür, dass die Schleife maximal so oft pro Sekunde durchläuft, wie in ````fps```` angegeben. Sie hat allerdings keinen Effekt, falls die Berechnung eines Bildes ohnehin länger dauert.
Wir können uns am Kino orientieren und begrenzen die Framerate auf 24. Nun sollte man die Kugel einmal mit gleichmäßoger Geschwindigkeit nach oben fliegen sehen.
%% Cell type:code id:70615870 tags:
``` python
for y in y_history:
vp.rate(25)
first_ball.pos = vp.vector(1.5, y, 0)
```
%% Cell type:markdown id:26ea1d38 tags:
Wenn die Schleife durchlaufen ist, ist die Animation vorbei. Wir können sie aber unbegrenzt in Schleife laufen lassen, indem wir sie in eine weitere Schleife mit einer Endlosbedingung (z.B. ````while true````) einbringen. Die Animation würde dann bis in alle Ewigkeit (bzw. bis wir mithilfe des Stopp-Zeichens im Notebook die Ausführung abbrechen oder den Browser schließen etc.) laufen. Alternativ könnte man auch eine Schleife mit vorgebeber Durchlaufzahl, z.B. 5 verwenden.
%% Cell type:code id:f0830b68 tags:
``` python
for iteration in range(5):
for y in y_history:
vp.rate(25)
first_ball.pos = vp.vector(1.5, y, 0)
```
%% Cell type:markdown id:601e9d70 tags:
Damit haben wir nun alle Kenntnisse für vpython, die wir für die Animation unserer Simulationsergebnisse benötigen, zusammen.
Weitere Attribute und Möglichkeiten (z.B. Rotationen der Objekte oder weitere Formentypen) für andere Visualisierung findet man in der Dokumentatation.
%% Cell type:markdown id:3a1f2e1a tags:
#### <font color='blue'>**Umsetzung**
Zunächst kopieren wir den Code aus Übung 10 hierher. Alternativ könnten wir auch die Ergbenisse aus einer csv-Datei einlesen. Die Erzeugung der Ergebnisse ist jedenfalls nicht Thema dieser Übung.
%% Cell type:code id:7a8d0120 tags:
``` python
# Dieser Code wird in UE10 besprochen. Wir brauchen ihn hier nur, um die Daten zu erzeugen. Alternativ koennten wir sie auch auch einer Datei einlesen.
import scipy.integrate
#Konstanten
g = 9.81
m = 1
rho = 1.225
C_W = 0.18
A = 0.05
vx_0, vz_0 = 70, 10
x_0, z_0 = 0, 500
y_0 = [vx_0, vz_0, x_0, z_0]
t_span = [0,20]
def func_2D(t,y,g,m,rho,C_W,A):
vx = y[0]
vz = y[1]
vektor_v = np.array([vx,vz])
betrag_v = np.linalg.norm(vektor_v)
F_W = 0.5*rho*vektor_v*betrag_v*C_W*A
ax = -F_W[0] /m
az = (-F_W[1] - m*g)/m
return [ax, az, vx, vz]
sol_2d = scipy.integrate.solve_ivp(func_2D, t_span, y_0, max_step = 0.25, args=(g, m, rho, C_W, A))
```
%% Cell type:markdown id:ab36d27f tags:
In Übung 10 haben wir für die 2D_Plots akzeptiert, dass die Zietschrittweite nicht konstant ist. In unserer jetzt zu erstellenden 3D-Animation wäre eine konstante Zeitspanne pro Bild aber wünschenswert. Ausßerdem möchten wir vielleicht für eine flüssige Darstellung mehrere Schritte anzeigen lassen, als berechnet wurden. Ein Ansatz dafür ist es, die Ergebnisse zu interpolieren. Dadurch können wir die Position zu jedem beliebigen Zeitpunkt aus den in der Simulation berechneten Zeitpunkten annähern. Dafür verwenden wir die scipy-Funktion und erzeugen uns somit Funktionen für die interpolierte Strecke und Höhe.
%% Cell type:code id:426dc830 tags:
``` python
import scipy.interpolate
x_interpolation = scipy.interpolate.interp1d(sol_2d["t"],sol_2d["y"][2])
y_interpolation = scipy.interpolate.interp1d(sol_2d["t"],sol_2d["y"][3])
```
%% Cell type:markdown id:186281ee tags:
Nun haben wir alle nötigen Informationen zusammen.
Wir sollten aber noch ein paar Überlegungen anstellen, um eine gute Animation zu erzeugen.
Zunächst können wir der ````vp.canvas```` mittels Attribut ````title```` eine Überschrift geben. Dann sollten wir uns Gedanken über die Farben und Objekttypen machen. Ich schlage folgendes vor: Der Hintergrund symbolisiert den Himmel - cyan. Das fallende Objekt soll auffällig sein: Rote Kugel. Den Boden stellen wir als sehr flache aber lange und breite grüne (Rasen) Box dar.
Dann müssen wir überlegen, wo die "Bodenbox" sein soll, und wie groß sie und die Kugel sein sollen. Hier muss man vielleicht etwas experimentieren, bis man eine gute Abstimmung gefunden hat. Anhaltspunkte können aber die darzustellenden Ergebnisse geben. Wir wissen zum Beispiel (Plots aus Übung 10), dass die x-Koordinate der Kugel zwischen 0 und 300m annimmt. Unser Boden sollte größer sein, als diese Spanne, da es sonst nach einer Ziellandung aussehen würde. Ich schlage 800m x 800m vor. Damit die Kugel sich etwa mittig bewegt, verschieben wir diese Fläche noch um 200m in Richtung x. Der Boden ist flach. Die Kugel fällt aus 500m Höhe. Eine Höhe der Box von 1m sollte dünn genug sein, um als Ebene zu wirken. Wenn wir der Kugel den Radius aus der Simulation geben würden (Querschnitt deutlich unter 1m), so würden wir sie kaum sehen können, da dies viel kleiner ist als die zurückgelegte Distanz. Wir werden also die Kugel deutlich größer darstellen. Bei Radius 20m ist sie etwa 10% der Fallhöhe breit und sollte gut zu sehen sein.
Die letzte Überlegung betrifft die Zeit. Im Beispiel oben haben wir (dort für y) Anfangs- und Endwert und die Anzahl der Stützstellen definiert. Das könnten wir hier natürlich auch tun. Wir könnten aber auch Anfangs- und Endwert, sowie eine Schrittweite angeben (````np.arange````). Das ermöglicht es uns, festzulegen, mit welcher Geschwindigkeit die Animation laufen soll. Soll sie in Echtzeit laufen, tragen wir für die Schrittweite das Inverse der Framerate ein. Also z.B. $\frac{1}{24}$. Unsere Simulation dauert aber 20 Sekunden, was etwas lang ist. Wir beschleunigen es daher, um das Zehnfache und nehmen $\frac{10}{24} \approx 0.4$
Dieser ganze Absatz hatte nicht mehr viel mit Programmierung zu tun, eher mit der Gestaltung der Animation. Nun können wir diese Überlegungen in Code umsetzen, alle Bestandteile haben wir bereits besprochen. Wir nutzen intensiv die Möglichkeit, Attribute mithilfe des Konstruktors zu initialisieren, und denken daran, dass die Höhe bei vpython die y Koordinate ist. Die z-Koordinate ist für unsere 2D-Simualtion dann immer 0.
%% Cell type:code id:e2d8caa6 tags:
``` python
# Anordnen der Elemente
scene = vp.canvas(title='Frei Fall Simulation', background=vp.color.cyan)
floor = vp.box(pos=vp.vector(200,0,0), length = 800, width = 800, height = 1, color = vp.color.green)
falling_object = vp.sphere(pos=vp.vector(x_interpolation(0), z_interpolation(0), 0), radius=20, color=vp.color.red)
# Animation der zeitlichen Bewegung
for iteration in range(5):
for t in np.arange(0, 20, 0.4):
vp.rate(24)
falling_object.pos = vp.vector(x_interpolation(t), y_interpolation(t), 0)
```
%% Cell type:markdown id:7131c2b3 tags:
Damit haben wir eine gute Visualisierung für unsere Simulationsergebnisse in 7 Zeilen eigenem Code erstellt. Ich hoffe, dieser kurze Exkurs gibt einen Ausblick auf die vielfältigen Möglichkeiten mit Python, und, wie mächtig und nützlich die Pakete sein können.