Einführung in das Wissenschaftliche Arbeiten – Einführung in python Sommersemester 2017 Ruhr-Universität Bochum Fakultät für Physik und Astronomie Lehrstuhl für Theoretische Physik IV Plasma-Astroteilchen Physik Dr. Lenka Tomankova Lukas Merten, M.Sc. April 2017 1 Inhaltsverzeichnis 1 Grundlagen 1.1 Stärken . . . . . . . . . . . . . . 1.1.1 jupyter/ipython notebook 1.2 Schwächen . . . . . . . . . . . . . 1.3 Download . . . . . . . . . . . . . . . . . 3 3 3 3 4 2 Erste Schritte 2.1 Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Grundlegende Rechenoperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Ein erster Plot (matplotlib) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 6 3 Fortgeschrittene Programmstrukturen 3.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Loops/Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 9 10 4 Namen, Objekte und Referenzen 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Numerische Rechnungen: numpy 11 5.1 N-dimensionales Array: numpy.ndarray() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 5.2 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 6 Wissenschaftliche Funktionen: SciPy 6.1 scipy.special . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Numerische Integration: scipy.integrate.quad . . . . . . 6.3 Nullstellensuche: scipy.optimize.root . . . . . . . . . . 6.4 Regression / Funktionsfitting: scipy.optimize.curve fit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 14 16 17 18 7 Anhang 19 7.1 The zen of python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 7.2 Nützliche Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2 1 Grundlagen Python ist eine interpretierte Programmiersprache, die immer einen Interpreter zur Ausführung benötigt. Das bedeutet, dass in python keine kompilierten “stand alone”-Programme geschrieben werden können. Dies stellt im allgemeinen Gebrauch allerdings keine große Einschränkung dar. Python wird häufig als eine leicht zu lernende Programmiersprache bezeichnet, was im Hinblick auf die einfache Syntax sicherlich auch stimmt. Das berühmte “Hello-World”-Beispiel lässt sich in nur einer Zeile mit einem einzigen Befehl ausführen: In [87]: print "Hello World" Hello World oder im jupyter notebook sogar ganz ohne einen Befehl: In [88]: ’Hello World’ Out[88]: ’Hello World’ Das liegt daran, dass in jupyter notebooks der Rückgabewert der letzten Funktion im Output der Zelle dargestellt wird. Gibt es keinen Rückgabewert erhält man nützliche Infos zum Objekt selbst. Außerdem muss man in python keine Variablentypen beim deklarieren einer Variable festlegen. So etwas wie double d = 2.; gibt es in python nicht. Der typ einer Variablen wird in python dynamisch angepasst: In [89]: a = 2 print "a is of type:", type(a) a = 2. print "now a is of type:", type(a) a is of type: <type ’int’> now a is of type: <type ’float’> Dies führt zu einer steilen Lernkurve, da man sich um die ganzen Typzuweisungen (und auch Deklararation) nicht kümmern muss. Allerdings kann dies auch zu “unsauberen” Programmierungen führen, was später zu ungewollten Programmverhalten führen kann. In python gilt also der Grundsatz: “What quacks like a duck is a duck!”. (Siehe try Anweisungen und fortgeschrittene Funktionsdefinition) 1.1 Stärken Python besitzt eine Menge nützliche Bibliotheken, hier Module genannt, die einem viel Programmierarbeit abnehmen. Im Zweifel gilt der Grundsatz, dass sich schon jemand vor euch mir dem Problem befasst hat (und es wahrscheinlich auch besser gelöst hat). Nützliche Module sind beispielsweise numpy (numerische Rechnung auf numpy.arrays), scipy (spezielle Funktionen, numerische Integration, statistische Tests), pandas (Datenverarbeitung bei großen Datenmengen) und matplotlib (2-dimensionale Plots) Weiterhin ist python bei der Ausführung von internen funktionen, wie range(10000).sum(), sehr schnell. Hier sollte man nicht versuchen das Problem durch einen handgeschriebenen loop zu lösen. 1.1.1 jupyter/ipython notebook Eine Umgebung, die auf einem virtuellen Server läuft, und es ermöglicht kleine Code-Schnipsel aber auch größere Programme effizient zu testen. Das vorliegende Skript ist als jupyter notebook geschrieben worden. 1.2 Schwächen Die Standardimplementierung von python “CPython” besitzt einen sog. “Global Interpreter Lock (GIL)” dieser macht eine intrinsische Parallelisierung unmöglich. Dieses Problem lässt sich entweder durch andere Implentierungen z.B. PyPy umgehen oder durch das Vorschalten eines anderen Programms z.B. mpi. In den Anfängen sollte dies aber keine Einschränkung darstellen. 3 1.3 Download Eine ziemlich umfangreiche Zusammenstellung aller wichtigen Module findet sich im Downloadpaket von anaconda unter https://www.continuum.io/downloads. Pakete können hier ganz einfach nachinstalliert werden: Einfach in der Shell eintippen: $ conda install <paket> 2 Erste Schritte Kommen wir nun zu den wirklichen Basics einer jeden Programmiersprache. 2.1 Basisklassen Es gibt verschiedene, unterschiedliche Basisklassen, die alle ihre Vor- und Nachteile haben. Hier stellen wir nun die wichtigsten vor. 1. Integer: Int = 42 2. Float: Float = 3.142 3. String: Str = Hello World 4. List: List = [’A’, 2., [2, 3, 4], ’Hello’] 5. Tuple: Tup = (1, 2, 3) 6. Dictionary: Dict = {’A’:2., ’Hello’:’World’, (1, 2):(3., 4.)} 7. NumpyArray: Arr = numpy.array([[1, 2, 3], [4, 5, 6]]) Für den letzten Typ muss zuvor das Modul “numpy” importiert werden. In [90]: # Importiere das Modul numpy. Auf die Funktionen kann man nun mit "np.<funktion>/<class>" import numpy as np Int = 2; Float = 3.142; Str = "Hello World"; List = [’A’, 2., [2, 3, 4], ’Hello’] Tup = (1, 2, 3); Dict = {’A’:2., ’Hello’:’World’, (1, 2):(3., 4.)} Arr = np.array([[1, 2, 3], [4, 5, 6]]) print "Int = {}".format(Int) print "Pi = {}".format(Float) print "Str = {}".format(Str) print "List = {}".format(List) print "Tup = {}".format(Tup) # Ein erster for loop: for key, value in Dict.items(): print "key = {}".format(key), ’\t’, "value = {}".format(value) print "Arr = {}".format(Arr) Int = 2 Pi = 3.142 Str = Hello World List = [’A’, 2.0, [2, 3, 4], ’Hello’] Tup = (1, 2, 3) key = A value = 2.0 key = (1, 2) value = (3.0, 4.0) key = Hello value = World Arr = [[1 2 3] [4 5 6]] 4 2.2 Grundlegende Rechenoperationen In diesem Abschnitt stellen wir kurz die grundlegenden Rechenoperationen vor. Natürlich können in python alle Grundrechenarten ausgeführt werden. Auch hier sind, wie bei allen Programmiersprachen, die Probleme mit der Fließkommagenauigkeit u.Ä. zu beachten. In [91]: a = 1 b = 2 A = 1. B = 2. c = a + b d = a / b D = A / B e = a - b f = a * b print "c = a+b = {}".format(c) print "e = a-b = {}".format(e) print "f = a*b = {}".format(f) # Achtung bei der Division von Integern print "d = a/b = {}".format(d) print "D = A/B = {}".format(D) c e f d D = = = = = a+b a-b a*b a/b A/B = = = = = 3 -1 2 0 0.5 Man kann auch mit Arrays elementweise Rechenoperationen ausführen. In [92]: Einer = np.array(range(10)) Quadrate = Einer**2. print "Einer**2. = {}".format(Quadrate) Zehner = Einer*10 print "Einer*10 = {}".format(Zehner) print """ Man achte auf die unterschiedlichen Typen von "Quadrate" und "Zehner". Dies wird durch eine interne Typkonvertierung verursacht.""" Einer**2. = [ 0. 1. 4. 9. 16. 25. Einer*10 = [ 0 10 20 30 40 50 60 70 80 90] 36. 49. 64. 81.] Man achte auf die unterschiedlichen Typen von "Quadrate" und "Zehner". Dies wird durch eine interne Typkonvertierung verursacht. Doch auch auf klassische Vektoroperationen muss man nicht verzichten: In [93]: # Vektor definieren e_x = np.array([1., 0., 0.]) # Vektor definieren e_y = np.array([0., 1., 0.]) # Skalarprodukt print "<e_x, e_y> = {}".format(np.dot(e_x, e_y)) # und Kreuzprodukt e_z = np.cross(e_x, e_y) 5 for name, vec in zip([’e_x’, ’e_y’, ’e_z’], [e_x, e_y, e_z]): print name+’ = {}’.format(vec) <e x, ex = ey = ez = e [ [ [ y> = 0.0 1. 0. 0.] 0. 1. 0.] 0. 0. 1.] 2.3 Ein erster Plot (matplotlib) Webpage: http://matplotlib.org/ Citing: Hunter, J. D. Matplotlib: A 2D graphics environment, Computing in Science & Engineering, 9, 90–95 (2007) Häufig geht es bei der Auswertung von Versuchen darum, Daten möglichst passend darzustellen. Das ist keine leichte Aufgabe aber zumindest das Erstellen von grundlegenden Abbildungen ist mit python sehr einfach. In [94]: #jupyter line magic: sorgt dafür, dass die Plots in die Seite eingebunden werden. %matplotlib inline # importiert die Graphik-Bibliothek import matplotlib.pyplot as plt # x # y 200 x-Werte zwischen -4 Pi und 4 Pi erstellen = np.linspace(-4*np.pi, 4*np.pi, 200) y-Werte berechnen = np.sin(x) # Den grundsätzlichen Plot erstellenn plt.plot(x, y) # x-Achsen-Beschriftung plt.xlabel("x") # y-Achsen-Beschriftung plt.ylabel("f(x)=sin(x)") # Titel plt.title(r"Figure 1: Sinusfunktion im Bereich $x\in(-4\pi, 4\pi)$") # Plot ausgeben plt.show() 6 3 Fortgeschrittene Programmstrukturen Funktionen, Generatoren, if-statements und Schleifen Im vorangegangenen Abschnitt sind die Basisklassen eingeführt worden und ein erster simpler Plot ist erstellt worden. Im nächsten Kapitel werden nun etwas fortgeschrittenere Techniken erläutert. Diese erlauben es den Code besser zu strukturieren und Teile des Codes immer wieder zu verwenden. Grundsätzlich kann man auch selbst komplett eigene Module entwickeln und dann wie z.B. numpy in den eigenen Code einbauen. Das geht sogar ohne das Verständinis komplizierten Konstrukte wie Klassen o.Ä. 3.1 Funktionen Funktionen sind das wohl nützlichste Handwerkszeug um python Code les- und wiederverwendbar zu gestalten. Im Grunde funktionieren sie wie eine einfache Zuordnungsvorschrift x → f(x). Funktionen werden mit einem def eingeleitet und einen return bestimmt den Rückgabewert: In [96]: def quadrat(x): """Gibt das Quadrat des Eingabewertes zurück""" return x*x # Funktioniert sowohl mit einem integer print quadrat(2) # als auch mit einem Array: print quadrat(np.linspace(1,10, 10)) # aber nicht mit einer Liste: 7 print quadrat(range(10)) # Das Programm wird abgebrochen! 4 [ 1. 4. 9. 16. 25. 36. 49. 64. 81. 100.] --------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-96-e7ae568bd5b5> in <module>() 10 11 # aber nicht mit einer Liste: ---> 12 print quadrat(range(10)) 13 # Das Programm wird abgebrochen! <ipython-input-96-e7ae568bd5b5> in quadrat(x) 1 def quadrat(x): 2 """Gibt das Quadrat des Eingabewertes zurück""" ----> 3 return x*x 4 5 # Funktioniert sowohl mit einem integer TypeError: can’t multiply sequence by non-int of type ’list’ Mann kann diesen Fehler in einem größeren Programmablauf abfangen. Dazu nutzt man einen try-undexcept-Block: In [98]: def quadrat_neu(x): """Funktion: quadrat_neu(x): Gibt das Quadrat des Eingabewertes zurück Außerdem werden Fehler jetzt vernünftig abgefangen""" try: x2 = x*x return x2 # Fängt nur Fehler vom Typ "TypeError" ab except TypeError, e: print "Input Variable hat den falschen Type, sie hat den Typ: {}".format(type(x)) print "Error Message:", e return None # Funktioniert immer noch sowohl mit einem integer print "Out1", quadrat_neu(2) # als auch mit einem Array: print "Out2", quadrat_neu(np.linspace(1,10, 10)) # und immer noch nicht mit einer Liste. # Aber der Fehler wird ordentlich abgefangen und das Programm nicht abgebrochen! print "Out3", quadrat_neu(range(10)) # Das Programm wird nicht abgebrochen! 8 # Außerdem kann man sich den docstring der Funktion anschauen: print "Out4", quadrat_neu.func_doc Out1 4 Out2 [ 1. 4. 9. 16. 25. 36. 49. 64. 81. 100.] Out3 Input Variable hat den falschen Type, sie hat den Typ: <type ’list’> Error Message: can’t multiply sequence by non-int of type ’list’ None Out4 Funktion: quadrat neu(x): Gibt das Quadrat des Eingabewertes zurück Außerdem werden Fehler jetzt vernünftig abgefangen 3.2 Generatoren Generatoren sind eine ziemlich praktische Sache und können ähnlich verwendet werden wie Funktionen. Eine detaillierte Beschreibung würde hier zu weit gehen, aber grundsätzlich stellt ein Generator das Grundgerüst für viele python Implementierungen dar. Häufig sind Generatoren bei sehr großen Schleifendurchläufen hilfreich, oder wenn eine Aktion nur ein einziges Mal durchgeführt werden muss. Ein Generator erzeugt die Ausgabe-Objekte nur auf “Zuruf” und nicht schon vorab, was eine Menge Speicherplatz sparen kann. Hier ein einfaches Beispiel: In [99]: def Fibonacci(n): """Fibonacci-Generator: Generates the n first Fibonacci numbers""" x, y = 0,1 for i in range(n): if x < y: yield x x=x+y else: yield y y=x+y # Der Fibonacci-Genrator an sich gibt print "Out1", Fibonacci(10) noch keine Werte zurück. # Erst wenn z.B. in einem Loop darüber iteriert wird, werden nach und nach Werte erzeugt. print "Out2" for f in Fibonacci(5): print f # Auch in einer list comprehension kann man einen Generator nutzen. print "Out3", sum([x for x in Fibonacci(10)]) Out1 <generator object Fibonacci at 0x0000000003E335E8> Out2 0 1 1 2 3 Out3 88 9 3.3 Loops/Schleifen An der ein oder anderen Stelle sind uns Loops schon begegnet. Grundsätzlich gibt es es in python zwei Arten von loops: 1. for-each-loop 2. while-loop Sie tun eigentlich genau das, was der Name sagt. Wichtig ist in python, dass man direkt über viele Objekte loopen kann, ohne umständlich über die Indizes gehen zu müssen: In [100]: Farben = [’Blau’, ’Rot’] # So nicht: for i in range(len(Farben)): print Farben[i] # so auch nicht: j = 0 while j < len(Farben): print Farben[j] j += 1 # Besser so: for Farbe in Farben: print Farbe # oder so, wenn man auch an den Index dran möchte: for index, Farbe in enumerate(Farben): print index, Farbe Blau Rot Blau Rot Blau Rot 0 Blau 1 Rot 4 Namen, Objekte und Referenzen Python hat eine etwas gewöhnungsbedürftige Art Objekte zu referenzieren. Hier soll nur eine kleine Übersicht gegeben werden. Weiterführende Infos auch zur Speicherverwaltung in python kann man z.B. unter dem Stichwort “Reference counter” finden. Grundsätzlich gibt es zwei unterschiedliche Objektarten in python: Mutable (veränderliche) und immutable (unveränderliche) Objekte. Immutable sind z.B. Strings, Tupel und Integer. Der große Rest der python Objekte ist mutable. Sie verhalten sich beim Referenzieren grundverschieden. Das lässt sich am einfachsten in einem kleinen Beispiel zeigen: In [101]: a = 1 print b = a print b = 2 print # Erzeugt den Namen "a", erzeugt ein Integerobjekt mit dem Wert "1" # und referenziert "1" mit "a" "a = {}".format(a) # Erzeugt den Namen "b", erzeugt eine Kopie vom Objekt auf das "a" referenziert und # referenziert dieses mit "b" "b = {}".format(b) # Erzeugt ein neues Objekt "2", löscht das alte Objekt und referenziert "2" mit "b" "a, b = {}, {}".format(a, b) 10 a = 1 b = 1 a, b = 1, 2 Hier verhält sich also alles so, wie man es erwarten könnte. b=a legt eine physische Kopie vom Inhalt von “a” an. Wenn wir diese Kopie dann mit b=2 ändern, ändert sich der Wert von “a” nicht. Intern passiert leider (und zum Glück für die Performance) noch mehr, da häufig genutzte Objekte, wie kleine Integer, immer im Speicher bereit gehalten werden und gar nicht erst gelöscht werden. In [102]: A = [1, 2, 3] print B = A print B[-1] print # Erzeugt den Namen "A", erzeugt eine Liste mit dem Wert "[1, 2, 3]" # und referenziert "[1, 2, 3]" mit "A" "A = {}".format(A) # Erzeugt den Namen "B" und referenziert, das Objekt das "A" referenziert auch # mit "B". Es wird keine(!) neue Kopie angelegt. "B = {}".format(B) = -4 # Ändert das Objekt, das von "B" referenziert wird. "A, B = {}, {}".format(A, B) #ACHTUNG: Es ändert sich natürlich auch der Wert von "A" A = [1, 2, 3] B = [1, 2, 3] A, B = [1, 2, -4], [1, 2, -4] Man kann dieses Verhalten gut oder schlecht finden. In der ein oder anderen Situation is es auf jeden Fall nützlich. Man darf aber auf keinen Fall vergessen, dass python dieses Verhalten hat. Um eine Kopie einer Liste anzulegen, kann man wie folgt vorgehen: In [103]: A = [1, 2, 3] # Erzeugt den Namen "A", erzeugt eine Liste mit dem Wert "[1, 2, 3]" # und referenziert "[1, 2, 3]" mit "A" print "A = {}".format(A) B = A[:] # Erzeugt den Namen "B" und referenziert eine Kopie des Objektes, welches von # referenziert wird print "B = {}".format(B) B[-1] = -4 # Ändert das Objekt, das von "B" referenziert wird. print "A, B = {}, {}".format(A, B) A = [1, 2, 3] B = [1, 2, 3] A, B = [1, 2, 3], [1, 2, -4] Manche andere python Klasse besitzt eine eingebaute Kopierfunktion, z.B. numpy.array.copy(), die eine Kopie des Objektes anlegt und nicht nur eine neue Referenz. 5 Numerische Rechnungen: numpy Webpage: http://www.numpy.org/ Citing: Stéfan van der Walt, S. Chris Colbert and Gaël Varoquaux. The NumPy Array: A Structure for Efficient Numerical Computation, Computing in Science & Engineering, 13, 22-30 (2011) Bereits in den vergangenen Abschnitten sind einige numpy Funktionen aufgetaucht. Hier werden jetzt ausgewählte Funktionen von numpy nochmal genauer unter die Lupe genommen. Außerdem werden die Laufzeitunterschiede verschiedener Techniken beleuchtet. 5.1 N-dimensionales Array: numpy.ndarray() Das wichtigste Objekt in python ist das n-dimensionale Array, welches die Daten für die wichtigsten Operationen bereit hält. Es ist sozusagen der Container, der die Daten für die mathematischen Operationen 11 zur Verfügung stellt. Das numpy.ndarray stellt selbst schon viele nützliche Funktionen, die mit dem PunktOperator ndarray.function() ausgeführt werden, zur Verfügung. Doch zu Beginn steht der Zugriff auf die einzelnen Elemente im Array: In [104]: import numpy as np # 1-dim Array von 1...9 Arr_1dim = np.linspace(1,27, 27) print "Out1", Arr_1dim # Gibt die Form des Arrays an. print "Out2", Arr_1dim.shape # Gibt ein Array der ersten drei Elemente zurück. print "Out3", Arr_1dim[:4] # Gibt ein Array der letzten drei Einträge zurück. print "Out4", Arr_1dim[-3:] # Gibt ein Array zurück, indem nur jeder dritte Eintrag genutzt wird. print "Out5", Arr_1dim[::3] # Wir bilden nun ein 3x9 array. Arr_2dim = Arr_1dim.copy().reshape(3, 9) print "Out6", Arr_2dim print "Out7", Arr_2dim.shape # 1. Spaltenvektor print "Out8", Arr_2dim[:,0] # 1. Zeilenvektor print "Out9", Arr_2dim[0,:] # Auch ein 3x3x3 Array funktioniert. Arr_3dim = Arr_1dim.copy().reshape(3, 3, 3) print "Out10", Arr_3dim print "Out11", Arr_3dim.shape # Der Eintrag mit Index (0,0,0) print "Out12", Arr_3dim[0,0,0] Out1 [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. Out2 (27L,) Out3 [ 1. 2. 3. 4.] Out4 [ 25. 26. 27.] Out5 [ 1. 4. 7. 10. 13. 16. 19. 22. 25.] Out6 [[ 1. 2. 3. 4. 5. 6. 7. 8. 9.] [ 10. 11. 12. 13. 14. 15. 16. 17. 18.] [ 19. 20. 21. 22. 23. 24. 25. 26. 27.]] Out7 (3L, 9L) Out8 [ 1. 10. 19.] Out9 [ 1. 2. 3. 4. 5. 6. 7. 8. 9.] Out10 [[[ 1. 2. 3.] [ 4. 5. 6.] [ 7. 8. 9.]] [[ 10. [ 13. [ 16. 11. 14. 17. 12.] 15.] 18.]] [[ 19. [ 22. [ 25. 20. 23. 26. 21.] 24.] 27.]]] 12 11. 12. 27.] 13. 14. 15. Out11 (3L, 3L, 3L) Out12 1.0 Nützliche Funktionen, die jedes Array besitzt: In [105]: # Summe über alle Elemente print "Out1", Arr_3dim.sum() # Der Mittelwert und viele weitere Funktionen sind auch verfügbar print "Out2", Arr_3dim.mean() # Man kann auch über nur eine Achse summieren print "Out3", Arr_3dim.sum(axis=(0)) # Oder auch über mehrere Achsen print "Out4", Arr_3dim.sum(axis=(0,2)) Out1 378.0 Out2 14.0 Out3 [[ 30. [ 39. 42. [ 48. 51. Out4 [ 99. 33. 36.] 45.] 54.]] 126. 153.] Das Ganze funktioniert auch in weiteren Dimensionen, ist dann aber natürlich nicht mehr vernünftig zu visualisieren. Es gibt noch viele weitere Dinge, die numpy leisten kann, wie beispielsweise die Integration von C/C++ oder FORTRAN code. Auch eine ziemlich mächtige Random-number-Einbindung, als numpy.random, ist darunter. 5.2 Performance iPython bietet mit %%timeit eine leichte Line-magic-Funktion, um die Performance von Codeteilen zu testen. Wir testen hier die Zeit, die zum Aufsummieren der ersten 1000 natürlichen Zahlen benötigt wird: In [106]: %%timeit Numbers = np.linspace(1, 1000, 1001) Sum = 0 for i in range(len(Numbers)): Sum += Numbers[i] 1000 loops, best of 3: 619 µs per loop In [107]: %%timeit Numbers = np.linspace(1, 1000, 1001) Sum = 0 for n in Numbers: Sum += n 1000 loops, best of 3: 567 µs per loop In [108]: %%timeit np.sum(np.linspace(1, 1000, 1001)) 10000 loops, best of 3: 55.3 µs per loop In [109]: %%timeit np.linspace(1, 1000, 1001).sum() 13 10000 loops, best of 3: 48.5 µs per loop Hier wird also deutlich, dass man mit sehr geringem Aufwand die Performance seines Codes signifikant verbessern kann. Je größer die Arrays werden, desto schneller werden die numpy-Funktionen gegenüber selbst geschriebenen Schleifen. Als praktischer Nebeneffekt wird der Code auch besser lesbar und leichter zu verwalten. Wenn man mit sehr großen Datenmengen zu tun hat, empfiehlt sich ein Blick auf die “pandas”-Bibliothek. Diese bringt nochmal Performancezuwachs gegenüber numpy und stellt eine Menge Funktionen zur statistischen Analyse bereit. 6 Wissenschaftliche Funktionen: SciPy Webpage: https://www.scipy.org/ Citing: Jones E, Oliphant E, Peterson P, et al. SciPy: Open Source Scientific Tools for Python, 2001-, http://www.scipy.org/ [Online; accessed 2017-04-06] Wie man Funktionen selbst schreibt, ist weiter oben schon vorgestellt worden. Allerdings kann das bei vielen in der Physik üblichen Funktionen langwierig und fehleranfällig sein. Niemand möchte freiwillig, Hypergeometrische-, Bessel- oder Sphärisch-Harmonische Funktionen 10.-Ordnung in ein Programm tippen. Aus diesem Grund bietet scipy eine Menge der gebräuchlichen aber auch weniger üblichen Funktionen in einem auch auf Performance optimierten Paket an. Neben diesen in scipy.special hinterlegten Funtionen, beinhaltet scipy auch eine Menge Algorithmen, um bspw. numerisch zu integrieren oder Nullstellen zu berechnen. 6.1 scipy.special Eventuell ist die Bessel’sche Differentialgleichung noch aus der Mechanik oder Quantenmechanik bekannt: 2 2 2 f = 0. Bessel’sche Differentialgleichung x2 ddxf2 + x df dx + x − ν Die Lösungen dieser Differentialgleichung sind die Besselfunktionen. Es gibt Besselfunktion 1. Art Jν (x) = P∞ (−1)r ( x2 )2r+ν Jν (x) cos(νπ)−J−ν (x) , welche noch den Parameter ν, r=0 Γ(ν+r+1)r! und Besselfunktion 2. Art Yν (x) = sin(νπ) welcher die Ordnung des Lösung angibt modifiziert wird. Diese Funktion is in scipy hinterlegt: In [110]: # Importieren der Besselfunktionen from scipy.special import jv, yv # Definitionsbereich für den Plot festlegen z = np.linspace(0, 20, 200) # Es sollen die ersten drei Ordnungen geplottet werden order = [0,1,2] # Legt die Größe (in inch) der Abbildung fest. plt.figure(figsize=(7.5, 4)) for o in order: # Berechnen der Besselfunktion B = jv(o, z) #Plotten und Erstellen eines Labels. plt.plot(z, B, label=r’$\nu$ = ’+str(o)) plt.title(’Besselfunktion 1. Art’) # Die Label können dann in der Legende verarbeitet werden. plt.legend(loc=’best’) plt.show() 14 In [111]: plt.figure(figsize=(7.5, 4)) for o in order: D = yv(o, z) plt.plot(z, D, label=r’$\nu$ = ’+str(o)) plt.title(’Besselfunktion 2. Art’) plt.ylim(-2, .6) plt.legend(loc=’best’) plt.show() 15 Man kann bei der Bessel-Funktion 2. Art die (logarithmische) Polstelle an der Stelle x = 0 erkennen. 6.2 Numerische Integration: scipy.integrate.quad Um die Numerische Integration wird man früher oder später in der wissenschaftlich Laufbahn nicht rum kommen. Selbst, wenn sich Ergebnisse noch analytisch aufschreiben lassen, ist die Auswertung der Lösungen häufig nur noch numerisch möglich. Hier wird ein einfaches Beispiel betrachtet, welches mit der analytischen Lösung verglichen werden kann: In [112]: # quad ist eine einfache, aber oft ausreichende, Integrationsroutine from scipy.integrate import quad def g(x): """Funktion, die integriert werden soll""" return x def f(x): """Stammfunktion für den Fall, dass x_0=0 ist""" return 0.5*x*x # Berechnung des Integrals print ’F(x;x_0) = {}’.format(quad(g, 0, 5)[0]) def F(x0, x1, func=f): """Eine Funktion, die sich wie die Stammfunktion verhält Sie funktioniert sohl für einzelne Werte als auch für Arrays.""" try: F=np.array([quad(func, x0, x1) for x0, x1 in zip(x0, x1)]) return F[:,0] except TypeError: F = quad(func, x0, x1) return F[0] # Untere Grenze x0 = np.zeros(100) # Obere Grenze x1 = np.linspace(0,10,100) plt.plot(x1, f(x1), label=’exact’) plt.plot(x1, F(x0, x1, g), linestyle=’-.’, label=’quad’) plt.legend(loc=’best’) plt.title(’Vergleich zwischen numerischer Integration \n und exaktem Ergebnis’) plt.show() F(x;x 0) = 12.5 16 Man erkennt, dass für eine so einfache, glatte Funktion die numerische Integration sehr gut funktioniert. Sollte die Funktion sich nicht so gutartig verhalten, oder über einen sehr großen Bereich ausgewertet werden müssen, bietet quad eine Vielzahl an Optionen, um Einfluss auf die Integration zu nehmen. Auch kann man natürlich im Vorfeld aus der Analysis bekannte Techniken, wie Substituation oder das Zerlegen der Funktion in kleine Abschnitte, nutzen, um bessere Resultate zu erzielen. Welches Verfahren zum Erfolg führt, hängt selbstverständlich stark vom gegebenen Problem. 6.3 Nullstellensuche: scipy.optimize.root Eine weitere häufige Problemstellung ist die Suche von Nullstellen. Das ist numerisch nicht so einfach und vor allem das Finden von mehreren Nullstellen (oder gar allen) ist sehr aufwendig. Die Suche funktioniert häufig nur mit der Vorgabe eines Startwertes. Ob eine Nullstelle gefunden werden kann und wenn ja welche, hängt auf jeden Fall von der Wahl des Startwertes ab. Betrachten wir das einfache Beispiel einer nach unten verschobenen Parabel: In [113]: # Import der passenden Funktion from scipy.optimize import root def f(x): """Nach unten verschobene Parabel Nullstellen sind x_1=-2**0.5 und x_2=2**0.5""" return x*x-2 def jac(x): """Jacobi-Matrix der zu untersuchenden Funktion Nicht nötig, aber hilfreich. Erhöht die Konvergenzwahrscheinlichkeit.""" return 2*x 17 # Ausgabe der Nullstelle (inklusive vieler weiterer Infos, wenn gewünscht) print "x_1={}".format(root(f, x0=-0.1, jac=jac).x[0]) # Die gefundene Nullstelle hängt vom Startwert ab print "x_2=+{}".format(root(f, x0=0.1, jac=jac).x[0]) x 1=-1.41421356237 x 2=+1.41421356237 6.4 Regression / Funktionsfitting: scipy.optimize.curve fit Als letztes betrachten wir die Aufgabe, eine bekannte Funktion an Messdaten anzupassen. Diese Aufagbe sollte aus dem Praktikum bekannt sein und lässt sich sehr leicht mit python lösen. Wie bei allen numerischen Verfahren, gilt auch hier, dass eine vorsichtige Auswahl der Paramter zum Fitten nötig ist, um eine stabile Lösung zu finden, welche im besten Fall auch noch gegen die echte Lösung konvergiert. Im Beispiel betrachten wir eine exponentiell abklingende Sinus-Funktion, bei der wir die Zerfallszeit τ und die Amplitude A bestimmen wollen. Die Messungenauigkeit wird durch sog. Weißes Rauschen simuliert, welches einer Normalverteilung folgt. Es handelt sich also um eine Idealisierung des Messprozesses, welcher die Konvergenz des Fittings verbessert. In [114]: # Import von curve_fit from scipy.optimize import curve_fit def Expectation(x, A, tau): """Funktion, die unseren Prozess beschreibt. """ E = A*np.sin(x)*np.exp(-x/tau) return E def Expectation2(x, A, tau, phi_0): """Alternative Funktion, um einen misslungenen Fit zu zeigen""" E = A*np.cos(x)*np.exp(-x/tau) return E # Messpunkte x = np.linspace(0, 20, 50) # Weißes Rauschen, mit Mittelwert E=0 und Varianz Var=0.8 white_noise = np.random.normal(0.,0.8, 50) # Messdaten, erzeugt aus dem echten Wert verschmiert mit dem Rauschen. Data = Expectation(x, 10., 4.)+white_noise # Plotten der Messwerte. plt.plot(x, Data, linewidth=0., marker=’o’, label=’Data’) # Plotten der echten Lösung plt.plot(x, Expectation(x, 10., 4.), label="True solution") # Curve Fit, die zu fittenden Parameter werden auomatisch ermittelt. # popt enhält die best-fit Parameter. # pcov enthält die Kovarianzmatrix, aus der sich der Fehler berechnen lässt. popt, pcov = curve_fit(Expectation, x, Data) # Plot der best-fit Funktion plt.plot(x, Expectation(x, *popt), linestyle=’-.’, label=’Fit’) print "Fit1: A={}({}), tau={}({})".format(np.round(popt[0], 1), np.round(np.sqrt(np.diag(pcov) np.round(popt[1], 1), np.round(np.sqrt(np.diag(pcov))[1], 18 # Das gleiche nochmal für dem anderen Fit. popt, pcov = curve_fit(Expectation2, x, Data) plt.plot(x, Expectation2(x, *popt), linestyle=’--’, label=’Fit2’) print "Fit2: A={}({}), tau={}({})".format(np.round(popt[0], 1), np.round(np.sqrt(np.diag(pcov) np.round(popt[1], 1), np.round(np.sqrt(np.diag(pcov))[1], plt.legend(loc=’best’) plt.show() Fit1: A=10.0(0.7), tau=4.6(0.5) Fit2: A=2.6(inf), tau=1.6(inf) 7 Anhang 7.1 The zen of python • Beautiful is better than ugly. • Explicit is better than implicit. • Simple is better than complex. • Complex is better than complicated. • Flat is better than nested. • Sparse is better than dense. • Readability counts. • Special cases aren’t special enough to break the rules. • Although practicality beats purity. 19 • Errors should never pass silently. • Unless explicitly silenced. • In the face of ambiguity, refuse the temptation to guess. • There should be one—and preferably only one—obvious way to do it. • Although that way may not be obvious at first unless you’re Dutch. • Now is better than never. • Although never is often better than right now. • If the implementation is hard to explain, it’s a bad idea. • If the implementation is easy to explain, it may be a good idea. • Namespaces are one honking great idea—let’s do more of those! |Tim Peters Programs must be written for people to read, and only incidentally for machines to execute. |Abelson & Sussman, Structure and Interpretation of Computer Programs 7.2 Nützliche Links Hilfreiche Hinweise, wie man einen Code im python-Stil schreiben sollte: good code (pdf) Englisch Anleitung was man alles besser vermeiden sollte: bad code (webpage) English 20