{ "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.4" }, "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Einf\u00fchrung in Python (numpy) und DUNE\n", "\n", "In der Vorlesung verwenden wir Python(3) mit den [`numpy`](http://www.numpy.org)- und [`scipy`](https://www.scipy.org)-Modulen.\n", "Sp\u00e4ter verwenden wir auch [DUNE](https://www.dune-project.org)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Python-Grundlagen\n", "\n", "### Zahlen, Zeichenketten, Tupel und Listen\n", "\n", "Grundlegende Datentypen sind (Ganz-)zahlen, Zeichenketten, Tupel und Listen.\n", "\n", "Tupel k\u00f6nnen sp\u00e4ter nicht mehr ver\u00e4ndert werden, Listen hingegen schon.\n", "Ansonsten verhalten sich beide gleich." ] }, { "cell_type": "code", "collapsed": true, "input": [ "ganzzahl = 3\n", "andere_zahl = ganzzahl + 4**2\n", "zahl = 3.2\n", "hallo = \"Hallo Welt!\"\n", "tupel = (1, 2, \"Hallo\")\n", "liste = [1, 2, \"Hallo\"]" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mit der `str.format`-Methode k\u00f6nnen Ausgaben formatiert werden.\n", "Die eigentlich Ausgabe erfolgt \u00fcber die `print`-Funktion." ] }, { "cell_type": "code", "collapsed": false, "input": [ "ausgabe = \"Die Zahl ist {}; die andere Zahl ist {}\".format(ganzzahl, andere_zahl)\n", "print(ausgabe)\n", "print(hallo, zahl)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ist der letzte Befehl in einem Block nur ein Ausdruck, wird dieser Wert ausgeben.\n", "Die Darstellung kann sich von der Ausgabe durch `print` unterscheiden (`print(repr(...))` gibt aber die gleiche Darstelleung)." ] }, { "cell_type": "code", "collapsed": false, "input": [ "ausgabe" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Bedingte Anweisungen, Schleifen\n", "\n", "In Python werden Bl\u00f6cke nach Bedingungen und in Schleifen durch Einr\u00fcckung ausgezeichnet.\n", "Vergleiche mit `==`; Verbinden von Ausdr\u00fccken mit `and`, `or`, `not`." ] }, { "cell_type": "code", "collapsed": false, "input": [ "if ganzzahl % 2 == 0 or False:\n", " print(\"ganzzahl ist gerade\")\n", "else:\n", " print(\"ganzzahl ist ungerade\")" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "for i in range(0, 3): # oder auch nur `range(3)`\n", " print(i)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "for i in range(0, 50, 10):\n", " print(i)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "for i in tupel:\n", " print(i)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Funktionen" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def plus1(x):\n", " return x + 1\n", "\n", "plus1(2)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sehr kurze Funktionen, die wie `plus1` nur aus einer `return`-Anweisung bestehen, lassen sich auch als `lambda`-Ausdruck schreiben:`" ] }, { "cell_type": "code", "collapsed": false, "input": [ "plus2 = lambda x: x + 2\n", "\n", "plus2(2)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "F\u00fcr noch nicht implementierte Funktionen oder bedingte Anweisungen, kann `pass` verwendet werden damit das Programm syntaktisch korrekt bleibt:" ] }, { "cell_type": "code", "collapsed": true, "input": [ "def toBeDoneLater():\n", " pass" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "F\u00fcr die Parameter von Funktionen k\u00f6nnen Vorgabewerte angegeben werden.\n", "Der vorgegebene Wert wird verwendet, wenn beim Aufruf der Funktion kein anderer Wert angegeben wird:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "def eineFunktion(x, y=1234):\n", " print(\"Aufruf mit {}, {}\".format(x, y))\n", "\n", "eineFunktion(123, 456)\n", "eineFunktion(789)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Objektorientierte Programmierung\n", "\n", "In Python kann [objektorientiert programmiert](https://de.wikipedia.org/wiki/Objektorientierte_Programmierung) werden: hierbei werden Klassen definiert, die Datenfelder und Funktionen (Methoden) sinnvoll gruppieren.\n", "Dies wurde weiter oben bereits genutzt: der Aufruf `\"Die Zahl ist {}\".format(42)` hat ein Objekt der `str`-Klasse angelegt und dessen Methode `format` aufgerufen.\n", "\n", "Selbst kann eine Klasse `Klasse` mit den Methoden `__init__`, `eineMethode` und `nochEineMethode` wie folgt definiert werden:" ] }, { "cell_type": "code", "collapsed": true, "input": [ "class Klasse:\n", " def __init__(self, wert):\n", " self.meinWert = wert\n", " def eineMethode(self):\n", " print(self.meinWert)\n", " def nochEineMethode(self, x, y=2):\n", " print(\"Aufruf mit {}, {}\".format(x, y))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Da Methoden nichts anderes als Funktionen sind, k\u00f6nnen auch hier Vorgabewerte f\u00fcr Parameter angegeben werden.\n", "\n", "Nun k\u00f6nnen Objekte instanziiert werden und Methoden aufgerufen werden:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "objekt = Klasse(42)\n", "objekt.eineMethode()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Die Methode `__init__` wird bei der Instanziierung der Klasse (dem Ausdruck `Klasse(42)`) aufgerufen und kann etwa Attributen einen sinnvollen Anfangswert zuweisen (im Beispiel dem Attribut `meinWert`).\n", "\n", "Es gibt weitere magische Funktionsnamen, mit der zum Beispiel Objekte wie eine Funktion aufgerufen werden k\u00f6nnen (`__call__`) oder auf sie wie auf Listen zugegriffen werden kann (`__getitem__`):" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class NochEineKlasse:\n", " def __call__(self, x, y, z):\n", " print(\"Aufruf mit {}, {}, {}\".format(x, y, z))\n", " def __getitem__(self, i):\n", " return \"Zugriff auf Element {}\".format(i)\n", "\n", "objekt = NochEineKlasse()\n", "objekt(1, 2, 3)\n", "print(objekt[4])" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Eine etwas ausf\u00fchrlichere Einf\u00fchrung findet sich etwa im [Python3-Kurs von Bernd Klein](https://www.python-kurs.eu/python3_klassen.php)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Module laden\n", "\n", "Mit der `import`-Anweisung k\u00f6nnen Module oder einzelne Objekte aus Modulen geladen werden.\n", "Module k\u00f6nnen auch unter einem anderen Namen geladen werden; es ist z.B. \u00fcblich das `numpy`-Modul unter dem Namen `np` zu laden." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy\n", "import numpy as np\n", "from numpy import array" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Dichte Matrizen und Vektoren" ] }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Vektoren" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import numpy as np\n", "\n", "v = np.array([1, 2, 3])\n", "w = np.array([4, 5, 6])\n", "print(v, w)\n", "print(v[0])\n", "print(len(v))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "print(v+w)\n", "print(2*v)\n", "print(2*v+w)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Vorsicht**: * ist nicht das Skalarprodukt, sondern elementweises Produkt:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print(v*w)\n", "print(v.dot(w))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Matrizen" ] }, { "cell_type": "code", "collapsed": false, "input": [ "m = np.array([[1,2],[3,4]])\n", "print(m)\n", "print(m[1,0])\n", "print(m[1,:])\n", "print(m[:,0])" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "m = np.zeros((3,3))\n", "print(m)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "m = np.eye(3)\n", "print(m)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "m = np.ones((3,3))\n", "print(m)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Vorsicht**: * ist nicht Matrix-Vektor-Multiplikation!" ] }, { "cell_type": "code", "collapsed": false, "input": [ "print(m*v)\n", "print(m.dot(v))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "heading", "level": 3, "metadata": {}, "source": [ "Lineare Gleichungssysteme" ] }, { "cell_type": "code", "collapsed": false, "input": [ "A = np.array([[1, 1], [0, 1]])\n", "b = np.array([2, 1])\n", "x = np.linalg.solve(A, b)\n", "print(x)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## D\u00fcnn besetzte Matrizen\n", "\n", "Die Gleichungssysteme, die durch Diskretisierung einer PDE entstehen, k\u00f6nnen sehr gro\u00df werden, allerdings ist ein Gro\u00dfteil der Eintr\u00e4ge \"0\".\n", "Diese m\u00f6chte man nicht speichern und bei Rechenoperationen ebenfalls nicht verwenden.\n", "Um dies zu erreichen werden spezielle Datenstrukturen f\u00fcr d\u00fcnn besetzte Matrizen verwendet.\n", "In den \u00dcbungen verwenden wir das [`scipy` Python-Modul](https://www.scipy.org/) und beschr\u00e4nken uns zun\u00e4chst auf die beiden Klassen `lil_matrix` und `csr_matrix`.\n", "\n", "Die Klasse `lil_matrix` wird f\u00fcr das Aufstellen der Matrix verwendet; f\u00fcr die weiteren Rechnungen wird zu einer `csr_matrix` konvertiert und damit gerechnet.\n", "\n", "Hier kann man auch den Unterschied zwischen der Ausgabe von `print(m)` und nur `m` sehen." ] }, { "cell_type": "code", "collapsed": false, "input": [ "from scipy.sparse import lil_matrix\n", "\n", "m = lil_matrix((10, 10))\n", "m[0, :] = np.array([1,2,3,4,5,6,7,8,9,10])\n", "for i in range(1, 10):\n", " m[i, i] = 1\n", "print(m)\n", "m" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "print(m.todense())" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "A = m.tocsr()\n", "A" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "import scipy.sparse.linalg\n", "\n", "b = np.ones(10)\n", "x = scipy.sparse.linalg.spsolve(A, b)\n", "x" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Matrizen visualisieren\n", "\n", "Es ist manchmal hilfreich Matrizen als Bild darzustellen:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", "Adense = np.array(abs(A).todense())\n", "plt.figure(figsize=(10,10))\n", "plt.pcolormesh(Adense, cmap=plt.cm.gray_r)\n", "plt.show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "plt.matshow(np.eye(1024), cmap=plt.cm.gray_r)\n", "plt.show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "heading", "level": 2, "metadata": { "collapsed": true }, "source": [ "Punktgitter mit numpy" ] }, { "cell_type": "code", "collapsed": false, "input": [ "nx, ny = (5, 5)\n", "x = np.linspace(0, 1, nx)\n", "y = np.linspace(0, 1, ny)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "for i in range(nx):\n", " for j in range(ny):\n", " print(\"Point [{}, {}] is at {}\".format(i, j, (x[i], y[j])))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "m = np.zeros((nx, ny))\n", "for i in range(nx):\n", " for j in range(ny):\n", " p = x[i], y[j]\n", " m[i, j] = np.sin(p[0]) + p[1]" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "code", "collapsed": false, "input": [ "plt.pcolormesh(y, x, m)\n", "plt.show()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## DUNE\n", "\n", "DUNE, das [Distributed and Unified Numerics Environment](https://www.dune-project.org), ist ein C++-Framework f\u00fcr die Diskretisierung und L\u00f6sung von PDEs.\n", "F\u00fcr die \u00dcbungen werden wir die Python-Anbindung f\u00fcr Gitter und Funktionenr\u00e4ume verwenden.\n", "\n", "Werden bestimmte Datentypen das erste Mal verwendet, wird im Hintergrund der Python-Anbindung eine C++-Datei erzeugt und \u00fcbersetzt.\n", "Dies kann eine ganze Weile dauern..." ] }, { "cell_type": "code", "collapsed": false, "input": [ "import dune.common\n", "import dune.grid" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Gitter erzeugen\n", "\n", "Erzeuge ein strukturiertes Rechtecksgitter f\u00fcr das Einheitsquadrat mit 5x5 Elementen:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "data = (dune.common.reader.structured, \"cube\", [0.0, 0.0], [1.0, 1.0], [5, 5])\n", "grid = dune.grid.yaspGrid(data, dimgrid=2)\n", "print(grid)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mit einer `for`-Schleife kann \u00fcber die einzelnen Elemente des Gitters iteriert werden:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "for e in grid.elements():\n", " print(e.geometry.center)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Analog kann mit `grid.facets()` \u00fcber Randfl\u00e4chen, mit `grid.edges()` \u00fcber Kanten und mit `grid.vertices()` \u00fcber die Knotenpunkte iteriert werden." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "F\u00fcr unstrukturierte Gitter verwenden wir `UGGrid` mit dem `gmsh`-Dateiformat:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "reader = (dune.grid.reader.gmsh, \"ldomain.msh\")\n", "grid2 = dune.grid.ugGrid(reader, dimgrid=2)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Beim Ausprobieren ist es f\u00fcr die automatische Vervollst\u00e4ndigung praktisch, ein konkretes Element zu haben:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "firstElement = next(iter(grid.elements()))\n", "print(firstElement.geometry.center)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Geometrie\n", "\n", "F\u00fcr ein Element `element` liefert `element.geometry` die Geometrieinformationen.\n", "\n", "Wichtige Eigenschaften sind die Abbildungen von Koordinaten des Referenzelements (\"lokale Koordinaten\") auf Koordinaten des Gitterelements (\"globale Koordinaten\") durch `position: Tref \u2192 T` und `localPosition: T \u2192 Tref`, das Integrationselement `integrationElement(xlocal)` und die Ableitung der Koordinatenabbildung (`jacobianTransposed` bzw. `jacobianInverseTransposed`)." ] }, { "cell_type": "code", "collapsed": false, "input": [ "geometry = firstElement.geometry\n", "\n", "position = geometry.position([1., 0.])\n", "localPosition = geometry.localPosition(position)\n", "\n", "print(\"Lokale Koordinate (1., 0.) ist {} in globalen Koordinaten.\".format(position))\n", "print(\"Globale Koordinate {} ist {} in lokalen Koordinaten.\".format(position, localPosition))\n", "\n", "print(\"Integrationselement in (0.25, 0.25): {}\".format(geometry.integrationElement([0.25, 0.25])))\n", "print(\"jacobianInverseTransposed an (0.0, 0.0) ist\\n{}\".format(geometry.jacobianInverseTransposed([0., 0.])))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Quadraturregeln\n", "\n", "Integrale auf einem Referenzelement `\\hat e` werden meist mit einer Quadraturregel numerisch berechnet:\n", "$$ \\int_{\\hat e} f(x) dx \\approx \\sum_i w_i f(x_i) $$\n", "mit den Gewichten $w_i$ und St\u00fctzstellen $x_i$.\n", "\n", "In DUNE k\u00f6nnen Sie eine Quadraturregel `n`-ter Ordnung f\u00fcr das zu einer Entit\u00e4t `entity` geh\u00f6rigen Referenzelements eines Gitters `grid` wie folgt erhalten:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "entity = firstElement\n", "n = 2\n", "\n", "quadRule = grid._module.quadratureRule(entity.type, n)\n", "for pt in quadRule:\n", " print(\"Posititon = {} ; Gewicht = {}\".format(pt.position, pt.weight))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "Soll ein Integral auf einer Entit\u00e4t berechnet werden, muss zus\u00e4tzlich noch das [Integrationselement](#Geometrie) ber\u00fccksichtigt werden." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### IndexSet und Mapper\n", "\n", "Gitterentit\u00e4ten (Elemente, Seiten, Kanten, Punkte) k\u00f6nnen global durchgehend nummeriert werden.\n", "In DUNE gibt es hierf\u00fcr `IndexSet`s, die Objekte eines Geometrietyps (Dreiecke, Vierecke, Punkte, ...) durchgehend nummerieren, und `Mapper`.\n", "\n", "Die Nummerierung durch `IndexSet`s und `Mapper` bleibt nicht erhalten wenn das Gitter ver\u00e4ndert wird, aber das wird erst sp\u00e4ter relevant.\n", "\n", "Es gibt zwei Methoden, um aus einem `IndexSet` oder `Mapper` den Index f\u00fcr eine Entit\u00e4t zu erhalten:\n", "es kann entweder direkt `indexSet.index(entity)` aufgerufen werden oder mit `indexSet.index(entity, i, codim)` der Index der `i`-ten Unterentit\u00e4t mit Kodimension `codim` erhalten werden." ] }, { "cell_type": "code", "collapsed": false, "input": [ "idxSet = grid.indexSet\n", "print(\"Index des ersten Elements:\", idxSet.index(firstElement))\n", "for i in range(firstElement.subEntities(grid.dimension)):\n", " idx = idxSet.subIndex(firstElement, i, grid.dimension)\n", " print(\"Index des {}-ten Knoten des ersten Elements: {}\".format(i, idx))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Achtung:** Enth\u00e4lt das Gitter unterschiedliche Elementtypen (etwa Drei- und Vierecke), weist das `IndexSet` zwei verschiedenen Elementen die gleiche Nummer zu.\n", "M\u00f6chte man eine durchgehende Nummerierung, muss ein `Mapper` verwendet werden.\n", "Bei Knoten und Kanten gibt es das Problem nicht, da es dort nur jeweils einen Geometrietyp gibt (Punkte bzw. Linien).\n", "\n", "F\u00fcr den `MultipleCodimMultipleGeomTypeMapper` kann mit einer Funktion angegeben werden, Objekte welchen Geometrietyps durchnummeriert werden:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from dune.grid.map import MultipleCodimMultipleGeomTypeMapper as Mapper\n", "mapper = Mapper(grid, lambda gt: gt.dim == grid.dimension)\n", "print(\"Index des ersten Elements:\", mapper.index(firstElement))" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": null } ], "metadata": {} } ] }