{ "cells": [ { "cell_type": "markdown", "id": "e02dcbed-810e-46b5-8f7d-7846c559ee44", "metadata": {}, "source": [ "# Laserinterferometrie \n", "\n", "Als Interferometer bezeichnet man ein Messsystem, indem zwei Wellen miteinander so überlagert werden, dass sie miteinander interferieren. Die Wellen tragen Informationen der zu messenden Größe, typischerweise in der Phase der Welle, welche durch verschiedenste Methoden weiter verarbeitet werden kann. \n", "\n", "Prinzipiell kann Interferenz bei jeder Art von Welle auftreten, egal ob Licht-, Schall-, Materie- oder Wasserwelle. Wir wollen ins in diesem Kapitel auf die Laserinterferometer, also optische Interferometrie, konzentrieren. \n", "\n", ":::{figure-md} ifo\n", "\"ifo\"\n", "\n", "Die Vermessung von Gravitation mittels Laserinterferometrie. Ein Überblick ausgewählter Missionen. \n", ":::" ] }, { "cell_type": "markdown", "id": "f1b045d1-a620-4f80-803f-4b8151ec3e83", "metadata": {}, "source": [ "## Interferometer\n", "\n", "Interferometer eignen sich insbesondere für Hochpräzisionsmessungen. Das Funktionsprinzip ist immer ähnlich: Zwei Lichtstrahlen werden durch Optiken (Spiegel) und halbdurchlässige Spiegel auf verschiedene Bahnen gelenkt. Wichtig ist es, die beiden Lichtstrahlen an einem halbdurchlässigen Spiegel möglichst perfekt am Ende zu überlagern (interferieren), sodass ein Interferenzmuster am Ausgang des Interferometers entsteht. Dadurch werden die beiden interferierten Lichtstrahlen miteinander verglichen. Ändert sich die Phase eines Lichtstrahls, so ändert sich das Interferenzmuster. D.h. das Muster hängt von der optischen Weglänge ab, die die jeweiligen Lichtstrahlen bis zur Interferenz zurückgelegt haben. \n", "Dadurch können differentielle Weglängenunterschiede zwischen den zwei Interferometer*armen* gemessen werden, was für folgende Anwendungen benutzt werden kann:\n", "\n", "* Positionsänderungen\n", "* Brechungsindex oder Dichteänderungen in einem der beiden Wege\n", "* Winkelmessungen \n", "* Spektroskopie\n", "* Vibrationsmessungen\n", "* Geschwindigkeit- und Beschleunigungsmessung (siehe Radar)\n", "* Verformungsmessung\n", "* FTIR-Spektrometrie (chemische Analysen)\n", "* Gravitationswellendetektion\n", "\n", "Kennt man die Wellenlänge $\\lambda$ des verwendeten Laserlichts, also z.B. 1550nm (die entspricht der Telekommunikationswellenlänge), so kann man die optische Phase in eine Längenänderung konvertieren.\n", "\n", "$$ 2 \\cdot \\Delta L = \\left( \\frac{\\Delta \\phi}{2\\pi} \\right) \\cdot \\lambda $$\n", "\n", "- $\\Delta L$ ist die Weglängendifferenz,\n", "- $ \\Delta \\phi$ ist die Phasenverschiebung in Radiant,\n", "- $ \\lambda $ ist die Wellenlänge des Lichts.\n", "\n", "Der Faktor 2 resultiert daraus, dass in Interferometern der Laserstrahl reflektiert wird und somit die Weglängendifferenz 2x durchläuft. Dies erhöht die **Auflösung** der Messung um den Faktor 2. \n", "\n", "* Je kleiner die Wellenlänge, desto genauer können Positiosänderungen des Spiegels gemessen werden\n", "* Allerdings müssen bei höheren Dynamiken, also Bewegungen die eine Wellenlänge überschreiten, die Wechsel von Hell- nach Dunkel gezählt werden, wobei Fehler passieren können. Daher eignen sich homodyne Laserinterferometer nur bedingt zur Messung von größeren Veränderungen\n", "* Absolute Längenmessungen sind nicht möglich, lediglich relative Positionsänderungen von Objekten, da sich die Kennlinie periodisch wiederholt. Dies nennt man auch **Inkrementalgeber**. \n", "\n", "## Signal eines *homodynen* Laserinterferometers\n", "\n", "Bei homodyner Laserinterferometrie werden zwei Lichtstrahlen mit der exakt gleichen Wellenlänge bzw. Frequenz überlagert. Die jeweiligen elektromagnetischen Felder in den beiden Interferometerarmen können wiefolgt aufgeschrieben werden:\n", "\n", "$$E_1 = A_1 \\cos(\\omega_0 t)$$\n", "\n", "und \n", "\n", "$$E_2 = A_2 \\cos(\\omega_0 t + \\varphi)$$\n", "\n", "wobei wir die Amplitude mit $A$ bezeichnen und $\\omega_0$ die Laserfrequenz ist (im Bereichen von einigen Hundert THz). Weiterhin nehmen wir an, dass der Interferometerarm 1 seine Länge nicht ändert und wir im 2. Laserarm die Phase $\\varphi$ messen, die die zu messende Information beinhaltet (z.B. die Bewegung eines Spiegels). \n", "\n", "Diese beiden Strahlen werden an einem halbdurchlässigen Spiegel miteinander überlagert. Deren Leistung wird mittels einer Photodiode gemessen, die die einfallene Strahlungsleistung in einen Photostrom umwandelt. Die eintreffende Intensität lässt sich wiefolgt aus den beiden elektromagnetischen Feldern berechnen:\n", "\n", "\\begin{align}\n", "I &\\propto |E_1 + E_2|^2 = |A_1 \\cos(\\omega_0 t) + A_2 \\cos(\\omega_0 t + \\varphi)|^2 \\\\\n", "&= A_1^2 \\cos(\\omega_0 t)^2 + 2 A_1 A_2 \\cos(\\omega_0 t) \\cos(\\omega_0 t + \\varphi) + A_2^2 \\cos(\\omega_0 t + \\varphi)^2\n", "\\end{align}\n", "\n", "Mittels Additionstheoremen\n", "\n", "\\begin{align}\n", "\\cos^2x &= \\frac{1}{2}\\left(1+ \\cos(2x) \\right) \\\\\n", "\\cos x \\cos y &= \\frac{1}{2}\\left(\\cos(x-y) + \\cos(x+y) \\right) \n", "\\end{align}\n", "\n", "lässt sich die Gleichung weiter umschreiben zu:" ] }, { "cell_type": "markdown", "id": "e3cf596b-2715-4e89-bbdb-6dced7153894", "metadata": {}, "source": [ "\\begin{align}\n", "I &\\propto \\frac{A_1^2}{2} + \\frac{A_1^2}{2}\\cos(2\\omega_0 t) + A_1 A_2 \\left( \\cos(-\\varphi) + \\cos(2\\omega_0 t + \\varphi) \\right) + \\frac{A_2^2}{2} + \\frac{A_2^2}{2}\\cos(2\\omega_0 t + 2 \\varphi)\n", "\\end{align}\n", "\n", "Da es sich bei der Laserfrequenz um eine sehr hoch frequente Schwingung handelt, die bist zu $10^14$ mal pro Sekunde oszilliert (z.B. 280 THz für infrarotes Licht), agiert die Photodiode selbst als Tiefpassfilter, da sie so hohe Frequenzen nicht mehr auflösen kann. Daher können wir im Folgenden alle Cosinus-Terme der Frequenz $2\\omega_0$ vernachlässigen:\n", "\n", "\\begin{align}\n", "I &\\propto \\frac{A_1^2}{2} + A_1 A_2 \\left( \\cos(-\\varphi) \\right) + \\frac{A_2^2}{2} \\\\\n", "&= \\frac{A_1^2}{2} + \\frac{A_2^2}{2} + A_1 A_2 \\cos(-\\varphi) \n", "\\end{align}\n", "\n", "Die Intensität, bzw. Leistung auf der Photodiode hängt somit also nur noch von einem DC-Term ab, der nicht oszilliert, von der Phase $\\varphi$ die es zu messen gilt und dem Produkt der zwei Amplituden. Der DC-Term entspricht dem Gleichanteil des Cosinus-Signals und kann ebenfalls rausgefiltert werden, sodass lediglich das Cosinus-Signal mit der relevanten Phaseninformation übrig bleibt:\n", "\n", "$$I \\propto A_1 A_2 \\cos(\\varphi)$$\n", "\n", "Dies ist die *periodische* Kennlinie eines homodynen Laserinterferometers und wird im Folgenden grafisch dargestellt. Das animierte Oszilloskop-Signal zeigt an, wie sich die Ausgangsleistung in einem Interferometer je nach Spiegelposition ändern würde. " ] }, { "cell_type": "code", "execution_count": 5, "id": "abda71de-32f0-44f7-8e31-ec8f8824f1b2", "metadata": { "tags": [ "hide-input" ] }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "from IPython.display import HTML\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "# MatplotLib Settings:\n", "plt.style.use('default') # Matplotlib Style wählen\n", "plt.rcParams['font.size'] = 10; # Schriftgröße\n", "fig, ax = plt.subplots(1,2,figsize=(7,3))\n", "\n", "A_1 = 1.0\n", "A_2 = 1.0\n", "phase = np.linspace(0, 4*np.pi, num=10000)\n", "ax[0].set_ylabel('Photostrom (a.u.)')\n", "ax[0].set_xlabel('Phase (rad)')\n", "ax[0].set_xticks([0, np.pi, 2*np.pi, 3*np.pi, 4*np.pi], ['0', r'$\\pi$', r'$2\\pi$', r'$3\\pi$', r'$4\\pi$'])\n", "ax2 = ax[0].twiny()\n", "ax2.set_xticks([0, np.pi, 2*np.pi, 3*np.pi, 4*np.pi], ['0', r'', r'775', r'', r'1550'])\n", "ax2.set_xlabel('Spiegelposition (nm)')\n", "ax[0].set_ylim(-1.1,1.1)\n", "\n", "t = np.linspace(0, 1, num=10000)\n", "\n", "ax[1].set_ylabel('Photostrom (a.u.)')\n", "ax[1].set_xlabel('Zeit (s)')\n", "ax[1].set_ylim(-1.1,1.1)\n", "ax[1].set_title('Oszilloskop: ')\n", "\n", "num = 51\n", "tTM = np.linspace(0,2*np.pi , num=num)\n", "xTM = np.cos(tTM)\n", "\n", "line, = ax[0].plot(phase, A_1 * A_2 * np.sin(phase), color='tab:blue')\n", "linet, = ax[1].plot(t, A_1 * A_2 * np.sin(0*phase), color = 'tab:red')\n", "\n", "\n", "def animate(i):\n", " line.set_ydata(A_1 * A_2 * np.sin(phase + xTM[i])) # update the data.\n", " linet.set_ydata(A_1 * A_2 * np.sin(0*phase + xTM[i])) # update the data.\n", " return line, linet,\n", "\n", "\n", "ani = animation.FuncAnimation(\n", " fig, animate, interval=20, blit=True, save_count=num)\n", "\n", "\n", "\n", "# To save the animation, use e.g.\n", "#\n", "# ani.save(\"movie.mp4\")\n", "#\n", "# or\n", "#\n", "# writer = animation.FFMpegWriter(\n", "# fps=15, metadata=dict(artist='Me'), bitrate=1800)\n", "# ani.save(\"movie.mp4\", writer=writer)\n", "plt.tight_layout()\n", "plt.close()\n", "ani.save('homodyne_ifo.mp4', writer='ffmpeg', dpi=300)\n", "HTML(ani.to_jshtml())\n" ] }, { "cell_type": "markdown", "id": "b0679bb5-7972-4862-9b63-43e8c39195cb", "metadata": {}, "source": [ "## Signal eines *heterodynen* Laserinterferometers\n", "\n", "Um auch dynamische Bewegungen von Objekten zu messen, die das Ausmaß einer Wellenlänge überschreiten, werden häufig heterodyne Laserinterferometer benutzt. Hierbei müssen zwar keine Ringe mehr gezählt werden, doch die Auslesung eines solchen Interferometers ist etwas komplizierter als nur den Intensitäts- bzw. Leistungsschwankungen auf einer Photodiode zu folgen, wie es bei homodynen Laserinterferometern praktiziert wird. \n", "\n", "In einem heterodynen Laserinterferometer überlagert man zwei elektromagnetische Wellen *unterschiedlicher* Frequenz:\n", "\n", "$$E_1 = A_1 \\cos(\\omega_1 t)$$\n", "\n", "und \n", "\n", "$$E_2 = A_2 \\cos(\\omega_2 t + \\varphi)$$\n", "\n", "Wir nehmen wieder der Einfachheithalber an, dass der eine Interferometerarm stabil ist und sich lediglich die Phasenlage im zweiten Interferometerarm mit $\\varphi$ ändert. \n", "Wieder berechnen wir die Intensität auf der Photodiode mittels:\n", "\n", "\\begin{align}\n", "I &\\propto |E_1 + E_2|^2 = |A_1 \\cos(\\omega_1 t) + A_2 \\cos(\\omega_2 t + \\varphi)|^2 \\\\\n", "&= A_1^2 \\cos(\\omega_1 t)^2 + 2 A_1 A_2 \\cos(\\omega_1 t) \\cos(\\omega_2 t + \\varphi) + A_2^2 \\cos(\\omega_2 t + \\varphi)^2\n", "\\end{align}\n", "\n", "und wieder können wir die Gleichung mittels Additionstheoremen von oben vereinfachen zu:\n", "\n", "\\begin{align}\n", "I &\\propto \\frac{A_1^2}{2} + \\frac{A_1^2}{2}\\cos([\\omega_1+\\omega_2] t) \\\\ \n", "&+ A_1 A_2 \\left( \\cos([\\omega_1-\\omega_2]t-\\varphi) + \\cos([\\omega_1+\\omega_2] t + \\varphi) \\right) \\\\\n", "& + \\frac{A_2^2}{2} + \\frac{A_2^2}{2}\\cos([\\omega_1+\\omega_2] t + 2 \\varphi)\n", "\\end{align}\n", "\n", "Wieder können wir annehmen, dass hohe Frequenzanteile aufgrund der Tiefpassverhaltens der Photodiode nicht mehr aufgenommen werden und wir können die Terme mit $\\omega_1 + \\omega_2$ vernachlässigen:\n", "\n", "\\begin{align}\n", "I &\\propto \\frac{A_1^2}{2} + A_1 A_2 \\cos([\\omega_1-\\omega_2]t-\\varphi) + \\frac{A_2^2}{2} \\\\\n", "&= \\frac{A_1^2}{2} + \\frac{A_2^2}{2} + A_1 A_2 \\cos([\\omega_1-\\omega_2]t-\\varphi)\n", "\\end{align}\n", "\n", "Der DC-Offset enthält wieder keine physikalischen Informationen und kann ebenfalls gefiltert werden. Übrig bleibt ein Photostrom, der nun mit einer Frequenz oszilliert, die genau. derDifferenzfrequenz der beiden interferierten Laserstrahlen entspricht, und die **heterodyne Frequenz** oder auch **Schwebung** genannt wird:\n", "\n", "$$I \\propto A_1 A_2 \\cos(\\omega_\\mathrm{het}t-\\varphi)$$\n", "\n", "Im Folgenden stellen wir diese Kennlinie wieder grafisch dar. Hierbei ist jetzt zu beachten, dass die Schwingung nicht gegenüber der Phasenlage gezeichnet wird, sondern gegenüber der Zeit. Das physikalische Signal steckt weiterhin in der Phase und muss mit spezieller Auswerteelektronik von der Schwebungsfrequenz extrahiert werden, was wir uns gleich genauer ansehen werden.\n", "\n", "Die Differenzfrequenz (Schwebung) ist:\n", "\n", "$$\\omega_\\mathrm{het} = \\omega_1 -\\omega_2$$\n", "\n", "Praktisch gesehen kann diese zwischen den beiden Lasern verändert werden, indem die Laserfrequent eines Lasers leicht geschoben wird. Dadurch kann sie auf die Bandbreite der Photodiode und Ausleseelektronik angepasst werden. Die Phase liefert wieder die Information über die Spiegelposition. Ändert sich die Phase, wie es im rechten Diagramm dargestellt ist, wandert die Cosinus-Kurve nach links oder rechts, je nach Vorzeichen der Phasenänderung, wie es in der nachfolgenden Animation dargestellt ist:\n", "\n", "$$\\varphi = \\frac{2 \\pi}{\\lambda}$$" ] }, { "cell_type": "code", "execution_count": 6, "id": "2ecda885", "metadata": { "tags": [ "hide-input" ] }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "from IPython.display import HTML\n", "# MatplotLib Settings:\n", "plt.style.use('default') # Matplotlib Style wählen\n", "plt.rcParams['font.size'] = 10; # Schriftgröße\n", "\n", "fig, ax = plt.subplots(figsize=(5,3))\n", "\n", "A_1 = 1.0\n", "A_2 = 1.0\n", "f_het = 10e3\n", "phase = 0\n", "t = np.linspace(0, 0.5e-3, num=10000)\n", "\n", "x = np.arange(0, 2*np.pi, 0.01)\n", "line, = ax.plot(t*1e3, A_1 * A_2 * np.sin(2*np.pi*f_het * t), color='tab:red')\n", "ax.set_xlabel('Zeit (ms)')\n", "ax.set_ylabel('Photostrom (a.u.)')\n", "ax.set_title('Oszilloskop: ')\n", "num = 51\n", "tTM = np.linspace(0,2*np.pi , num=num)\n", "xTM = np.cos(tTM)\n", "\n", "def animate(i):\n", " line.set_ydata(A_1 * A_2 * np.sin(2*np.pi*f_het * t + xTM[i])) # update the data.\n", " return line,\n", "\n", "\n", "ani = animation.FuncAnimation(\n", " fig, animate, interval=40, blit=True, save_count=num)\n", "\n", "# To save the animation, use e.g.\n", "#\n", "# ani.save(\"movie.mp4\")\n", "#\n", "# or\n", "#\n", "# writer = animation.FFMpegWriter(\n", "# fps=15, metadata=dict(artist='Me'), bitrate=1800)\n", "# ani.save(\"movie.mp4\", writer=writer)\n", "plt.tight_layout()\n", "plt.close()\n", "ani.save('heterodyne_ifo.mp4', writer='ffmpeg', dpi=300)\n", "HTML(ani.to_jshtml())\n" ] }, { "cell_type": "markdown", "id": "fd00694d-ff88-428d-9e13-ccd4eeaee192", "metadata": {}, "source": [ "Um an die physikalische Information, die Positionsänderung des Objektes, zu kommen, muss also die Phase einer Schwingung ausgelesen werden. Hierfür eignen sich im Prinzip zwei Methoden, die je nach Anwedungsbereich und der zu erwartenden Dynamik des Signals, benutzt werden.\n", "\n", "### Phasenauslesung\n", "\n", "Die IQ-Demodulation, oder auch I&Q-Verfahren (In-Phase-&-Quadrature-Verfahren), bietet die Möglichkeit die Phaseninformation $\\varphi(t) = \\varphi$ aus einem hochfrequenten Trägersignal zu erhalten, z.B. von\n", "\n", "$$A(t) = A_0 \\cos(\\omega_\\mathrm{het}t-\\varphi)$$\n", "\n", "Dies kann sowohl die Phasenlage eines Interferometers auslesen, wir aber auch in der Radartechnik benutzt.\n", "\n", "Das gemessene Signal $A(t)$ wird analog oder digital in zwei Wege aufgeteilt und in jedem demoduliert indem es mit einer Oszillatorfrequenz, die der Signalfrequenz $\\omega_\\mathrm{het}$ entspricht, gemischt wird:\n", "\n", "* Im ersten Weg wird die Demodulation mit der originalen Phasenlage (*in-phase*) durchgeführt und man erhält die **I-Quadratur**:\n", "\n", "\\begin{align}\n", "I &= A(t) \\cdot \\cos(\\omega_\\mathrm{het}t) \\\\\n", "&= A_0 \\cos(\\omega_\\mathrm{het}t-\\varphi) \\cdot \\cos(\\omega_\\mathrm{het}t) \\\\\n", "&= \\frac{A_0}{2} \\cdot \\left( \\cos(-\\varphi) + \\cos(2\\omega_\\mathrm{het}t-\\varphi) \\right) \\\\\n", "&= \\frac{A_0}{2} \\cdot \\cos(\\varphi)\n", "\\end{align}\n", "\n", "* Der zweite Weg wird mit Referenzfrequenz demoduliert, die um 90° gegenüber dem ersten Weg verschoben ist, was die **Q-Quadratur** ergibt:\n", "\n", "\\begin{align}\n", "Q &= A(t) \\cdot \\sin(\\omega_\\mathrm{het}t) \\\\\n", "&= A_0 \\cos(\\omega_\\mathrm{het}t-\\varphi) \\cdot \\sin(\\omega_\\mathrm{het}t) \\\\\n", "&= \\frac{A_0}{2} \\cdot \\left( \\sin(\\varphi) + \\sin(2\\omega_\\mathrm{het}t-\\varphi) \\right) \\\\\n", "&= \\frac{A_0}{2} \\cdot \\sin(\\varphi)\n", "\\end{align}" ] }, { "cell_type": "code", "execution_count": 4, "id": "016db575-992c-48f9-8a81-e42ef2b6a4dc", "metadata": { "tags": [ "hide-input" ] }, "outputs": [ { "ename": "KeyboardInterrupt", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[4], line 62\u001b[0m\n\u001b[1;32m 60\u001b[0m plt\u001b[38;5;241m.\u001b[39mtight_layout()\n\u001b[1;32m 61\u001b[0m plt\u001b[38;5;241m.\u001b[39mclose()\n\u001b[0;32m---> 62\u001b[0m HTML(\u001b[43mani\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mto_jshtml\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m)\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/animation.py:1353\u001b[0m, in \u001b[0;36mAnimation.to_jshtml\u001b[0;34m(self, fps, embed_frames, default_mode)\u001b[0m\n\u001b[1;32m 1349\u001b[0m path \u001b[38;5;241m=\u001b[39m Path(tmpdir, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtemp.html\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 1350\u001b[0m writer \u001b[38;5;241m=\u001b[39m HTMLWriter(fps\u001b[38;5;241m=\u001b[39mfps,\n\u001b[1;32m 1351\u001b[0m embed_frames\u001b[38;5;241m=\u001b[39membed_frames,\n\u001b[1;32m 1352\u001b[0m default_mode\u001b[38;5;241m=\u001b[39mdefault_mode)\n\u001b[0;32m-> 1353\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwriter\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mwriter\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1354\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_html_representation \u001b[38;5;241m=\u001b[39m path\u001b[38;5;241m.\u001b[39mread_text()\n\u001b[1;32m 1356\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_html_representation\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/animation.py:1105\u001b[0m, in \u001b[0;36mAnimation.save\u001b[0;34m(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)\u001b[0m\n\u001b[1;32m 1102\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m data \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;241m*\u001b[39m[a\u001b[38;5;241m.\u001b[39mnew_saved_frame_seq() \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m all_anim]):\n\u001b[1;32m 1103\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m anim, d \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(all_anim, data):\n\u001b[1;32m 1104\u001b[0m \u001b[38;5;66;03m# TODO: See if turning off blit is really necessary\u001b[39;00m\n\u001b[0;32m-> 1105\u001b[0m \u001b[43manim\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_draw_next_frame\u001b[49m\u001b[43m(\u001b[49m\u001b[43md\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mblit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[1;32m 1106\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m progress_callback \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 1107\u001b[0m progress_callback(frame_number, total_frames)\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/animation.py:1141\u001b[0m, in \u001b[0;36mAnimation._draw_next_frame\u001b[0;34m(self, framedata, blit)\u001b[0m\n\u001b[1;32m 1139\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_pre_draw(framedata, blit)\n\u001b[1;32m 1140\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_draw_frame(framedata)\n\u001b[0;32m-> 1141\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_post_draw\u001b[49m\u001b[43m(\u001b[49m\u001b[43mframedata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mblit\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/animation.py:1166\u001b[0m, in \u001b[0;36mAnimation._post_draw\u001b[0;34m(self, framedata, blit)\u001b[0m\n\u001b[1;32m 1164\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_blit_draw(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_drawn_artists)\n\u001b[1;32m 1165\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1166\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_fig\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcanvas\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdraw_idle\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/backend_bases.py:1893\u001b[0m, in \u001b[0;36mFigureCanvasBase.draw_idle\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1891\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_idle_drawing:\n\u001b[1;32m 1892\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_idle_draw_cntx():\n\u001b[0;32m-> 1893\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/backends/backend_agg.py:388\u001b[0m, in \u001b[0;36mFigureCanvasAgg.draw\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 385\u001b[0m \u001b[38;5;66;03m# Acquire a lock on the shared font cache.\u001b[39;00m\n\u001b[1;32m 386\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtoolbar\u001b[38;5;241m.\u001b[39m_wait_cursor_for_draw_cm() \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mtoolbar\n\u001b[1;32m 387\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m nullcontext()):\n\u001b[0;32m--> 388\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfigure\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 389\u001b[0m \u001b[38;5;66;03m# A GUI class may be need to update a window using this draw, so\u001b[39;00m\n\u001b[1;32m 390\u001b[0m \u001b[38;5;66;03m# don't forget to call the superclass.\u001b[39;00m\n\u001b[1;32m 391\u001b[0m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39mdraw()\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/artist.py:95\u001b[0m, in \u001b[0;36m_finalize_rasterization..draw_wrapper\u001b[0;34m(artist, renderer, *args, **kwargs)\u001b[0m\n\u001b[1;32m 93\u001b[0m \u001b[38;5;129m@wraps\u001b[39m(draw)\n\u001b[1;32m 94\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdraw_wrapper\u001b[39m(artist, renderer, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[0;32m---> 95\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43martist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 96\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m renderer\u001b[38;5;241m.\u001b[39m_rasterizing:\n\u001b[1;32m 97\u001b[0m renderer\u001b[38;5;241m.\u001b[39mstop_rasterizing()\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/artist.py:72\u001b[0m, in \u001b[0;36mallow_rasterization..draw_wrapper\u001b[0;34m(artist, renderer)\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 70\u001b[0m renderer\u001b[38;5;241m.\u001b[39mstart_filter()\n\u001b[0;32m---> 72\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43martist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 74\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/figure.py:3154\u001b[0m, in \u001b[0;36mFigure.draw\u001b[0;34m(self, renderer)\u001b[0m\n\u001b[1;32m 3151\u001b[0m \u001b[38;5;66;03m# ValueError can occur when resizing a window.\u001b[39;00m\n\u001b[1;32m 3153\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpatch\u001b[38;5;241m.\u001b[39mdraw(renderer)\n\u001b[0;32m-> 3154\u001b[0m \u001b[43mmimage\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_draw_list_compositing_images\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3155\u001b[0m \u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43martists\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msuppressComposite\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3157\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m sfig \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39msubfigs:\n\u001b[1;32m 3158\u001b[0m sfig\u001b[38;5;241m.\u001b[39mdraw(renderer)\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/image.py:132\u001b[0m, in \u001b[0;36m_draw_list_compositing_images\u001b[0;34m(renderer, parent, artists, suppress_composite)\u001b[0m\n\u001b[1;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m not_composite \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m has_images:\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m artists:\n\u001b[0;32m--> 132\u001b[0m \u001b[43ma\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 133\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 134\u001b[0m \u001b[38;5;66;03m# Composite any adjacent images together\u001b[39;00m\n\u001b[1;32m 135\u001b[0m image_group \u001b[38;5;241m=\u001b[39m []\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/artist.py:72\u001b[0m, in \u001b[0;36mallow_rasterization..draw_wrapper\u001b[0;34m(artist, renderer)\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 70\u001b[0m renderer\u001b[38;5;241m.\u001b[39mstart_filter()\n\u001b[0;32m---> 72\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43martist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 74\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/axes/_base.py:3070\u001b[0m, in \u001b[0;36m_AxesBase.draw\u001b[0;34m(self, renderer)\u001b[0m\n\u001b[1;32m 3067\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artists_rasterized:\n\u001b[1;32m 3068\u001b[0m _draw_rasterized(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfigure, artists_rasterized, renderer)\n\u001b[0;32m-> 3070\u001b[0m \u001b[43mmimage\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_draw_list_compositing_images\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3071\u001b[0m \u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43martists\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfigure\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msuppressComposite\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3073\u001b[0m renderer\u001b[38;5;241m.\u001b[39mclose_group(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124maxes\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 3074\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstale \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mFalse\u001b[39;00m\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/image.py:132\u001b[0m, in \u001b[0;36m_draw_list_compositing_images\u001b[0;34m(renderer, parent, artists, suppress_composite)\u001b[0m\n\u001b[1;32m 130\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m not_composite \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m has_images:\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m a \u001b[38;5;129;01min\u001b[39;00m artists:\n\u001b[0;32m--> 132\u001b[0m \u001b[43ma\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 133\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 134\u001b[0m \u001b[38;5;66;03m# Composite any adjacent images together\u001b[39;00m\n\u001b[1;32m 135\u001b[0m image_group \u001b[38;5;241m=\u001b[39m []\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/artist.py:72\u001b[0m, in \u001b[0;36mallow_rasterization..draw_wrapper\u001b[0;34m(artist, renderer)\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[1;32m 70\u001b[0m renderer\u001b[38;5;241m.\u001b[39mstart_filter()\n\u001b[0;32m---> 72\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mdraw\u001b[49m\u001b[43m(\u001b[49m\u001b[43martist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 74\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m artist\u001b[38;5;241m.\u001b[39mget_agg_filter() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/axis.py:1394\u001b[0m, in \u001b[0;36mAxis.draw\u001b[0;34m(self, renderer, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1391\u001b[0m tick\u001b[38;5;241m.\u001b[39mdraw(renderer)\n\u001b[1;32m 1393\u001b[0m \u001b[38;5;66;03m# Shift label away from axes to avoid overlapping ticklabels.\u001b[39;00m\n\u001b[0;32m-> 1394\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_update_label_position\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrenderer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1395\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlabel\u001b[38;5;241m.\u001b[39mdraw(renderer)\n\u001b[1;32m 1397\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_update_offset_text_position(tlb1, tlb2)\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/axis.py:2378\u001b[0m, in \u001b[0;36mXAxis._update_label_position\u001b[0;34m(self, renderer)\u001b[0m\n\u001b[1;32m 2375\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n\u001b[1;32m 2376\u001b[0m \u001b[38;5;66;03m# use Axes if spine doesn't exist\u001b[39;00m\n\u001b[1;32m 2377\u001b[0m spinebbox \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39maxes\u001b[38;5;241m.\u001b[39mbbox\n\u001b[0;32m-> 2378\u001b[0m bbox \u001b[38;5;241m=\u001b[39m \u001b[43mmtransforms\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mBbox\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43munion\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbboxes\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mspinebbox\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2379\u001b[0m bottom \u001b[38;5;241m=\u001b[39m bbox\u001b[38;5;241m.\u001b[39my0\n\u001b[1;32m 2381\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlabel\u001b[38;5;241m.\u001b[39mset_position(\n\u001b[1;32m 2382\u001b[0m (x, bottom \u001b[38;5;241m-\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlabelpad \u001b[38;5;241m*\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mfigure\u001b[38;5;241m.\u001b[39mdpi \u001b[38;5;241m/\u001b[39m \u001b[38;5;241m72\u001b[39m)\n\u001b[1;32m 2383\u001b[0m )\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/transforms.py:657\u001b[0m, in \u001b[0;36mBboxBase.union\u001b[0;34m(bboxes)\u001b[0m\n\u001b[1;32m 655\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mbboxes\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m cannot be empty\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 656\u001b[0m x0 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmin([bbox\u001b[38;5;241m.\u001b[39mxmin \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[0;32m--> 657\u001b[0m x1 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmax([bbox\u001b[38;5;241m.\u001b[39mxmax \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[1;32m 658\u001b[0m y0 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmin([bbox\u001b[38;5;241m.\u001b[39mymin \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[1;32m 659\u001b[0m y1 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmax([bbox\u001b[38;5;241m.\u001b[39mymax \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/transforms.py:657\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 655\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mbboxes\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m cannot be empty\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 656\u001b[0m x0 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmin([bbox\u001b[38;5;241m.\u001b[39mxmin \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[0;32m--> 657\u001b[0m x1 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmax([\u001b[43mbbox\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mxmax\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[1;32m 658\u001b[0m y0 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmin([bbox\u001b[38;5;241m.\u001b[39mymin \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n\u001b[1;32m 659\u001b[0m y1 \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mmax([bbox\u001b[38;5;241m.\u001b[39mymax \u001b[38;5;28;01mfor\u001b[39;00m bbox \u001b[38;5;129;01min\u001b[39;00m bboxes])\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/matplotlib/transforms.py:315\u001b[0m, in \u001b[0;36mBboxBase.xmax\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 312\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[1;32m 313\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mxmax\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 314\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"The right edge of the bounding box.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 315\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmax\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_points\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m[\u001b[49m\u001b[43m:\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m<__array_function__ internals>:5\u001b[0m, in \u001b[0;36mamax\u001b[0;34m(*args, **kwargs)\u001b[0m\n", "File \u001b[0;32m~/opt/anaconda3/lib/python3.9/site-packages/numpy/core/fromnumeric.py:2754\u001b[0m, in \u001b[0;36mamax\u001b[0;34m(a, axis, out, keepdims, initial, where)\u001b[0m\n\u001b[1;32m 2638\u001b[0m \u001b[38;5;129m@array_function_dispatch\u001b[39m(_amax_dispatcher)\n\u001b[1;32m 2639\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mamax\u001b[39m(a, axis\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, out\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, keepdims\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39m_NoValue, initial\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39m_NoValue,\n\u001b[1;32m 2640\u001b[0m where\u001b[38;5;241m=\u001b[39mnp\u001b[38;5;241m.\u001b[39m_NoValue):\n\u001b[1;32m 2641\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 2642\u001b[0m \u001b[38;5;124;03m Return the maximum of an array or maximum along an axis.\u001b[39;00m\n\u001b[1;32m 2643\u001b[0m \n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 2752\u001b[0m \u001b[38;5;124;03m 5\u001b[39;00m\n\u001b[1;32m 2753\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m-> 2754\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _wrapreduction(a, \u001b[43mnp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmaximum\u001b[49m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmax\u001b[39m\u001b[38;5;124m'\u001b[39m, axis, \u001b[38;5;28;01mNone\u001b[39;00m, out,\n\u001b[1;32m 2755\u001b[0m keepdims\u001b[38;5;241m=\u001b[39mkeepdims, initial\u001b[38;5;241m=\u001b[39minitial, where\u001b[38;5;241m=\u001b[39mwhere)\n", "\u001b[0;31mKeyboardInterrupt\u001b[0m: " ] } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "from matplotlib.patches import ConnectionPatch\n", "\n", "fig, (axl, axr) = plt.subplots(\n", " ncols=2,\n", " sharey=True,\n", " figsize=(6, 2),\n", " gridspec_kw=dict(width_ratios=[1, 3], wspace=0),\n", ")\n", "axl.set_aspect(1)\n", "axr.set_box_aspect(1 / 3)\n", "axr.yaxis.set_visible(False)\n", "axr.xaxis.set_ticks([0, np.pi, 2 * np.pi], [\"0\", r\"$\\pi$\", r\"$2\\pi$\"])\n", "axl.set_xlabel('I-Quadratur')\n", "axl.set_ylabel('Q-Quadratur')\n", "axl.xaxis.set_ticks([])\n", "axl.yaxis.set_ticks([])\n", "# draw circle with initial point in left Axes\n", "x = np.linspace(0, 2 * np.pi, 50)\n", "axl.plot(np.cos(x), np.sin(x), \"k\", lw=0.3)\n", "point, = axl.plot(0, 0, \"o\")\n", "\n", "# draw full curve to set view limits in right Axes\n", "sine, = axr.plot(x, np.sin(x))\n", "\n", "# draw connecting line between both graphs\n", "con = ConnectionPatch(\n", " (1, 0),\n", " (0, 0),\n", " \"data\",\n", " \"data\",\n", " axesA=axl,\n", " axesB=axr,\n", " color=\"C0\",\n", " ls=\"dotted\",\n", ")\n", "fig.add_artist(con)\n", "\n", "\n", "def animate(i):\n", " pos = np.cos(i), np.sin(i)\n", " point.set_data(*pos)\n", " x = np.linspace(0, i, int(i * 25 / np.pi))\n", " sine.set_data(x, np.sin(x))\n", " con.xy1 = pos\n", " con.xy2 = i, pos[1]\n", " return point, sine, con\n", "\n", "\n", "ani = animation.FuncAnimation(\n", " fig,\n", " animate,\n", " interval=50,\n", " blit=False, # blitting can't be used with Figure artists\n", " frames=x,\n", " repeat_delay=100,\n", ")\n", "plt.tight_layout()\n", "plt.close()\n", "HTML(ani.to_jshtml())" ] }, { "cell_type": "markdown", "id": "e6cf3fa4-ace2-41cc-af38-035777277015", "metadata": {}, "source": [ "Die Phaseninformation erhält man über die Rückrechnung der Quadraturen:\n", "\n", "$$\\varphi = \\arctan\\left(\\frac{Q}{I}\\right) = \\arctan\\left(\\frac{A_0/2 \\sin(\\varphi)}{A_0/2 \\cos(\\varphi)}\\right) = \\arctan\\left(\\frac{\\sin(\\varphi)}{\\cos(\\varphi)}\\right) = \\arctan(\\tan(\\varphi)) = \\varphi$$\n", "\n", "Die Amplitude des Signal kann ebenfalls aus den Quadraturen berechnet werden:\n", "\n", "$$A = \\sqrt{I^2 + Q^2} = \\sqrt{\\frac{A_0^2}{4} \\cos^2(\\varphi) + \\frac{A_0^2}{4} \\sin^2(\\varphi)} = \\sqrt{\\frac{A_0^2}{4} (\\cos^2(\\varphi) + \\sin^2(\\varphi))} = \\frac{A_0}{2}$$" ] }, { "cell_type": "markdown", "id": "da62ae3e-32fc-405f-b719-c0cb708475d1", "metadata": {}, "source": [ "### Phasenregelschleife\n", "\n", "Die zweite, etwas robustere Methode für höhere Dynamiken, ist die **Phasenregelschleife**. Hierbei wird in einem Regelkreis die Phase des Referenz-Oszillators dem einkommenden Signal entsprechend nachgesteuert. Dadurch kann auch Frequenzänderungen oder großen Phasenänderungen gefolgt werden, da die Oszillatorfrequenz (bzw. -phase) intern nachgesteuer wird. \n", "Im Prinzip werden hierfür vier Schritte benötigt:\n", "\n", "1. Die Phasenlage zwischen Messsignal und Referenz-Signal (Oszillator) wird mittels einem **Phasenvergleicher** bestimmt (z.B. über IQ-Demodulation). \n", "2. Ein **Aktor/Regler**, der ein Steuersignal in Abhängigkeit von der zuvor gemessenen Phasenlage ausgibt. Diese Komponente nennt sich auch Schleifenfilter und weist das Verhalten eines Tiefpassfilters mit relativ hoher Eckfrequenz auf. Häufig handelt es sich hierbei um einen PI (Proportional-Integral) Regler \n", "3. Ein **regelbarer Oszillator**, dem das Steuersignal zugeführt wird und dadurch dem einkommenden Signal hinterher läuft.\n", "4. Ein **Frequenzteiler**, der die Ausgangsphase/-frequenz an den Phasenvergleicher unter Punkt 1.) zurück gibt und die Regelschleife gilt. Der andere Ausgangs des Frequenzteilers ist liefert das gewünschte Messsignal. \n", "\n", "Eine Phasenregelschleife (kurz PLL = phase-locked loop) kann sowohl mittels analogen als auch digitalen Komponenten implementiert werden. Die digitale Variante nennt sich dann DPLL. " ] }, { "cell_type": "markdown", "id": "de1c5190-aa57-4066-abe3-62776dcdaa2f", "metadata": {}, "source": [ "## Optischen Resonatoren\n", "\n", "Die Empfindlichkeit von Interferometern kann durch den Einsatz von *optischen Resonatoren* stark verbessert werden. Mittels zusätzlichen Spiegeln werden die Anzahl der Reflektionen für die Längenmessung zusätzlich erhöhen. \n", "Die optischen Resonatoren \"speichern\" das Licht, indem sie es zwischen zwei Spiegeln hin und her reflektieren, wodurch die effektive Weglänge des Lichts verlängert wird. Dies erhöht die Empfindlichkeit des Interferometers für Messungen der Längenänderung.\n", "\n", ":::{figure-md} ifo_oR\n", "\"ifo_oR\"\n", "\n", "Laserinterferometer mit optischen Resonator.\n", ":::\n", "\n", "Der Einfluss der Phasenänderung auf die Intensität kann durch die Finesse des Resonators und die Eigenschaften des verwendeten Lichts beeinflusst werden.\n", "Die Grundgleichung für die Intensität der Lichtwelle in einem Fabry-Perot-Resonator (eine Art von optischem Resonator) in Abhängigkeit von der Phasenänderung ist:\n", "\n", "$$\n", "I(\\Delta\\phi) = I_0 \\cdot \\frac{1}{1 + F \\cdot \\sin^2(\\Delta\\phi/2)}\n", "$$\n", "\n", "wo:\n", "- $I(\\phi)$ ist die Intensität des Lichts im Resonator,\n", "- $I_0$ ist die Intensität des einfallenden Lichts,\n", "- $\\mathcal F$ ist die Finesse des Resonators,\n", "- $\\Delta\\phi$ ist die Phasenänderung.\n", "\n", "In dieser Gleichung ist die Finesse ($\\mathcal F$) ein Maß für die \"Schärfe\" der Resonanz des Resonators und wird oft durch die Formel\n", "\n", "$$\n", "\\mathcal F = \\frac{\\pi \\sqrt{R}}{1 - R}\n", "$$\n", "\n", "ausgedrückt, wobei $R$ die Reflektivität der Spiegel ist.\n", "\n", "Die Gleichung zeigt, dass die Intensität im Resonator maximiert wird, wenn die Phasenänderung ein Vielfaches von $2\\pi$ ist, was bedeutet, dass die Bedingungen für *konstruktive* Interferenz erfüllt sind. Umgekehrt wird die Intensität minimiert, wenn die Phasenänderung ein ungerades Vielfaches von $\\pi$ ist, was die Bedingungen für *destruktive* Interferenz erfüllt.\n", "\n", "Die Breite der Resonanzspitzen und die Tiefe der Resonanztäler werden durch die *Finesse* des Resonators beeinflusst. Ein Resonator mit hoher Finesse wird schmalere Resonanzspitzen und tiefere Resonanztäler aufweisen, d.h. die Kennlinie wird steiler, was einer höheren Empfindlichkeit entspricht.\n", "\n", "Eine kleine Bewegung der Spiegel kann nun eine drastische Änderung in der Intensität verursachen, was die Messung von sehr kleinen Signalen, wie den von Gravitationswellen verursachten, ermöglicht. Im nachfolgenden Diagramm ist dargestellt, wie sich die Kennlinie in der Anwesenheit von optischen Resonatoren in Abhängigkeit von der Spiegelreflektivität $R$ in einem Interferometer ändert. " ] }, { "cell_type": "code", "execution_count": null, "id": "e79bcf6e-0166-4113-aad5-e3efc5f43692", "metadata": { "tags": [ "hide-input" ] }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", "
\n", "
\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "from IPython.display import HTML\n", "plt.rcParams['font.size'] = 10; # Schriftgröße\n", "\n", "\n", "# Initialize the figure and axis\n", "fig, ax = plt.subplots(figsize=(5, 3), tight_layout=True)\n", "\n", "\n", "# Funktion zur Berechnung der Intensität in Abhängigkeit von der Phasenverschiebung\n", "def calculate_intensity(reflectivity, phase_difference):\n", " finesse = np.pi * reflectivity / (1 - reflectivity)\n", " intensity = 1 / (1 + finesse * np.sin(phase_difference / 2.0)**2)\n", " return intensity\n", "\n", "# Funktion zum Aktualisieren der Grafik\n", "def update(reflectivity):\n", " x = np.linspace(0, 6 * np.pi, 1000) # Phasenverschiebung von 0 bis 6*pi\n", " phase_difference = x\n", " intensity = calculate_intensity(reflectivity, phase_difference)\n", " \n", " ax.plot(phase_difference, calculate_intensity(0.9, phase_difference), color='tab:blue', lw=4, alpha=0.5, label='Resonator Reflektivität: R = 0.9')\n", " ax.plot(phase_difference, intensity, color='tab:red', label='Resonator Reflektivität: R =%5.2f'%(reflectivity))\n", " # Set plot labels and legend\n", " #ax.set_title('Analog zeitdiskret: Abgetastete Sinuswelle')\n", " ax.set_xlabel('Phasenverschiebung (rad)')\n", " ax.set_ylabel('Intensität')\n", " ax.set_ylim([0, 1])\n", " ax.grid(True)\n", " ax.legend(loc='upper right')\n", "\n", "\n", "# Funktion zur Aktualisierung der Animation\n", "def animate(reflectivity):\n", " ax.clear()\n", " update(reflectivity)\n", "\n", "# Create an animation by varying reflectivity from 0.01 to 0.99 with a step of 0.01\n", "ani = FuncAnimation(fig, animate, frames=np.arange(0.01, 0.999, 0.01), repeat=False)\n", "\n", "# Anzeigen der Animation\n", "#plt.tight_layout()\n", "plt.close()\n", "HTML(ani.to_jshtml())" ] }, { "cell_type": "markdown", "id": "b1d241d7-7954-4746-be6b-39a7df0228aa", "metadata": {}, "source": [ "Die Empfindlichkeit kann durch optische Resonatoren extrem gesteigert werden, jedoch wird der Messbereich dadurch verkleinert und das System selber darf sich kaum noch bewegen. " ] } ], "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.9.18" } }, "nbformat": 4, "nbformat_minor": 5 }