diff --git a/Lab-1/.ipynb_checkpoints/ejemyr_lab1-checkpoint.ipynb b/Lab-1/.ipynb_checkpoints/ejemyr_lab1-checkpoint.ipynb new file mode 100644 index 0000000..ccb802d --- /dev/null +++ b/Lab-1/.ipynb_checkpoints/ejemyr_lab1-checkpoint.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 1: Matrix algorithms**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**\n", + "\n", + "In this report we implement the most fundamental vector and matrix algorithms. They are compared to the solutions of the highly regarded package numpy. All algorithms passed down to the seventh decimal." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "# **About the code**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "ce1a945e-2dae-4530-cb4d-1236274284c0" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "# from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "class Tests(unittest.TestCase):\n", + " @staticmethod\n", + " def check_accuracy(est, true, decimal = 7):\n", + " np.testing.assert_almost_equal(est, true, decimal=decimal)\n", + "\n", + " @staticmethod\n", + " def check_accuracy_multiple_random(num_of_tests, generating_func, decimal = 7):\n", + " for i in range(num_of_tests):\n", + " est, true = generating_func()\n", + " Tests.check_accuracy(est, true, decimal)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will define multiplication of the most fundamental parts of linear algebra; vectors and matrices. They will ble implemented acording to the algorithms described in the course literature." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "uwVrMhGQ4viZ" + }, + "source": [ + "### Scalar product" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jqrNhl6Np2Ze" + }, + "outputs": [], + "source": [ + "def scalar_product(x, y):\n", + " if type(x) != np.ndarray:\n", + " try:\n", + " x = np.array(x)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " if type(y) != np.ndarray:\n", + " try:\n", + " y = np.array(y)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + "\n", + " if x.ndim != 1 or y.ndim != 1:\n", + " raise Exception(\"Vector dimentions error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " if x.size != y.size:\n", + " raise Exception(\"Vector formats don't agree.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " \n", + " sum = 0\n", + " for i in range(len(x)):\n", + " sum += x[i] * y[i]\n", + "\n", + " return sum" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CDxXDYtrrQ-a" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_scalar_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([1,2], [1,2,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [[0,0],[0,0]])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([\"s\",2], [1,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product(\"Hej\", [1,2,3])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " n = np.random.randint(min_length, max_length)\n", + " a = np.random.rand(n)\n", + " b = np.random.rand(n)\n", + " return scalar_product(a, b), a.dot(b)\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "FNWnhrnD4jmh" + }, + "source": [ + "### Matrix-vector product" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "j7rPWYYP4iUV" + }, + "outputs": [], + "source": [ + "def matrix_vector_product(M, x):\n", + " if type(M) != np.ndarray:\n", + " try:\n", + " M = np.array(M)\n", + " except:\n", + " raise Exception(\"Matrix format error.\\n\" + \"M: \" + str(M))\n", + " if type(x) != np.ndarray:\n", + " try:\n", + " x = np.array(x)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x))\n", + "\n", + " if x.ndim != 1 or M.ndim != 2:\n", + " raise Exception(\"Matrix or vector dimentions error.\\n\" + \"M: \" + str(M) + \"\\nx: \" + str(x))\n", + " if M.shape[1] != x.size:\n", + " raise Exception(\"Matrix and vector formats don't agree.\\n\" + \"M: \" + str(M) + \"\\nx: \" + str(x))\n", + "\n", + " b = np.zeros(M.shape[0])\n", + " for i in range(M.shape[0]):\n", + " for j in range(M.shape[1]):\n", + " b[i] += M[i, j] * x[j]\n", + "\n", + " return b" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "53zAuKI06Ona" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_matrix_vector_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [1,2,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [\"Hej\", \"Hå\"])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " n = np.random.randint(min_length, max_length)\n", + " m = np.random.randint(min_length, max_length)\n", + " x = np.random.rand(n)\n", + " M = np.random.rand(m, n)\n", + " return matrix_vector_product(M, x), M.dot(x)\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mEWh21BX8mzG" + }, + "source": [ + "### Matrix-Matrix product" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "2UAZ0QxV8mLb" + }, + "outputs": [], + "source": [ + "def matrix_matrix_product(A, B):\n", + " def format_test(M, name):\n", + " if type(M) != np.ndarray:\n", + " try:\n", + " M = np.array(M)\n", + " except:\n", + " raise Exception(\"Matrix format error.\\n\" + name + \": \" + str(M))\n", + "\n", + " if M.ndim != 2:\n", + " raise Exception(\"Matrix dimentions error.\\n\" + name + \": \" + str(M))\n", + " format_test(A, \"A\")\n", + " format_test(B, \"B\")\n", + " \n", + " if A.shape[1] != B.shape[0]:\n", + " raise Exception(\"Matrix formats don't agree.\\n\" + \"A: \" + str(A) + \"\\nB: \" + str(B))\n", + " \n", + " \n", + " C = np.zeros((A.shape[0], B.shape[1]))\n", + " for i in range(A.shape[0]):\n", + " for j in range(B.shape[1]):\n", + " for k in range(A.shape[1]):\n", + " C[i, j] += A[i, k] * B[k, j]\n", + "\n", + " return C" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "bI2zuBOP9lIx" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_matrix_matrix_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0,0],[0,0,0]], [[0,0],[0,0]])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [[\"Hej\"], [\"Hå\"]])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " i = np.random.randint(min_length, max_length)\n", + " j = np.random.randint(min_length, max_length)\n", + " k = np.random.randint(min_length, max_length)\n", + " A = np.random.rand(i, j)\n", + " B = np.random.rand(j, k)\n", + " return matrix_matrix_product(A, B), A.dot(B)\n", + "\n", + " Tests.check_accuracy_multiple_random(100, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To test the methods and to receive a result we test both the error handling and the accuracy of the methods. Sice we don't have any known solution to the expresions tested we compare with a solution known to be exact enough. It is difficult to know where the limit of an \"accurate\" result lie. In this lab i've chosen to check that all result lie within a 7 decimal margin.\n", + "\n", + "The test first checks that exceptions are raised when entering incompatible inputs, and then somwhere between 100 and 1000 random vectors or matricies are tested for accuracy according to the method described above." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "G1hVxfti4Ib-", + "outputId": "159525fa-e6f3-4acb-c401-f8b9d8ab9928" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "...\n", + "----------------------------------------------------------------------\n", + "Ran 3 tests in 20.209s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "suite = unittest.TestSuite()\n", + "suite.addTest(Tests('test_scalar_product'))\n", + "suite.addTest(Tests('test_matrix_vector_product'))\n", + "suite.addTest(Tests('test_matrix_matrix_product'))\n", + "\n", + "if __name__ == '__main__':\n", + " runner = unittest.TextTestRunner()\n", + " runner.run(suite)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All the test passes, meaning that the potential floating-point-errors is within an acceptable level." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Rather non-supricing all methods passed the tests. Since all implemented algorithms have a unique and mathematically explicit way of calculationg the result there ain't much room for error." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "ejemyr_lab1.ipynb", + "provenance": [], + "toc_visible": true + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-1/ejemyr_lab1.ipynb b/Lab-1/ejemyr_lab1.ipynb new file mode 100644 index 0000000..88bc363 --- /dev/null +++ b/Lab-1/ejemyr_lab1.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 1: Matrix algorithms**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**\n", + "\n", + "In this report we implement the most fundamental vector and matrix algorithms. They are compared to the solutions of the highly regarded package numpy. All algorithms passed down to the seventh decimal." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "# **About the code**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "ce1a945e-2dae-4530-cb4d-1236274284c0" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "# from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "class Tests(unittest.TestCase):\n", + " @staticmethod\n", + " def check_accuracy(est, true, decimal = 7):\n", + " np.testing.assert_almost_equal(est, true, decimal=decimal)\n", + "\n", + " @staticmethod\n", + " def check_accuracy_multiple_random(num_of_tests, generating_func, decimal = 7):\n", + " for i in range(num_of_tests):\n", + " est, true = generating_func()\n", + " Tests.check_accuracy(est, true, decimal)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will define multiplication of the most fundamental parts of linear algebra; vectors and matrices. They will ble implemented acording to the algorithms described in the course's literature." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "uwVrMhGQ4viZ" + }, + "source": [ + "### Scalar product" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jqrNhl6Np2Ze" + }, + "outputs": [], + "source": [ + "def scalar_product(x, y):\n", + " if type(x) != np.ndarray:\n", + " try:\n", + " x = np.array(x)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " if type(y) != np.ndarray:\n", + " try:\n", + " y = np.array(y)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + "\n", + " if x.ndim != 1 or y.ndim != 1:\n", + " raise Exception(\"Vector dimentions error.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " if x.size != y.size:\n", + " raise Exception(\"Vector formats don't agree.\\n\" + \"x: \" + str(x) + \"\\ny: \" + str(y))\n", + " \n", + " sum = 0\n", + " for i in range(len(x)):\n", + " sum += x[i] * y[i]\n", + "\n", + " return sum" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CDxXDYtrrQ-a" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_scalar_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([1,2], [1,2,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [[0,0],[0,0]])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([\"s\",2], [1,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product(\"Hej\", [1,2,3])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " n = np.random.randint(min_length, max_length)\n", + " a = np.random.rand(n)\n", + " b = np.random.rand(n)\n", + " return scalar_product(a, b), a.dot(b)\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "FNWnhrnD4jmh" + }, + "source": [ + "### Matrix-vector product" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "j7rPWYYP4iUV" + }, + "outputs": [], + "source": [ + "def matrix_vector_product(M, x):\n", + " if type(M) != np.ndarray:\n", + " try:\n", + " M = np.array(M)\n", + " except:\n", + " raise Exception(\"Matrix format error.\\n\" + \"M: \" + str(M))\n", + " if type(x) != np.ndarray:\n", + " try:\n", + " x = np.array(x)\n", + " except:\n", + " raise Exception(\"Vector format error.\\n\" + \"x: \" + str(x))\n", + "\n", + " if x.ndim != 1 or M.ndim != 2:\n", + " raise Exception(\"Matrix or vector dimentions error.\\n\" + \"M: \" + str(M) + \"\\nx: \" + str(x))\n", + " if M.shape[1] != x.size:\n", + " raise Exception(\"Matrix and vector formats don't agree.\\n\" + \"M: \" + str(M) + \"\\nx: \" + str(x))\n", + "\n", + " b = np.zeros(M.shape[0])\n", + " for i in range(M.shape[0]):\n", + " for j in range(M.shape[1]):\n", + " b[i] += M[i, j] * x[j]\n", + "\n", + " return b" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "53zAuKI06Ona" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_matrix_vector_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [1,2,3])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [\"Hej\", \"Hå\"])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " n = np.random.randint(min_length, max_length)\n", + " m = np.random.randint(min_length, max_length)\n", + " x = np.random.rand(n)\n", + " M = np.random.rand(m, n)\n", + " return matrix_vector_product(M, x), M.dot(x)\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mEWh21BX8mzG" + }, + "source": [ + "### Matrix-Matrix product" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "2UAZ0QxV8mLb" + }, + "outputs": [], + "source": [ + "def matrix_matrix_product(A, B):\n", + " def format_test(M, name):\n", + " if type(M) != np.ndarray:\n", + " try:\n", + " M = np.array(M)\n", + " except:\n", + " raise Exception(\"Matrix format error.\\n\" + name + \": \" + str(M))\n", + "\n", + " if M.ndim != 2:\n", + " raise Exception(\"Matrix dimentions error.\\n\" + name + \": \" + str(M))\n", + " format_test(A, \"A\")\n", + " format_test(B, \"B\")\n", + " \n", + " if A.shape[1] != B.shape[0]:\n", + " raise Exception(\"Matrix formats don't agree.\\n\" + \"A: \" + str(A) + \"\\nB: \" + str(B))\n", + " \n", + " \n", + " C = np.zeros((A.shape[0], B.shape[1]))\n", + " for i in range(A.shape[0]):\n", + " for j in range(B.shape[1]):\n", + " for k in range(A.shape[1]):\n", + " C[i, j] += A[i, k] * B[k, j]\n", + "\n", + " return C" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "bI2zuBOP9lIx" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_matrix_matrix_product(self):\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0,0],[0,0,0]], [[0,0],[0,0]])\n", + " with self.assertRaises(Exception):\n", + " scalar_product([[0,0],[0,0]], [[\"Hej\"], [\"Hå\"]])\n", + "\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " i = np.random.randint(min_length, max_length)\n", + " j = np.random.randint(min_length, max_length)\n", + " k = np.random.randint(min_length, max_length)\n", + " A = np.random.rand(i, j)\n", + " B = np.random.rand(j, k)\n", + " return matrix_matrix_product(A, B), A.dot(B)\n", + "\n", + " Tests.check_accuracy_multiple_random(100, genetator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To test the methods and to receive a result we test both the error handling and the accuracy of the methods. Sice we don't have any known solution to the expresions tested we compare with a solution known to be exact enough. It is difficult to know where the limit of an \"accurate\" result lie. In this lab i've chosen to check that all result lie within a 7 decimal margin.\n", + "\n", + "The test first checks that exceptions are raised when entering incompatible inputs, and then somwhere between 100 and 1000 random vectors or matricies are tested for accuracy according to the method described above." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "G1hVxfti4Ib-", + "outputId": "159525fa-e6f3-4acb-c401-f8b9d8ab9928" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "...\n", + "----------------------------------------------------------------------\n", + "Ran 3 tests in 20.209s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "suite = unittest.TestSuite()\n", + "suite.addTest(Tests('test_scalar_product'))\n", + "suite.addTest(Tests('test_matrix_vector_product'))\n", + "suite.addTest(Tests('test_matrix_matrix_product'))\n", + "\n", + "if __name__ == '__main__':\n", + " runner = unittest.TextTestRunner()\n", + " runner.run(suite)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All the test passes, meaning that the potential floating-point-errors is within an acceptable level." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Rather non-supricing all methods passed the tests. Since all implemented algorithms have a unique and mathematically explicit way of calculationg the result there ain't much room for error." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "ejemyr_lab1.ipynb", + "provenance": [], + "toc_visible": true + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-2/.ipynb_checkpoints/ejemyr_lab2-checkpoint.ipynb b/Lab-2/.ipynb_checkpoints/ejemyr_lab2-checkpoint.ipynb new file mode 100644 index 0000000..2aecf8e --- /dev/null +++ b/Lab-2/.ipynb_checkpoints/ejemyr_lab2-checkpoint.ipynb @@ -0,0 +1,707 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 2: Matrix factorization**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yJipbXtnjrJZ" + }, + "source": [ + "This lab aims to implement efficient algorithms to both multiply and factorize matrices. The focus lies both in mathematical accuracy and computational cost.\n", + "\n", + "All methods were implemented in a satisfactory way and the accuracy was generally high." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "#**About the code**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HmB2noTr1Oyo" + }, + "source": [ + "A short statement on who is the author of the file, and if the code is distributed under a certain license. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "a8b10835-c325-4b91-88f8-4bcdc70c7177" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 2, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "# In collaboration with Leo Enge (leoe@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "from google.colab import files\n", + "\n", + "import unittest\n", + "import numpy as np\n", + "import scipy.sparse as sparse" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kJiVD0LvXN6e" + }, + "source": [ + "This lab aims to implement efficient algorithms to both multiply and factorize matrices. The focus lies both in mathematical accuracy and computational cost.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "P7YR1kG_RYLl" + }, + "source": [ + "## Sparce matrix class\n", + "This class saves and handles sparce matrices in CRS format. I've overvritten the numpy method *dot(self, other)* to manually define the matrix-vector product. The algorithm uses the liniarity of matrix-vector multiplication by adding the contribution from each non-zero element in the sparse matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "gWsprvd7pCVf" + }, + "outputs": [], + "source": [ + "class SparseMatrix(sparse.csr_matrix):\n", + " def dot(self, other):\n", + " if not(type(other) == np.ndarray and other.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " \n", + " if other.size != self.shape[1]:\n", + " raise Exception(\"Vector is of wrong length.\")\n", + "\n", + " b = np.zeros(self.shape[0])\n", + " for i in range(self.shape[0]):\n", + " for j in range(self.indptr[i], self.indptr[i + 1]):\n", + " b[i] += self.data[j] * other[self.indices[j]]\n", + " return b\n", + "\n", + " def __str__(self):\n", + " return str(self.todense())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "qJtBIpbVPIHZ" + }, + "source": [ + "## QR-factorization\n", + "The QR-factorization factorizes a matrix $A$ into a matrix $Q$ with normalized column vectors spanning $\\text{Range}(A)$ and an upper triangonal square matrix $R$ with values scaling the column vectors of $Q$ back to $A$.\n", + "\n", + "The algorithm used is the ordenary Gram-Schmidt method. Its simlisity makes it easely adaptable and is here implemented for all $m \\times n$ matrices where $m \\geq n$.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "daYOUpyXvOp9" + }, + "outputs": [], + "source": [ + "def QR_factorization(A):\n", + " if not(type(A) == np.ndarray and A.ndim == 2 and A.shape[1] <= A.shape[0]):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + "\n", + " R = np.zeros((A.shape[1], A.shape[1]))\n", + " Q = np.zeros(A.shape)\n", + " v = np.zeros(A.shape[0])\n", + " v[:] = A[:, 0]\n", + "\n", + " for i in range(A.shape[1]):\n", + " R[i, i] = np.linalg.norm(v)\n", + " Q[:, i] = v / R[i, i]\n", + " for j in range(i + 1, A.shape[1]):\n", + " R[i, j] = Q[:, i].dot(A[:, j])\n", + "\n", + " if i + 1 != A.shape[1]:\n", + " v[:] = A[:, i + 1]\n", + " for j in range(i + 1):\n", + " v[:] -= R[j, i + 1] * Q[:, j]\n", + " \n", + " return Q, R" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "EVCT6FJn9Cx3" + }, + "source": [ + "## Matrix equation solvers\n", + "Below three functions for solving or optimizing matrix equations are implemented. All functions solve equations on the form\n", + "$$ A x = b. $$\n", + "The *backwards_substitution* function works for square, upper triangular matrices $A$. It then uses posibility to explicitly calculate each component of $x$ by backtracking from the equation of the last row in $Ax=b$.\n", + "\n", + "The *eq_sys_solver* function only takes square non-singular matrices. Using QR-factorization and the fact that for square matrices $Q^{-1} = Q^T$ you get\n", + "\n", + "\\begin{align}\n", + "Ax&=b \\\\\n", + "QRx&=b \\\\\n", + "Rx&=Q^Tb\n", + "\\end{align}\n", + "\n", + "Since $R$ is upper triangonal the system can be solved by backwards substitution.\n", + "\n", + "In a similar manner the *least_squares* uses the fact that the solution $x$ to $A^TAx = A^Tb$ will minimise $||Ax - b||$ and thus be the least square solution. Since $A^TA$ is a square matrix we can use the same method as in the *eq_sys_solver* function to solve this new matrix equation." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Q_YfLjXmez8O" + }, + "outputs": [], + "source": [ + "def backwards_substitution(U, b):\n", + " if not(type(U) == np.ndarray and U.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if (U != np.triu(U)).any():\n", + " raise Exception(\"Matrix is not upper triangular.\")\n", + " if np.linalg.det(U) == 0:\n", + " raise Exception(\"Matrix is singular\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != U.shape[1]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " n = U.shape[0]\n", + " x = np.zeros(n)\n", + " x[-1] = b[-1] / U[-1, -1]\n", + "\n", + " for i in range(n - 2, -1, -1):\n", + " s = 0\n", + " for j in range(i + 1, n):\n", + " s += U[i, j] * x[j]\n", + " x[i] = (b[i] - s) / U[i, i]\n", + "\n", + " return x\n", + "\n", + "def eq_sys_solver(A, b):\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " if np.linalg.det(A) == 0:\n", + " raise Exception(\"Matrix is singular.\")\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != A.shape[0]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " Q, R = QR_factorization(A)\n", + " return backwards_substitution(R, Q.transpose().dot(b))\n", + "\n", + "def least_squares(A, b):\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != A.shape[0]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " Q, R = QR_factorization(A.transpose().dot(A))\n", + " return backwards_substitution(R, Q.transpose().dot(A.transpose().dot(b)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "MF1Y_pnQD0u0" + }, + "source": [ + "## QR eigenvalue algorithm\n", + "This eigenvalue algorithm will, after many itterations, converge A and U to the Schur factorization matrices. Since only square matrices will have eigenvalues we limit $A_{in}$ to beeing a square matrix. The method returns the approximation of the eigenvalues and the corresponding eigenvectors." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "6eE3gbc5ClA5" + }, + "outputs": [], + "source": [ + "def eigen_vals_vecs(A_in, ittr :int):\n", + " A = A_in.copy()\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " U = np.eye(A.shape[0])\n", + " for i in range(ittr):\n", + " Q, R = QR_factorization(A)\n", + " A = R.dot(Q)\n", + " U = U.dot(Q)\n", + " return A.diagonal(), U" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yc1nUGpYA2kG" + }, + "source": [ + "## Block matrix matrix multiplication\n", + "My implementation divides the matrices into blocks using the folowing algorithm.\n", + "\n", + "Given a dimention of length $N$ that is to be divided into $n$ blocks the fisrt block will be of size\n", + "$$d_1 = \\text{ceil}(N / n).$$\n", + "The next block will then be of size\n", + "$$d_2 = \\text{ceil}(\\frac{N - d_1}{n - 1}).$$\n", + "Continuing with the $i$:th block beeing of size\n", + "$$d_i = \\text{ceil}(\\frac{N - d_1 - d_2 - \\ldots - d_{i-1}}{n - (i - 1)}.)$$\n", + "\n", + "This method will garantuee that the blocks will be of sizes differing by maximally one element and that the larges blocks will be first follwed by the smaller blocks.\n", + "\n", + "I chose this method since it is very deterministic and not dependent on coinsidences between matrix sizes and bock numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "J5pySVVVBQaP" + }, + "outputs": [], + "source": [ + "def blocked_matrix_matrix(A, B, m :int, n :int, p :int):\n", + " if not(type(A) == np.ndarray and A.ndim == 2 and type(B) == np.ndarray and B.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if A.shape[1] != B.shape[0]:\n", + " raise Exception(\"Matrix format do not argree.\")\n", + " if m > A.shape[0] or m < 1 or n > B.shape[1] or n < 1 or p > A.shape[1] or p < 1:\n", + " raise Exception(\"Invlid number of blocks.\")\n", + "\n", + " C = np.zeros((A.shape[0], B.shape[1]))\n", + "\n", + " idx_i, idx_j, idx_k = 0, 0, 0\n", + " step_i, step_j, step_k = 0, 0, 0\n", + "\n", + " for i in range(m):\n", + " idx_i += step_i\n", + " step_i = int(np.ceil((A.shape[0] - idx_i) / (m - i)))\n", + " idx_j = 0\n", + " step_j = 0\n", + " for j in range(n):\n", + " idx_j += step_j\n", + " step_j = int(np.ceil((B.shape[1] - idx_j) / (n - j)))\n", + " idx_k = 0\n", + " step_k = 0\n", + " for k in range(p):\n", + " idx_k += step_k\n", + " step_k = int(np.ceil((A.shape[1] - idx_k) / (p - k)))\n", + " C[idx_i : idx_i + step_i, idx_j : idx_j + step_j] += A[idx_i : idx_i + step_i, idx_k : idx_k + step_k].dot(B[idx_k : idx_k + step_k, idx_j : idx_j + step_j])\n", + " \n", + " return C" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "0cdBusjD2FO8" + }, + "source": [ + "# Tests\n", + "Testing the algorithms mainly consists of two parts: checking raises and other assertions and testing for accuracy and floating point precition.\n", + "\n", + "Generally the first is done for some common mistakes and checks that exceptions are raised. The second test is done by multiple times generating random input data and testing either against nown results, like norm equal to zero, or against other algorithms that are known to be accurate.\n", + "\n", + "Most of the accurasy testing methods are strait forward and easy to understand. One that is more interesting is the method to test the least squares solution. Since the norm will not always be zero (only in special cases) we must instead check that the norm of the error is the smallest one for all x. What I did was to repeatidly add a small vector $v$ to the solution $x$ and check that the norm of the error was never smaller than the least squares solution." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "6rt2JT47al64", + "outputId": "5eade20c-6f2c-4559-cff6-dccffb18daec" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "..........\n", + "----------------------------------------------------------------------\n", + "Ran 10 tests in 15.323s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "class TestSparseMatrix(unittest.TestCase):\n", + "\n", + " def test_exceptions(self):\n", + " A = SparseMatrix([[1,0,0],[0,1,0],[0,0,1]])\n", + " B = SparseMatrix([[0,0,0],[0,0,0],[0,0,1]])\n", + " v = np.array([1, 4])\n", + " u = [1, 3, 4]\n", + " with self.assertRaises(Exception):\n", + " A.dot(B)\n", + " with self.assertRaises(Exception):\n", + " A.dot(v1)\n", + " with self.assertRaises(Exception):\n", + " A.dot(v2)\n", + "\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " M = np.random.rand(np.random.randint(1, max_n), n)\n", + " M_spase = SparseMatrix(M)\n", + " v = np.random.rand(n)\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(M.dot(v), M_spase.dot(v), decimal=i)\n", + "\n", + "\n", + "class TestQRFactorization(unittest.TestCase):\n", + " def test_exceptions(self):\n", + " with self.assertRaises(Exception):\n", + " QR_factorization(np.array([1]))\n", + " with self.assertRaises(Exception):\n", + " QR_factorization([[1, 2], [3, 4]])\n", + " \n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " M = np.random.rand(n, n)\n", + "\n", + " Q, R = QR_factorization(M)\n", + " M_reconstructed = Q.dot(R)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(Q.transpose().dot(Q)-np.eye(Q.shape[0]), 'fro'),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(Q.dot(R) - M, 'fro'),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(R, np.triu(R), decimal=i)\n", + "\n", + "\n", + "class TestBackwardsSub(unittest.TestCase):\n", + " def test_exceptions(self):\n", + " with self.assertRaises(Exception):\n", + " backwards_substitution(np.array([[1, 2, 3], [1, 2, 3], [0, 0, 1]]),\n", + " np.array([1, 2, 3]))\n", + " \n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " U = np.triu(np.random.rand(n, n))\n", + " x = np.random.rand(n)\n", + " b = U.dot(x)\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(x, backwards_substitution(U, b), decimal=i)\n", + "\n", + "\n", + "class TestEqSolver(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " A = np.random.rand(n, n)\n", + " x_true = np.random.rand(n)\n", + " b = A.dot(x_true)\n", + " x = eq_sys_solver(A, b)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(x - x_true),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(A.dot(x) - b),\n", + " 0,\n", + " decimal=i)\n", + "\n", + "\n", + "class TestLeastSquare(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_dim = 10\n", + " for i in range(num_of_tests):\n", + " A = np.zeros((1,1))\n", + " while np.linalg.det(A.transpose().dot(A)) == 0:\n", + " m = np.random.randint(1, max_dim)\n", + " n = np.random.randint(1, m + 1)\n", + " A = np.random.rand(m, n)\n", + " b = np.random.rand(m)\n", + " x = least_squares(A, b)\n", + "\n", + " for i in range(100):\n", + " diff_vec = 0.01 * (2 * np.random.rand(n) - 1)\n", + " self.assertTrue(np.linalg.norm(A.dot(x) - b) <= np.linalg.norm(A.dot(x + diff_vec) - b))\n", + "\n", + "\n", + "class TestEigenValues(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " A = np.random.rand(n, n)\n", + " A = A.transpose().dot(A)\n", + " eigen_vals, eigen_vectors = eigen_vals_vecs(A, 100)\n", + "\n", + " for i in range(4):\n", + " for i, e in enumerate(eigen_vals):\n", + " np.testing.assert_almost_equal(np.linalg.det(A - e * np.eye(A.shape[0])), 0, decimal=i)\n", + " np.testing.assert_almost_equal(np.linalg.norm(A.dot(eigen_vectors[:, i]) - e * eigen_vectors[:, i]), 0, decimal=i)\n", + "\n", + "\n", + "class TestBlockedMatrixMult(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_dim = 50\n", + " for i in range(num_of_tests):\n", + " M = np.random.randint(1, max_dim + 1)\n", + " N = np.random.randint(1, max_dim + 1)\n", + " P = np.random.randint(1, max_dim + 1)\n", + " A = np.random.rand(M, P)\n", + " B = np.random.rand(P, N)\n", + "\n", + " m = np.random.randint(1, M + 1)\n", + " n = np.random.randint(1, N + 1)\n", + " p = np.random.randint(1, P + 1)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " blocked_matrix_matrix(A, B, m, n, p),\n", + " A.dot(B),\n", + " decimal=i\n", + " )\n", + "\n", + "\n", + "num_of_tests = 100\n", + "\n", + "if __name__ == '__main__':\n", + " unittest.main(argv=['first-arg-is-ignored'], exit=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "poXBrl37O0G2" + }, + "source": [ + "All test passed with an accuracy of up to seven decimals, except the eigenvalue finder that only had an accuracy of someware around four decimal places. The accuracy of that method did increase with the number of itterations, but is very dependent on the matrix given." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6bcsDSoRXHZe" + }, + "source": [ + "No suprices were encounterd and all methods implemented in a satisfactory way. I'm especially pleased with the block-size algorithm in the blocked matrix-matrix multiplication algorithm that I figured out on my own." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "Copy of ejemyr_lab2.ipynb", + "provenance": [] + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-2/ejemyr_lab2.ipynb b/Lab-2/ejemyr_lab2.ipynb new file mode 100644 index 0000000..2aecf8e --- /dev/null +++ b/Lab-2/ejemyr_lab2.ipynb @@ -0,0 +1,707 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 2: Matrix factorization**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yJipbXtnjrJZ" + }, + "source": [ + "This lab aims to implement efficient algorithms to both multiply and factorize matrices. The focus lies both in mathematical accuracy and computational cost.\n", + "\n", + "All methods were implemented in a satisfactory way and the accuracy was generally high." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "#**About the code**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HmB2noTr1Oyo" + }, + "source": [ + "A short statement on who is the author of the file, and if the code is distributed under a certain license. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "a8b10835-c325-4b91-88f8-4bcdc70c7177" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 2, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "# In collaboration with Leo Enge (leoe@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "from google.colab import files\n", + "\n", + "import unittest\n", + "import numpy as np\n", + "import scipy.sparse as sparse" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kJiVD0LvXN6e" + }, + "source": [ + "This lab aims to implement efficient algorithms to both multiply and factorize matrices. The focus lies both in mathematical accuracy and computational cost.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "P7YR1kG_RYLl" + }, + "source": [ + "## Sparce matrix class\n", + "This class saves and handles sparce matrices in CRS format. I've overvritten the numpy method *dot(self, other)* to manually define the matrix-vector product. The algorithm uses the liniarity of matrix-vector multiplication by adding the contribution from each non-zero element in the sparse matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "gWsprvd7pCVf" + }, + "outputs": [], + "source": [ + "class SparseMatrix(sparse.csr_matrix):\n", + " def dot(self, other):\n", + " if not(type(other) == np.ndarray and other.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " \n", + " if other.size != self.shape[1]:\n", + " raise Exception(\"Vector is of wrong length.\")\n", + "\n", + " b = np.zeros(self.shape[0])\n", + " for i in range(self.shape[0]):\n", + " for j in range(self.indptr[i], self.indptr[i + 1]):\n", + " b[i] += self.data[j] * other[self.indices[j]]\n", + " return b\n", + "\n", + " def __str__(self):\n", + " return str(self.todense())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "qJtBIpbVPIHZ" + }, + "source": [ + "## QR-factorization\n", + "The QR-factorization factorizes a matrix $A$ into a matrix $Q$ with normalized column vectors spanning $\\text{Range}(A)$ and an upper triangonal square matrix $R$ with values scaling the column vectors of $Q$ back to $A$.\n", + "\n", + "The algorithm used is the ordenary Gram-Schmidt method. Its simlisity makes it easely adaptable and is here implemented for all $m \\times n$ matrices where $m \\geq n$.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "daYOUpyXvOp9" + }, + "outputs": [], + "source": [ + "def QR_factorization(A):\n", + " if not(type(A) == np.ndarray and A.ndim == 2 and A.shape[1] <= A.shape[0]):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + "\n", + " R = np.zeros((A.shape[1], A.shape[1]))\n", + " Q = np.zeros(A.shape)\n", + " v = np.zeros(A.shape[0])\n", + " v[:] = A[:, 0]\n", + "\n", + " for i in range(A.shape[1]):\n", + " R[i, i] = np.linalg.norm(v)\n", + " Q[:, i] = v / R[i, i]\n", + " for j in range(i + 1, A.shape[1]):\n", + " R[i, j] = Q[:, i].dot(A[:, j])\n", + "\n", + " if i + 1 != A.shape[1]:\n", + " v[:] = A[:, i + 1]\n", + " for j in range(i + 1):\n", + " v[:] -= R[j, i + 1] * Q[:, j]\n", + " \n", + " return Q, R" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "EVCT6FJn9Cx3" + }, + "source": [ + "## Matrix equation solvers\n", + "Below three functions for solving or optimizing matrix equations are implemented. All functions solve equations on the form\n", + "$$ A x = b. $$\n", + "The *backwards_substitution* function works for square, upper triangular matrices $A$. It then uses posibility to explicitly calculate each component of $x$ by backtracking from the equation of the last row in $Ax=b$.\n", + "\n", + "The *eq_sys_solver* function only takes square non-singular matrices. Using QR-factorization and the fact that for square matrices $Q^{-1} = Q^T$ you get\n", + "\n", + "\\begin{align}\n", + "Ax&=b \\\\\n", + "QRx&=b \\\\\n", + "Rx&=Q^Tb\n", + "\\end{align}\n", + "\n", + "Since $R$ is upper triangonal the system can be solved by backwards substitution.\n", + "\n", + "In a similar manner the *least_squares* uses the fact that the solution $x$ to $A^TAx = A^Tb$ will minimise $||Ax - b||$ and thus be the least square solution. Since $A^TA$ is a square matrix we can use the same method as in the *eq_sys_solver* function to solve this new matrix equation." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Q_YfLjXmez8O" + }, + "outputs": [], + "source": [ + "def backwards_substitution(U, b):\n", + " if not(type(U) == np.ndarray and U.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if (U != np.triu(U)).any():\n", + " raise Exception(\"Matrix is not upper triangular.\")\n", + " if np.linalg.det(U) == 0:\n", + " raise Exception(\"Matrix is singular\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != U.shape[1]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " n = U.shape[0]\n", + " x = np.zeros(n)\n", + " x[-1] = b[-1] / U[-1, -1]\n", + "\n", + " for i in range(n - 2, -1, -1):\n", + " s = 0\n", + " for j in range(i + 1, n):\n", + " s += U[i, j] * x[j]\n", + " x[i] = (b[i] - s) / U[i, i]\n", + "\n", + " return x\n", + "\n", + "def eq_sys_solver(A, b):\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " if np.linalg.det(A) == 0:\n", + " raise Exception(\"Matrix is singular.\")\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != A.shape[0]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " Q, R = QR_factorization(A)\n", + " return backwards_substitution(R, Q.transpose().dot(b))\n", + "\n", + "def least_squares(A, b):\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if not(type(b) == np.ndarray and b.ndim == 1):\n", + " raise Exception(\"Vector format not recognized.\")\n", + " if len(b) != A.shape[0]:\n", + " raise Exception(\"Vector and matrix formats does not match.\")\n", + " \n", + " Q, R = QR_factorization(A.transpose().dot(A))\n", + " return backwards_substitution(R, Q.transpose().dot(A.transpose().dot(b)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "MF1Y_pnQD0u0" + }, + "source": [ + "## QR eigenvalue algorithm\n", + "This eigenvalue algorithm will, after many itterations, converge A and U to the Schur factorization matrices. Since only square matrices will have eigenvalues we limit $A_{in}$ to beeing a square matrix. The method returns the approximation of the eigenvalues and the corresponding eigenvectors." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "6eE3gbc5ClA5" + }, + "outputs": [], + "source": [ + "def eigen_vals_vecs(A_in, ittr :int):\n", + " A = A_in.copy()\n", + " if not(type(A) == np.ndarray and A.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " U = np.eye(A.shape[0])\n", + " for i in range(ittr):\n", + " Q, R = QR_factorization(A)\n", + " A = R.dot(Q)\n", + " U = U.dot(Q)\n", + " return A.diagonal(), U" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yc1nUGpYA2kG" + }, + "source": [ + "## Block matrix matrix multiplication\n", + "My implementation divides the matrices into blocks using the folowing algorithm.\n", + "\n", + "Given a dimention of length $N$ that is to be divided into $n$ blocks the fisrt block will be of size\n", + "$$d_1 = \\text{ceil}(N / n).$$\n", + "The next block will then be of size\n", + "$$d_2 = \\text{ceil}(\\frac{N - d_1}{n - 1}).$$\n", + "Continuing with the $i$:th block beeing of size\n", + "$$d_i = \\text{ceil}(\\frac{N - d_1 - d_2 - \\ldots - d_{i-1}}{n - (i - 1)}.)$$\n", + "\n", + "This method will garantuee that the blocks will be of sizes differing by maximally one element and that the larges blocks will be first follwed by the smaller blocks.\n", + "\n", + "I chose this method since it is very deterministic and not dependent on coinsidences between matrix sizes and bock numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "J5pySVVVBQaP" + }, + "outputs": [], + "source": [ + "def blocked_matrix_matrix(A, B, m :int, n :int, p :int):\n", + " if not(type(A) == np.ndarray and A.ndim == 2 and type(B) == np.ndarray and B.ndim == 2):\n", + " raise Exception(\"Matrix format not recognized.\")\n", + " if A.shape[1] != B.shape[0]:\n", + " raise Exception(\"Matrix format do not argree.\")\n", + " if m > A.shape[0] or m < 1 or n > B.shape[1] or n < 1 or p > A.shape[1] or p < 1:\n", + " raise Exception(\"Invlid number of blocks.\")\n", + "\n", + " C = np.zeros((A.shape[0], B.shape[1]))\n", + "\n", + " idx_i, idx_j, idx_k = 0, 0, 0\n", + " step_i, step_j, step_k = 0, 0, 0\n", + "\n", + " for i in range(m):\n", + " idx_i += step_i\n", + " step_i = int(np.ceil((A.shape[0] - idx_i) / (m - i)))\n", + " idx_j = 0\n", + " step_j = 0\n", + " for j in range(n):\n", + " idx_j += step_j\n", + " step_j = int(np.ceil((B.shape[1] - idx_j) / (n - j)))\n", + " idx_k = 0\n", + " step_k = 0\n", + " for k in range(p):\n", + " idx_k += step_k\n", + " step_k = int(np.ceil((A.shape[1] - idx_k) / (p - k)))\n", + " C[idx_i : idx_i + step_i, idx_j : idx_j + step_j] += A[idx_i : idx_i + step_i, idx_k : idx_k + step_k].dot(B[idx_k : idx_k + step_k, idx_j : idx_j + step_j])\n", + " \n", + " return C" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "0cdBusjD2FO8" + }, + "source": [ + "# Tests\n", + "Testing the algorithms mainly consists of two parts: checking raises and other assertions and testing for accuracy and floating point precition.\n", + "\n", + "Generally the first is done for some common mistakes and checks that exceptions are raised. The second test is done by multiple times generating random input data and testing either against nown results, like norm equal to zero, or against other algorithms that are known to be accurate.\n", + "\n", + "Most of the accurasy testing methods are strait forward and easy to understand. One that is more interesting is the method to test the least squares solution. Since the norm will not always be zero (only in special cases) we must instead check that the norm of the error is the smallest one for all x. What I did was to repeatidly add a small vector $v$ to the solution $x$ and check that the norm of the error was never smaller than the least squares solution." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "6rt2JT47al64", + "outputId": "5eade20c-6f2c-4559-cff6-dccffb18daec" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "..........\n", + "----------------------------------------------------------------------\n", + "Ran 10 tests in 15.323s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "class TestSparseMatrix(unittest.TestCase):\n", + "\n", + " def test_exceptions(self):\n", + " A = SparseMatrix([[1,0,0],[0,1,0],[0,0,1]])\n", + " B = SparseMatrix([[0,0,0],[0,0,0],[0,0,1]])\n", + " v = np.array([1, 4])\n", + " u = [1, 3, 4]\n", + " with self.assertRaises(Exception):\n", + " A.dot(B)\n", + " with self.assertRaises(Exception):\n", + " A.dot(v1)\n", + " with self.assertRaises(Exception):\n", + " A.dot(v2)\n", + "\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " M = np.random.rand(np.random.randint(1, max_n), n)\n", + " M_spase = SparseMatrix(M)\n", + " v = np.random.rand(n)\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(M.dot(v), M_spase.dot(v), decimal=i)\n", + "\n", + "\n", + "class TestQRFactorization(unittest.TestCase):\n", + " def test_exceptions(self):\n", + " with self.assertRaises(Exception):\n", + " QR_factorization(np.array([1]))\n", + " with self.assertRaises(Exception):\n", + " QR_factorization([[1, 2], [3, 4]])\n", + " \n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " M = np.random.rand(n, n)\n", + "\n", + " Q, R = QR_factorization(M)\n", + " M_reconstructed = Q.dot(R)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(Q.transpose().dot(Q)-np.eye(Q.shape[0]), 'fro'),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(Q.dot(R) - M, 'fro'),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(R, np.triu(R), decimal=i)\n", + "\n", + "\n", + "class TestBackwardsSub(unittest.TestCase):\n", + " def test_exceptions(self):\n", + " with self.assertRaises(Exception):\n", + " backwards_substitution(np.array([[1, 2, 3], [1, 2, 3], [0, 0, 1]]),\n", + " np.array([1, 2, 3]))\n", + " \n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " U = np.triu(np.random.rand(n, n))\n", + " x = np.random.rand(n)\n", + " b = U.dot(x)\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(x, backwards_substitution(U, b), decimal=i)\n", + "\n", + "\n", + "class TestEqSolver(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " A = np.random.rand(n, n)\n", + " x_true = np.random.rand(n)\n", + " b = A.dot(x_true)\n", + " x = eq_sys_solver(A, b)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(x - x_true),\n", + " 0,\n", + " decimal=i)\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(A.dot(x) - b),\n", + " 0,\n", + " decimal=i)\n", + "\n", + "\n", + "class TestLeastSquare(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_dim = 10\n", + " for i in range(num_of_tests):\n", + " A = np.zeros((1,1))\n", + " while np.linalg.det(A.transpose().dot(A)) == 0:\n", + " m = np.random.randint(1, max_dim)\n", + " n = np.random.randint(1, m + 1)\n", + " A = np.random.rand(m, n)\n", + " b = np.random.rand(m)\n", + " x = least_squares(A, b)\n", + "\n", + " for i in range(100):\n", + " diff_vec = 0.01 * (2 * np.random.rand(n) - 1)\n", + " self.assertTrue(np.linalg.norm(A.dot(x) - b) <= np.linalg.norm(A.dot(x + diff_vec) - b))\n", + "\n", + "\n", + "class TestEigenValues(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_n = 10\n", + " for i in range(num_of_tests):\n", + " n = np.random.randint(1, max_n)\n", + " A = np.random.rand(n, n)\n", + " A = A.transpose().dot(A)\n", + " eigen_vals, eigen_vectors = eigen_vals_vecs(A, 100)\n", + "\n", + " for i in range(4):\n", + " for i, e in enumerate(eigen_vals):\n", + " np.testing.assert_almost_equal(np.linalg.det(A - e * np.eye(A.shape[0])), 0, decimal=i)\n", + " np.testing.assert_almost_equal(np.linalg.norm(A.dot(eigen_vectors[:, i]) - e * eigen_vectors[:, i]), 0, decimal=i)\n", + "\n", + "\n", + "class TestBlockedMatrixMult(unittest.TestCase):\n", + " def test_accuracy(self):\n", + " max_dim = 50\n", + " for i in range(num_of_tests):\n", + " M = np.random.randint(1, max_dim + 1)\n", + " N = np.random.randint(1, max_dim + 1)\n", + " P = np.random.randint(1, max_dim + 1)\n", + " A = np.random.rand(M, P)\n", + " B = np.random.rand(P, N)\n", + "\n", + " m = np.random.randint(1, M + 1)\n", + " n = np.random.randint(1, N + 1)\n", + " p = np.random.randint(1, P + 1)\n", + "\n", + " for i in range(7):\n", + " np.testing.assert_almost_equal(\n", + " blocked_matrix_matrix(A, B, m, n, p),\n", + " A.dot(B),\n", + " decimal=i\n", + " )\n", + "\n", + "\n", + "num_of_tests = 100\n", + "\n", + "if __name__ == '__main__':\n", + " unittest.main(argv=['first-arg-is-ignored'], exit=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "poXBrl37O0G2" + }, + "source": [ + "All test passed with an accuracy of up to seven decimals, except the eigenvalue finder that only had an accuracy of someware around four decimal places. The accuracy of that method did increase with the number of itterations, but is very dependent on the matrix given." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6bcsDSoRXHZe" + }, + "source": [ + "No suprices were encounterd and all methods implemented in a satisfactory way. I'm especially pleased with the block-size algorithm in the blocked matrix-matrix multiplication algorithm that I figured out on my own." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "Copy of ejemyr_lab2.ipynb", + "provenance": [] + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-3/.ipynb_checkpoints/ejemyr_lab3-checkpoint.ipynb b/Lab-3/.ipynb_checkpoints/ejemyr_lab3-checkpoint.ipynb new file mode 100644 index 0000000..1c1465e --- /dev/null +++ b/Lab-3/.ipynb_checkpoints/ejemyr_lab3-checkpoint.ipynb @@ -0,0 +1,624 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 3: Iterative methods**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "**About the code**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HmB2noTr1Oyo" + }, + "source": [ + "A short statement on who is the author of the file, and if the code is distributed under a certain license. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "f74fa781-413b-41e7-a2ec-1bba2288ad4f" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Spectral radius\n", + "\n", + "We define the spectral radius of a matrix $M$ as \n", + "\n", + "$$\\rho(M) = |\\text{max}(\\lambda_1, \\lambda_2, \\ldots, \\lambda_n)|$$\n", + "\n", + "where $\\lambda_1, \\lambda_2, \\ldots, \\lambda_n$ are the eigenvalues of $M$.\n", + "\n", + "### Richardson iteration\n", + "\n", + "Below I defined the left preconditioned Richardson iteration. Using $B = I$ (letting parameter `B=None`\n", + ") you get the non preconitioned Richardson iteration.\n", + "\n", + "In my implementation I have the method raising an Exception when $\\rho(I - \\alpha BA) \\geq1$. This is beceause we can not guarantee convergence. But having $\\rho(I - \\alpha BA) \\geq 1$ does not necessarily make it divergent.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "def spectral_radius(M):\n", + " if type(M) != np.ndarray or M.ndim != 2:\n", + " raise Exception(\"M matrix format not recogniced.\")\n", + " return abs(np.max(np.linalg.eig(M)[0]))\n", + "\n", + "def richardson_iteration(A, b, alpha, tol=10e-6, x0=None, B=None):\n", + " \"\"\"The left preconditioned Richardson iteration.\"\"\"\n", + " \n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if B is None:\n", + " B = np.eye(A.shape[0])\n", + " if type(B) != np.ndarray or B.ndim != 2:\n", + " raise Exception(\"B matrix format not recogniced.\")\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " if (x0 is not None) and x0.size != A.shape[1]:\n", + " raise Exception(\"Shapes of x0 and A does not agree.\")\n", + " if A.shape[0] != B.shape[1]:\n", + " raise Exception(\"Shapes of A and B does not agree.\")\n", + " if B.shape[0] != b.size:\n", + " raise Exception(\"Shapes of B and b does not agree.\")\n", + " \n", + " x = None\n", + " if x0 is None:\n", + " x = np.zeros(A.shape[1])\n", + " else:\n", + " x = x0.copy()\n", + " \n", + " if spectral_radius(np.eye(B.shape[0]) - alpha * B.dot(A)) >= 1:\n", + " raise Exception(\"Not converging.\")\n", + " \n", + " \n", + " r = np.zeros(B.shape[0])\n", + " r[:] = b - A.dot(x)\n", + " i = 0\n", + " while np.linalg.norm(r) >= tol:\n", + " r[:] = b - A.dot(x)\n", + " x[:] = x[:] + alpha * B.dot(r)\n", + " i += 1\n", + "\n", + " return x, i" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Jacobi iteration\n", + "\n", + "As the lecture notes pointed out the Jacobi iteration is only the left preconditioned richardson itteration with $B = (\\alpha D)^{-1}$, where $D$ is the diagonal matrix with $\\text{diag}(D) = \\text{diag}(A)$." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "def jacobi_iteration(A, b, alpha, tol=10e-6, x0=None):\n", + " B = (1. / alpha) * np.diag(1. / np.diag(A))\n", + " return richardson_iteration(A, b, alpha, tol=tol, x0=x0, B=B)\n", + "\n", + "def check_jacobi_convergence(A, alpha):\n", + " B = (1. / alpha) * np.diag(1. / np.diag(A))\n", + " return spectral_radius(np.eye(B.shape[0]) - alpha * B.dot(A)) < 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Gauss-Seidel iteration\n", + "\n", + "As the lecture notes pointed out the Gauss-Seidel iteration is only the left preconditioned richardson itteration with $B = (\\alpha L)^{-1}$, where $L$ is the lower triangonal matrix created by zeroing out the over-diagonal elements in $A$." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "def gauss_seidel_iteration(A, b, alpha, tol=10e-6, x0=None):\n", + " try:\n", + " B = (1. / alpha) * np.linalg.inv(np.tril(A))\n", + " return richardson_iteration(A, b, alpha, tol=tol, x0=x0, B=B)\n", + " except:\n", + " return False\n", + "\n", + "def check_gauss_seidel_convergence(A, b, alpha):\n", + " try:\n", + " B = (1. / alpha) * np.linalg.inv(np.tril(A))\n", + " return spectral_radius(np.eye(B.shape[0]) - alpha * B.dot(A)) < 1\n", + " except:\n", + " return False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Arnoldi iteration\n", + "\n", + "I used the algorithm in the lecturenotes with slight modifications. Having problems with the algorithm dividing by zero I (with slight inpiration from Wikipedia, heh.) added a test `H[j + 1, j] > 1e-12` to ensure that no `nan` values occur." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "def arnoldi_iteration(A, b, k: int):\n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if type(b) != np.ndarray or b.ndim != 1:\n", + " raise Exception(\"b vector format not recogniced.\")\n", + " if A.shape[0] != b.size:\n", + " raise Exception(\"Shapes of A and b does not agree.\")\n", + " \n", + " H = np.zeros((k + 1, k))\n", + " Q = np.zeros((A.shape[0], k + 1))\n", + " Q[:, 0] = b / np.linalg.norm(b)\n", + " \n", + " for j in range(k):\n", + " v = A.dot(Q[:, j])\n", + " for i in range(j + 1):\n", + " H[i, j] = np.dot(Q[:, i].conj(), v)\n", + " v = v - H[i, j] * Q[:, i]\n", + "\n", + " H[j + 1, j] = np.linalg.norm(v)\n", + " if H[j + 1, j] > 1e-12:\n", + " Q[:, j + 1] = v / H[j + 1, j]\n", + " else:\n", + " return Q, H\n", + " return Q, H" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Standard basis\n", + "Super simple vector generator. Replace element $i$ with $1$." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "def standard_basis(n: int, i: int):\n", + " e_i = np.zeros(n)\n", + " e_i[i] = 1.\n", + " return e_i" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### GMRES algorithm\n", + "\n", + "Since we already written a least squares solver in a previous lab I use Numpy's `numpy.linalg.lstsq` method. To the algorithm in the lecture notes I've also added a maximum number of itterations." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "def gmres(A, b, max_itr=None, tol=10e-6):\n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if type(b) != np.ndarray or b.ndim != 1:\n", + " raise Exception(\"b vector format not recogniced.\")\n", + " if A.shape[0] != b.size:\n", + " raise Exception(\"Shapes of A and b does not agree.\")\n", + " \n", + " norm_b = np.linalg.norm(b)\n", + " \n", + " Q = np.zeros((b.size, 1))\n", + " Q[:, 0] = b[:]/norm_b\n", + " \n", + " y = None\n", + " r = tol * norm_b\n", + " \n", + " k = 0\n", + " while np.linalg.norm(r) >= tol * norm_b:\n", + " Q, H = arnoldi_iteration(A, b, k)\n", + " y = np.linalg.lstsq(H, norm_b * standard_basis(k+1, 0), rcond=None)[0]\n", + " r = H.dot(y)\n", + " r[:] = norm_b * standard_basis(k+1, 0) - r[:]\n", + " k += 1\n", + " if not(max_itr is None) and k >= max_itr:\n", + " break\n", + " \n", + " x = Q[:, 0:k-1].dot(y)\n", + " return x, k" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing iteration algorithms\n", + "\n", + "The testing of accuracy of the iteration solvers are very alike. Therefore I defined a `test_iteration_solver` method. It generates random matrix $A$ of size $\\text{max_size} \\times \\text{max_size}$ and a random vector $x$ of size $\\text{max_size}$ and then creates $b = Ax$. It then checks $||x_{est} - x|| \\approx 0$ and $||Ax - b|| \\approx 0$ down to `decimal` decimals. The process is repeated `num_of_tests` times." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "F.F\n", + "======================================================================\n", + "FAIL: test_gauss_seidel (__main__.TestIterationSolvers)\n", + "----------------------------------------------------------------------\n", + "Traceback (most recent call last):\n", + " File \"\", line 41, in test_gauss_seidel\n", + " test_iteration_solver(gauss_seidel_iteration,\n", + " File \"\", line 23, in test_iteration_solver\n", + " np.testing.assert_almost_equal(\n", + " File \"/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/numpy/testing/_private/utils.py\", line 593, in assert_almost_equal\n", + " raise AssertionError(_build_err_msg())\n", + "AssertionError: \n", + "Arrays are not almost equal to 0 decimals\n", + " ACTUAL: nan\n", + " DESIRED: 0\n", + "\n", + "======================================================================\n", + "FAIL: test_jacobi (__main__.TestIterationSolvers)\n", + "----------------------------------------------------------------------\n", + "Traceback (most recent call last):\n", + " File \"\", line 34, in test_jacobi\n", + " test_iteration_solver(jacobi_iteration,\n", + " File \"\", line 23, in test_iteration_solver\n", + " np.testing.assert_almost_equal(\n", + " File \"/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/numpy/testing/_private/utils.py\", line 593, in assert_almost_equal\n", + " raise AssertionError(_build_err_msg())\n", + "AssertionError: \n", + "Arrays are not almost equal to 0 decimals\n", + " ACTUAL: nan\n", + " DESIRED: 0\n", + "\n", + "----------------------------------------------------------------------\n", + "Ran 3 tests in 0.107s\n", + "\n", + "FAILED (failures=2)\n" + ] + } + ], + "source": [ + "def test_iteration_solver(solver, alpha=None, decimal=6, num_of_tests=1000, max_size=100):\n", + " i = 0\n", + " while i < num_of_tests:\n", + " n = np.random.randint(1, max_size)\n", + " A = np.random.rand(n, n)\n", + " x_true = np.random.rand(n)\n", + " b = A.dot(x_true)\n", + " x = np.zeros(n)\n", + "\n", + " if solver == jacobi_iteration and (not check_jacobi_convergence(A, b, alpha)):\n", + " continue\n", + " elif solver == gauss_seidel_iteration and (not check_gauss_seidel_convergence(A, b, alpha)):\n", + " continue\n", + " \n", + " if alpha is None:\n", + " x[:] = solver(A, b, tol=10**(-decimal))[0]\n", + " else:\n", + " x[:] = solver(A, b, alpha, tol=10**(-decimal))[0]\n", + "\n", + " i += 1\n", + "\n", + " for j in range(decimal):\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(x - x_true),\n", + " 0,\n", + " decimal=j)\n", + " np.testing.assert_almost_equal(\n", + " np.linalg.norm(A.dot(x) - b),\n", + " 0,\n", + " decimal=j)\n", + "\n", + "class TestIterationSolvers(unittest.TestCase):\n", + " def test_jacobi(self):\n", + " test_iteration_solver(jacobi_iteration,\n", + " alpha=0.1,\n", + " decimal=6,\n", + " num_of_tests=10,\n", + " max_size=10)\n", + " \n", + " def test_gauss_seidel(self):\n", + " test_iteration_solver(gauss_seidel_iteration,\n", + " alpha=0.1,\n", + " decimal=6,\n", + " num_of_tests=10,\n", + " max_size=10)\n", + "\n", + " def test_gmres(self):\n", + " test_iteration_solver(gmres,\n", + " decimal=6,\n", + " num_of_tests=10,\n", + " max_size=10)\n", + "\n", + "if __name__ == '__main__':\n", + " unittest.main(argv=['first-arg-is-ignored'], exit=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Convergence rate of different iterators.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mis_ok\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0mA\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrandom\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrand\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 17\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;32mnot\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0misclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiag\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0many\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mis_invertible\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtril\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 18\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mspectral_radius\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meye\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlinalg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtril\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdot\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m1\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mspectral_radius\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meye\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiag\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1.\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdiag\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdot\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mA\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m<__array_function__ internals>\u001b[0m in \u001b[0;36mtril\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "\u001b[0;32m/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/numpy/lib/twodim_base.py\u001b[0m in \u001b[0;36mtril\u001b[0;34m(m, k)\u001b[0m\n\u001b[1;32m 436\u001b[0m \u001b[0mmask\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtri\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mm\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mk\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdtype\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mbool\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 437\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 438\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mwhere\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmask\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mm\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mzeros\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mm\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 439\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 440\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m<__array_function__ internals>\u001b[0m in \u001b[0;36mwhere\u001b[0;34m(*args, **kwargs)\u001b[0m\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "iterators = [jacobi_iteration, gauss_seidel_iteration]\n", + "alphas = np.linspace(0.001, 0.1, 10)\n", + "rates = [[], []]\n", + "\n", + "n = 10\n", + "num_of_tests = 10\n", + "\n", + "alpha = max(alphas)\n", + "\n", + "def is_invertible(a):\n", + " return a.shape[0] == a.shape[1] and np.linalg.matrix_rank(a) == a.shape[0]\n", + "\n", + "is_ok = False\n", + "A = np.zeros((n, n))\n", + "while not is_ok:\n", + " A = np.random.rand(n, n)\n", + " if (not np.isclose(np.diag(A), 0).any()) and is_invertible(np.tril(A)):\n", + " if spectral_radius(np.eye(n) - np.linalg.inv(np.tril(A)).dot(A)) < 1 and spectral_radius(np.eye(n) - np.diag(1. / np.diag(A)).dot(A)) < 1:\n", + " print(A)\n", + " is_ok = True\n", + " \n", + "b = A.dot(np.random.rand(n))\n", + "for i, iterator in enumerate(iterators):\n", + " for alpha in alphas:\n", + " t0 = time.time()\n", + " for j in range(num_of_tests):\n", + " iterator(A, b, alpha, tol=10e-6)\n", + " \n", + " rates[i].append(time.time() - t0)\n", + " \n", + " plt.plot(alphas, rates[i], label=iterator)\n", + " \n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "\n", + "# **Discussion**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "include_colab_link": true, + "name": "template-report-lab-X.ipynb", + "provenance": [] + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-3/ejemyr_lab3.ipynb b/Lab-3/ejemyr_lab3.ipynb new file mode 100644 index 0000000..33a9f52 --- /dev/null +++ b/Lab-3/ejemyr_lab3.ipynb @@ -0,0 +1,769 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "ejemyr_lab_3.ipynb", + "provenance": [], + "collapsed_sections": [], + "toc_visible": true, + "include_colab_link": true + }, + "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.8.1" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 3: Iterative methods**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# Abstract\n", + "\n", + "In this lab many different itterative methods were investigated. They generally succeded, but not allways. It is intresting how the initial values sometimes can matter to the degree that the method never converges for some initial values." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "# About the code" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HmB2noTr1Oyo" + }, + "source": [ + "A short statement on who is the author of the file, and if the code is distributed under a certain license. " + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "855e77bf-545b-4a37-abbc-8cdc89ee8069", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 1 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "Xw7VlErAX7NS", + "colab": {} + }, + "source": [ + "# Load neccessary modules.\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "import math\n", + "import random\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# Introduction\n", + "\n", + "In this lab we will solve systems of linear equations, as well as finding zeros of functions. This will all be done using iterative methods." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "EN1qvsnqRj4z" + }, + "source": [ + "### Standard basis\n", + "Super simple vector generator. Replace element $i$ with $1$." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "qN-JazumRj41", + "colab": {} + }, + "source": [ + "def standard_basis(n: int, i: int):\n", + " e_i = np.zeros(n)\n", + " e_i[i] = 1.\n", + " return e_i" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "nHeust2SRj4S" + }, + "source": [ + "### Spectral radius\n", + "\n", + "We define the spectral radius of a matrix $M$ as \n", + "\n", + "$$\\rho(M) = \\text{max}\\lbrace|\\lambda_1|, |\\lambda_2|, \\ldots, |\\lambda_n|\\rbrace$$\n", + "\n", + "where $\\lambda_1, \\lambda_2, \\ldots, \\lambda_n$ are the eigenvalues of $M$.\n" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "Y6YTpQi8OZjR", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def spectral_radius(M):\n", + " if type(M) != np.ndarray or M.ndim != 2:\n", + " raise Exception(\"M matrix format not recogniced.\")\n", + " return np.max(np.abs(np.linalg.eig(M)[0]))" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TlvMgwWqOWlV", + "colab_type": "text" + }, + "source": [ + "### Richardson iteration\n", + "\n", + "Below I defined the left preconditioned Richardson iteration. Using $B = I$ (letting parameter `B=None`\n", + ") you get the non preconitioned Richardson iteration.\n", + "\n", + "In my implementation I have the method raising an Exception when $\\rho(I - \\alpha BA) \\geq1$. This is beceause we can not guarantee convergence. But having $\\rho(I - \\alpha BA) \\geq 1$ does not necessarily make it divergent.\n", + "\n", + "I've also changed the stoping criteria to be $||b - Ax|| < \\text{TOL}$ instead of $||B(b - Ax)|| < \\text{TOL}$, since it generally is the residual $||b - Ax||$ you want to minimize." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "mvBbM7pURj4W", + "colab": {} + }, + "source": [ + "def richardson_iteration(A, b, alpha, tol=1e-6, x0=None, B=None):\n", + " \"\"\"The left preconditioned Richardson iteration.\"\"\"\n", + " \n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if B is None:\n", + " B = np.eye(A.shape[0])\n", + " if type(B) != np.ndarray or B.ndim != 2:\n", + " raise Exception(\"B matrix format not recogniced.\")\n", + " if A.shape[0] != A.shape[1]:\n", + " raise Exception(\"Matrix not square.\")\n", + " if (x0 is not None) and x0.size != A.shape[1]:\n", + " raise Exception(\"Shapes of x0 and A does not agree.\")\n", + " if A.shape[0] != B.shape[1]:\n", + " raise Exception(\"Shapes of A and B does not agree.\")\n", + " if B.shape[0] != b.size:\n", + " raise Exception(\"Shapes of B and b does not agree.\")\n", + " \n", + " x = None\n", + " if x0 is None:\n", + " x = np.zeros(A.shape[1])\n", + " else:\n", + " x = x0.copy()\n", + " \n", + " if spectral_radius(np.eye(B.shape[0]) - alpha * B.dot(A)) >= 1:\n", + " return None\n", + " \n", + " r = np.zeros(B.shape[0])\n", + " r[:] = b - A.dot(x)\n", + " i = 0\n", + " while np.linalg.norm(r) > tol:\n", + " r[:] = b - A.dot(x)\n", + " x[:] = x[:] + alpha * B.dot(r)\n", + " i += 1\n", + "\n", + " return x, i" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CNMzSwaQRj4a" + }, + "source": [ + "### Jacobi iteration\n", + "\n", + "As the lecture notes pointed out the Jacobi iteration is only the left preconditioned richardson itteration with $B = (\\alpha D)^{-1}$, where $D$ is the diagonal matrix with $\\text{diag}(D) = \\text{diag}(A)$." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "R9ZTdJEARj4d", + "colab": {} + }, + "source": [ + "def check_jacobi_convergence(A):\n", + " if (np.diag(A) != 0).all():\n", + " B = np.diag(1. / np.diag(A))\n", + " return spectral_radius(np.eye(B.shape[0]) - B.dot(A)) < 1\n", + " else:\n", + " return False\n", + "\n", + "def jacobi_iteration(A, b, tol=1e-6, x0=None):\n", + " if check_jacobi_convergence(A):\n", + " B = np.diag(1. / np.diag(A))\n", + " return richardson_iteration(A, b, 1., tol=tol, x0=x0, B=B)\n", + " else:\n", + " return None" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "FhX3ZwNDRj4g" + }, + "source": [ + "### Gauss-Seidel iteration\n", + "\n", + "As the lecture notes pointed out the Gauss-Seidel iteration is only the left preconditioned richardson itteration with $B = (\\alpha L)^{-1}$, where $L$ is the lower triangonal matrix created by zeroing out the over-diagonal elements in $A$." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "ULfGq2P8Rj4h", + "colab": {} + }, + "source": [ + "def check_gauss_seidel_convergence(A):\n", + " if (np.diag(A) != 0).all():\n", + " B = np.linalg.inv(np.tril(A))\n", + " return spectral_radius(np.eye(B.shape[0]) - B.dot(A)) < 1\n", + " else:\n", + " return False\n", + "\n", + "def gauss_seidel_iteration(A, b, tol=1e-6, x0=None):\n", + " if check_gauss_seidel_convergence(A):\n", + " B = np.linalg.inv(np.tril(A))\n", + " return richardson_iteration(A, b, 1., tol=tol, x0=x0, B=B)\n", + " else:\n", + " return None" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Mjl7lz1HuvKS", + "colab_type": "text" + }, + "source": [ + "### Newtons method" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "zY4BPEMku4XB", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def get_derivative(f, x: np.array, dx_vec: np.array):\n", + " return (f(x + dx_vec) - f(x - dx_vec)) / (2 * np.linalg.norm(dx_vec))\n", + "\n", + "def jacobian(f, x0, dx: float):\n", + " n = x0.size\n", + " Df = np.zeros((n,n))\n", + " for i in range(0,n):\n", + " Df[:, i] = get_derivative(f, x0, dx * standard_basis(n, i))\n", + " return Df\n", + "\n", + "def newtons_method(f, x0, dx: float, tol=1e-6, max_itr=1e3):\n", + " x = x0\n", + " i = 0\n", + " while np.linalg.norm(f(x)) >= tol:\n", + " if i >= max_itr:\n", + " print(\"Max itr\")\n", + " return None\n", + " i += 1\n", + "\n", + " Df = jacobian(f, x0, dx)\n", + " if np.allclose(np.linalg.det(Df), 0, atol=1e-9):\n", + " print(\"Singular jacobian\")\n", + " return None\n", + " \n", + " x[:] = x - np.linalg.solve(Df, f(x))\n", + "\n", + " return x, i\n", + "\n", + "def scalar_newton(f, x0, dx: float, tol=1e-6, max_itr=1e4):\n", + " if type(x0) != np.ndarray:\n", + " x0 = np.array([x0])\n", + " \n", + " ans = newtons_method(f, x0, dx, tol=tol, max_itr=max_itr)\n", + " if ans is None:\n", + " return None, 0\n", + " else: \n", + " return ans[0][0], ans[1]\n" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fB2CO5yPRj4s" + }, + "source": [ + "### Arnoldi iteration\n", + "\n", + "I used the algorithm in the lecturenotes with slight modifications. Having problems with the algorithm dividing by zero I (with slight inpiration from Wikipedia, heh.) added a test `H[j + 1, j] > 1e-12` to ensure that no `nan` values occur." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "mDJtku63Rj4t", + "colab": {} + }, + "source": [ + "def arnoldi_iteration(A, b, k: int):\n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if type(b) != np.ndarray or b.ndim != 1:\n", + " raise Exception(\"b vector format not recogniced.\")\n", + " if A.shape[0] != b.size:\n", + " raise Exception(\"Shapes of A and b does not agree.\")\n", + " \n", + " H = np.zeros((k + 1, k))\n", + " Q = np.zeros((A.shape[0], k + 1))\n", + " Q[:, 0] = b / np.linalg.norm(b)\n", + " \n", + " for j in range(k):\n", + " v = A.dot(Q[:, j])\n", + " for i in range(j + 1):\n", + " H[i, j] = np.dot(Q[:, i].conj(), v)\n", + " v = v - H[i, j] * Q[:, i]\n", + "\n", + " H[j + 1, j] = np.linalg.norm(v)\n", + " if H[j + 1, j] > 1e-12:\n", + " Q[:, j + 1] = v / H[j + 1, j]\n", + " else:\n", + " break\n", + " return Q, H" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kf274YzBRj48" + }, + "source": [ + "### GMRES algorithm\n", + "\n", + "Since we already written a least squares solver in a previous lab I use Numpy's `numpy.linalg.lstsq` method. To the algorithm in the lecture notes I've also added a maximum number of itterations." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "ARnPi-VMRj49", + "colab": {} + }, + "source": [ + "def gmres(A, b, max_itr=None, tol=1e-6):\n", + " if type(A) != np.ndarray or A.ndim != 2:\n", + " raise Exception(\"A matrix format not recogniced.\")\n", + " if type(b) != np.ndarray or b.ndim != 1:\n", + " raise Exception(\"b vector format not recogniced.\")\n", + " if A.shape[0] != b.size:\n", + " raise Exception(\"Shapes of A and b does not agree.\")\n", + " \n", + " norm_b = np.linalg.norm(b)\n", + " \n", + " Q = np.zeros((b.size, 1))\n", + " Q[:, 0] = b[:]/norm_b\n", + " \n", + " y = None\n", + " r = tol * norm_b\n", + " \n", + " k = 0\n", + " while np.linalg.norm(r) >= tol * norm_b:\n", + " Q, H = arnoldi_iteration(A, b, k)\n", + " y = np.linalg.lstsq(H, norm_b * standard_basis(k+1, 0), rcond=None)[0]\n", + " r = H.dot(y)\n", + " r[:] = norm_b * standard_basis(k+1, 0) - r[:]\n", + " k += 1\n", + " if not(max_itr is None) and k >= max_itr:\n", + " break\n", + " \n", + " x = Q[:, 0:k-1].dot(y)\n", + " return x, k" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SxtzgZLtRj5A" + }, + "source": [ + "# Testing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h__3J5w4L7CT", + "colab_type": "text" + }, + "source": [ + "## Iteration algorithms\n", + "\n", + "The testing of accuracy of the iteration solvers are very alike. Therefore I defined a `test_iteration_solver` method. It generates random matrix $A$ of size $\\text{max_size} \\times \\text{max_size}$ and a random vector $x$ of size $\\text{max_size}$ and then creates $b = Ax$. It then checks $||x_{est} - x|| \\approx 0$ and $||Ax - b|| \\approx 0$ down to `decimal` decimals. The process is repeated `num_of_tests` times." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab_type": "code", + "id": "-B5mIAyKRj5E", + "colab": {} + }, + "source": [ + "def test_iteration_solver(solver, decimal=4, num_of_tests=1000, max_size=10, alpha=None):\n", + " i = 0\n", + " tol = 1e-6\n", + "\n", + " while i < num_of_tests:\n", + " n = np.random.randint(1, max_size)\n", + " A = 1000 * np.random.rand(n, n)\n", + " x_true = np.random.rand(n)\n", + " b = A.dot(x_true)\n", + " \n", + " if np.allclose(np.linalg.det(A), 0, 1e-9):\n", + " continue\n", + "\n", + " ans = None\n", + " if solver == richardson_iteration:\n", + " ans = solver(A, b, alpha, tol=tol)\n", + " else:\n", + " ans = solver(A, b, tol=tol)\n", + " if ans is None:\n", + " continue\n", + " i += 1\n", + "\n", + " x = ans[0]\n", + " \n", + " np.testing.assert_allclose(\n", + " np.linalg.norm(A.dot(x) - b),\n", + " 0,\n", + " atol=10 * tol)\n", + "\n", + "class TestIterationSolvers(unittest.TestCase):\n", + " def test_jacobi(self):\n", + " test_iteration_solver(jacobi_iteration)\n", + " \n", + " def test_gauss_seidel(self):\n", + " test_iteration_solver(gauss_seidel_iteration)\n", + "\n", + " def test_gmres(self):\n", + " test_iteration_solver(gmres)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "01tRYtQDI1gA", + "colab_type": "text" + }, + "source": [ + "## Newtons method\n", + "For the scalar Newtons method I generate polynomials with roots spaced along the $x$-axis. I use Newtons method to find these polynomials to find the zeros of the function and then check for accuracy. I checked that $|f(x)|\\approx 0$ and that $|x-r|\\approx 0$ for some root $r$. I calculated the tolerance of $|x-r|$ by using the derivative at the calculated root and using a linear approximation. Then the tolerance in the $x$-axis is given by the contition that\n", + "$$|x - r| < \\frac{\\text{TOL}}{f'(x)}$$\n", + "where $\\text{TOL}$ is the tolerance in the $y$-axis.\n", + "\n", + "I tested for a $1000$ random polynomials in a predefined (convenient) subspace of polynomials ($\\text{deg} < 10$, $r < \\text{10000}$, $0.1 < |r_n - r_m| < 100$)." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "qbDknwBh-6jy", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def get_rand_polynomial(deg: int, root_max_abs: float, minimal_root_dist: float):\n", + " if 2 * root_max_abs < (deg - 1) * minimal_root_dist:\n", + " raise Exception(\"Intervall error\")\n", + "\n", + " roots = []\n", + " low = -root_max_abs\n", + " high = root_max_abs - (deg - 1) * minimal_root_dist\n", + " for i in range(deg):\n", + " roots.append(random.uniform(low, high))\n", + " low = roots[i] + minimal_root_dist\n", + " high += minimal_root_dist\n", + "\n", + " def f(x):\n", + " y = 1\n", + " for root in roots:\n", + " y *= (x - root)\n", + " return y\n", + "\n", + " return f, roots\n", + "\n", + "class TestNewtonScalar(unittest.TestCase):\n", + " def test_rand_polynomial(self):\n", + " max_deg = 2\n", + " max_dist = 100\n", + " max_root_max = 10000\n", + " tol = 1e-6\n", + "\n", + " for i in range(1000):\n", + " root_dist = random.uniform(0.1, max_dist)\n", + " deg = random.randint(1, max_deg)\n", + " root_max = random.uniform((deg - 1) * root_dist, max_root_max)\n", + " \n", + " f, roots = get_rand_polynomial(deg, root_max, root_dist)\n", + "\n", + " x0 = random.uniform(-root_max, root_max)\n", + " root = scalar_newton(f, x0, tol)[0]\n", + "\n", + " np.testing.assert_allclose(f(root), 0, atol=tol)\n", + " is_close = False\n", + " dfdx = get_derivative(f, np.array([root]), np.array([1e-6]))[0]\n", + " for r in roots:\n", + " diff = r - root\n", + " if np.allclose(diff, 0, atol=tol/abs(dfdx)):\n", + " is_close = True\n", + " break\n", + "\n", + " assert is_close\n" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "Hh7QQSSLL0eW", + "colab_type": "code", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "outputId": "f1926116-8a77-46b6-c9af-551edb714f32" + }, + "source": [ + "if __name__ == '__main__':\n", + " unittest.main(argv=['first-arg-is-ignored'], exit=False)" + ], + "execution_count": 17, + "outputs": [ + { + "output_type": "stream", + "text": [ + "....\n", + "----------------------------------------------------------------------\n", + "Ran 4 tests in 9.055s\n", + "\n", + "OK\n" + ], + "name": "stderr" + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# Results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8pvacOInMQdP", + "colab_type": "text" + }, + "source": [ + "Above we can se that the iterative solvers all solve systems of linear equations. The toleranse-levels are generally accomplished, but not allways (strange...).\n", + "\n", + "The method for scalar Newton succeed in all polynomials with zeros not to close together. It can not allways be guaraanteed to succeed since you can create \"deadlocks\", but that is more common in symetric equations as $cos(x)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "\n", + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AbqFg1rSPC7V", + "colab_type": "text" + }, + "source": [ + "Having more time for the lab it would have been interesting to investigate both the number of iterations taken by the different methods, but allso the absolute time. The GMRES has a very different method to the Jacobi and Gauss-Seidel methods, so even thou the number of itterations are fewer the time complexity or absolute time might be very different." + ] + } + ] +} \ No newline at end of file diff --git a/Lab-4/ejemyr_lab4.ipynb b/Lab-4/ejemyr_lab4.ipynb new file mode 100644 index 0000000..745ddca --- /dev/null +++ b/Lab-4/ejemyr_lab4.ipynb @@ -0,0 +1,698 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "ejemyr_lab4.ipynb", + "provenance": [], + "collapsed_sections": [], + "toc_visible": true, + "machine_shape": "hm", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6RgtXlfYO_i7", + "colab_type": "text" + }, + "source": [ + "# **Lab 4: Function approximation**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9x_J5FVuPzbm", + "colab_type": "text" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4puuvkvu-nOL", + "colab_type": "text" + }, + "source": [ + "In this lab we estimate functions i one and two dimentions. Generally it was found that an arbitrarily good approximation can be made, at the expense of computational work and memory load." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OkT8J7uOWpT3", + "colab_type": "text" + }, + "source": [ + "#**About the code**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HmB2noTr1Oyo", + "colab_type": "text" + }, + "source": [ + "A short statement on who is the author of the file, and if the code is distributed under a certain license. " + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "Pdll1Xc9WP0e", + "colab_type": "code", + "outputId": "126a8872-c9fb-4b2b-b16b-c977f466d23e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "# Written in colaboration with Leo Enge (leoe@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 3 + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "28xLGz8JX3Hh", + "colab_type": "text" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D2PYNusD08Wa", + "colab_type": "text" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "Xw7VlErAX7NS", + "colab_type": "code", + "colab": {} + }, + "source": [ + "# Load neccessary modules.\n", + "from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.integrate import quad, dblquad" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gnO3lhAigLev", + "colab_type": "text" + }, + "source": [ + "# **Introduction**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0DlTQl-x6dfJ", + "colab_type": "text" + }, + "source": [ + "In this lab we will investigate how to approximate functions in a 1d- and 2d-space. This will be done by finding the function $f_{\\pi} \\in P(I)$ such that $\\| f - f_{\\pi} \\|_{L^2}$ is minimized." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WeFO9QMeUOAu", + "colab_type": "text" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1WQUJfPg_drt", + "colab_type": "text" + }, + "source": [ + "## Approximation in 1D\n", + "We use the algorithm in the lecture notes.\n", + "\n", + "Using a global basis consisting of hat functions $\\phi_i$ with peak at mesh-point $x_i$ we calculate the values of a matrix $A$ and a vector $b$ as\n", + "\n", + "$$A_{ij} = (\\phi_i, \\phi_j) \\\\ b_i = (f, \\phi_i)$$\n", + "\n", + "where $f$ is the function to approximate.\n", + "\n", + "Using the hat funcitons we have that \n", + "\n", + "\\begin{align}\n", + "(\\phi_i, \\phi_i) &= \\frac{x_i - x_{i-1}}{3} + \\frac{x_{i+1} - x_{i}}{3},\\\\\n", + "(\\phi_i, \\phi_{i+1}) &= \\frac{x_{i+1} - x_{i}}{3}, \\\\\n", + "(\\phi_i, \\phi_j) &= 0 \\quad \\text{else}.\n", + "\\end{align}\n", + "\n", + "The value of $b_i$ is calculated by integrating using the `scipy.integrate.quad` method. you only nead to integrate on the interval where $\\phi_i \\neq 0$ (the interval $(x_{i-1}, x_{i+1})$).\n", + "\n", + "By using clever indexing and itteration order, as well as thanks to the linearity of integrals, we can divide the integrals and continiusly add the contribution of each interval on all the different values in $A$ and $b$ as we traverse them.\n", + "\n", + "When $A$ and $b$ are filled with values we solve the linear system $A\\alpha = b$. $\\alpha$ will then contain the approximated values of each point on the grid. $\\alpha_i \\approx f(x_i)$.\n", + "\n" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "QBtX4Q4mKY_Y", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def lagrange_poly_1D(x : float, i : int, xk0 : float, xk1 : float, hk : float):\n", + " if i == 0:\n", + " return (xk1 - x) / hk\n", + " elif i == 1:\n", + " return (x - xk0) / hk\n", + " else:\n", + " raise Exception(\"Index error.\")\n", + "\n", + "\n", + "def L2_projection_1D(f, mesh):\n", + " h = np.diff(mesh)\n", + " b = np.zeros(len(mesh))\n", + " A = np.zeros((len(mesh), len(mesh)))\n", + " for k in range(len(mesh) - 1):\n", + " for i in range(2):\n", + " b[k + i] += quad(lambda x : f(x) * lagrange_poly_1D(x, i, mesh[k], mesh[k + 1], h[k]), mesh[k], mesh[k + 1])[0]\n", + " for j in range(2):\n", + " if i == j:\n", + " A[k + i, k + j] += h[k] / 3.\n", + " else:\n", + " A[k + i, k + j] += h[k] / 6.\n", + "\n", + " return np.linalg.solve(A, b)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "4DosyShHTsMy", + "colab_type": "code", + "outputId": "88620613-5d8c-49db-a70c-92506007aa6d", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 282 + } + }, + "source": [ + "f = lambda x : np.sin(x)**2\n", + "\n", + "x = np.linspace(-5, 5, 1000)\n", + "mesh = np.linspace(-5, 5, 20)\n", + "\n", + "alpha = L2_projection_1D(f, mesh)\n", + "\n", + "plt.plot(x, f(x))\n", + "plt.plot(mesh, alpha)" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[]" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 20 + }, + { + "output_type": "display_data", + "data": { + "image/png": "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\nywAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GFRi4a1GCFOS", + "colab_type": "text" + }, + "source": [ + "## Approximation in 2D\n", + "\n", + "### General\n", + "As with the 1D-case we itterate all intervalls, in this case triangular regions in the $(x,y)$-plane.\n", + "\n", + "For each region there are three scalar products between the function $f$ and the lagrangian polynomials $\\lambda_{p_k}$, where $\\lambda_{p_k}$ is the lagrangian polynomial with its peak at the point $p_k$.\n", + "\n", + "### Lagrangian polynomials\n", + "We use the Barycentric coordinate system to define the lagrangian polynomial (plane) over the region contained by the points $p_1, p_2, p_3$. The three polynomials are given by:\n", + "\n", + "$$\\lambda_1=\\frac{(y_2-y_3)(x-x_3)+(x_3-x_2)(y-y_3)}{(y_2-y_3)(x_1-x_3)+(x_3-x_2)(y_1-y_3)}, $$\n", + "$$\\lambda_2=\\frac{(y_3-y_1)(x-x_3)+(x_1-x_3)(y-y_3)}{(y_2-y_3)(x_1-x_3)+(x_3-x_2)(y_1-y_3)}, $$\n", + "$$\\lambda_3=1-\\lambda_1-\\lambda_2,$$\n", + "\n", + "where $(x_i, y_i) = p_i$.\n", + "\n", + "Source: [https://en.wikipedia.org/wiki/Barycentric_coordinate_system](https://en.wikipedia.org/wiki/Barycentric_coordinate_system)\n", + "\n", + "\n", + "### Integrating over a triangle\n", + "To integrate over an arbitrary triangle is not an obvious and easy task. After some research I found a method using coordinate a coordinate change, forcing the region to be the triangle with corners $(0,0), (1,0), (0,1)$.\n", + "\n", + "The coordinate change $(x,y)\\rightarrow (u,v)$ is given by\n", + "\n", + "$$(u,v)\\mapsto\\left\\{\\eqalign{x(u,v)&=a_1+u(b_1-a_1)+v(c_1-a_1) \\cr y(u,v)&=a_2+u(b_2-a_2)+v(c_2-a_2)\\ ,\\cr}\\right.$$\n", + "\n", + "This in turn leads to the following:\n", + "\n", + "$$\\int_{S} f(x,y) dS = \\int_{0}^{1}\\int_{0}^{1 - u} f(x(u, v), y(u, v)) dvdu$$\n", + "\n", + "The right integral can easily be calculated using scipy's function `scipy.integrate.dblquad`.\n", + "\n", + "Source: [https://math.stackexchange.com/questions/954409/double-integral-over-an-arbitrary-triangle](https://math.stackexchange.com/questions/954409/double-integral-over-an-arbitrary-triangle)\n" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "ccvfs7XsbUDy", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def lagrange_poly_2D(p, i, p1, p2, p3):\n", + " if i == 0:\n", + " return ((p2[1] - p3[1])*(p[0] - p3[0]) + (p3[0] - p2[0])*(p[1] - p3[1])) / ((p2[1] - p3[1])*(p1[0] - p3[0]) + (p3[0] - p2[0])*(p1[1] - p3[1]))\n", + " elif i == 1:\n", + " return ((p3[1] - p1[1])*(p[0] - p3[0]) + (p1[0] - p3[0])*(p[1] - p3[1])) / ((p2[1] - p3[1])*(p1[0] - p3[0]) + (p3[0] - p2[0])*(p1[1] - p3[1]))\n", + " elif i == 2:\n", + " return 1. - lagrange_poly_2D(p, 0, p1, p2, p3) - lagrange_poly_2D(p, 1, p1, p2, p3)\n", + "\n", + "def TriagArea(p1, p2, p3):\n", + " return 0.5 * np.linalg.norm( np.cross( p2 - p1, p3 - p1 ) )\n", + "\n", + "def inner_product_lagrange_2D(i, j, area):\n", + " if i == j:\n", + " return area / 12.\n", + " else:\n", + " return area / 6.\n", + "\n", + "def integrate_triangle(f, p1, p2, p3):\n", + " x = lambda u, v : p1[0] + u*(p2[0] - p1[0]) + v*(p3[0] - p1[0])\n", + " y = lambda u, v : p1[1] + u*(p2[1] - p1[1]) + v*(p3[1] - p1[1])\n", + "\n", + " J_abs = abs((p2[0] - p1[0]) * (p3[1] - p1[1]) - (p3[0] - p1[0]) * (p2[1] - p1[1]))\n", + "\n", + " return J_abs * dblquad(lambda v, u : f(x(u, v), y(u, v)), 0, 1, lambda x : 0, lambda x : 1 - x)[0]\n", + "\n", + "def L2_projection_2D(f, mesh):\n", + " b = np.zeros(len(mesh.x))\n", + " A = np.zeros((len(mesh.x), len(mesh.x)))\n", + " for k in range(len(mesh.triangles)):\n", + " p1 = np.array([mesh.x[mesh.triangles[k][0]], mesh.y[mesh.triangles[k][0]]])\n", + " p2 = np.array([mesh.x[mesh.triangles[k][1]], mesh.y[mesh.triangles[k][1]]])\n", + " p3 = np.array([mesh.x[mesh.triangles[k][2]], mesh.y[mesh.triangles[k][2]]])\n", + " area = TriagArea(p1, p2, p3)\n", + "\n", + " for i in range(3):\n", + " peak_ind = mesh.triangles[k][i]\n", + " b[peak_ind] += integrate_triangle(lambda x, y : f(x, y) * lagrange_poly_2D((x, y), i, p1, p2, p3), p1, p2, p3)\n", + " for j in range(i, 3):\n", + " inner_product = integrate_triangle(lambda x, y : lagrange_poly_2D((x, y), j, p1, p2, p3) * lagrange_poly_2D((x, y), i, p1, p2, p3), p1, p2, p3)\n", + " A[peak_ind, mesh.triangles[k][j]] += inner_product\n", + " if i != j:\n", + " A[mesh.triangles[k][j], peak_ind] += inner_product\n", + "\n", + "\n", + " return np.linalg.solve(A, b)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": { + "id": "GInkRIfrfZj1", + "colab_type": "code", + "outputId": "a30c3501-a56d-4af5-9aa3-0cb284628b6d", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 248 + } + }, + "source": [ + "def generate_mesh(lowX, highX, lowY, highY, Nx, Ny):\n", + " hx = (highX - lowX) / (Nx - 1)\n", + " hy = (highY - lowY) / (Ny - 1)\n", + " x = np.zeros((Nx * Ny))\n", + " y = np.zeros((Nx * Ny))\n", + " for i in range(Nx):\n", + " for j in range(Ny):\n", + " x[i * Ny + j] = i * hx + lowX\n", + " y[i * Ny + j] = j * hy + lowY\n", + "\n", + " return tri.Triangulation(x, y)\n", + "\n", + "def generate_meshgrid(lowX, highX, lowY, highY, Nx, Ny):\n", + " x = np.linspace(lowX, highX, Nx)\n", + " y = np.linspace(lowY, highY, Ny)\n", + " return np.meshgrid(x, y)\n", + "\n", + "lowX = -5\n", + "highX = 5\n", + "lowY = -5\n", + "highY = 5\n", + "\n", + "Nx = 10\n", + "Ny = 10\n", + "mesh = generate_mesh(lowX, highX, lowY, highY, Nx, Ny)\n", + "\n", + "f = lambda x, y : np.sin(x + y)\n", + "alpha = L2_projection_2D(f, mesh)\n", + "\n", + "Nxy_fine = 100\n", + "X, Y = generate_meshgrid(lowX, highX, lowY, highY, Nxy_fine, Nxy_fine)\n", + "Z = f(X, Y)\n", + "\n", + "plt.figure()\n", + "ax = plt.axes(projection='3d')\n", + "ax.plot_surface(X, Y, Z)\n", + "ax.plot_surface(mesh.x.reshape((Nx, Ny)), mesh.y.reshape((Nx, Ny)), alpha.reshape((Nx, Ny)))\n", + "plt.show()" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SsQLT38gVbn_", + "colab_type": "text" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UMhwHZ-s8h53", + "colab_type": "text" + }, + "source": [ + "One of the most intresting things to examine is the error of the approximation as a function of mesh size. It is desired from a computational standpoint to have a coarse mesh, but this might not meet approximation targets.\n", + "\n", + "To measure the error the norm of the difference between estimation and function was calculated. To make this a measurement not dependent of grid size we divide by the number of grid points." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "y2_ZmURfrS6B", + "colab_type": "code", + "colab": {} + }, + "source": [ + "def get_residual_1D(f, alpha, mesh):\n", + " return np.linalg.norm(f(mesh) - alpha) / len(alpha)\n", + "def get_residual_2D(f, alpha, mesh):\n", + " return np.linalg.norm(f(mesh.x, mesh.y) - alpha) / len(alpha)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hTxfdvJ552LN", + "colab_type": "text" + }, + "source": [ + "## 1D Tests" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "vSD9UX5w5xM7", + "colab_type": "code", + "outputId": "0a796c7b-4084-4fa5-be04-a21c6e207fed", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 626 + } + }, + "source": [ + "functions = [lambda x : np.sin(x)**2, lambda x : 1./(x + 5.05), lambda x : np.sin(10 * x)]\n", + "labels = [r'$sin^2(x)$', r'$\\frac{1}{x + 5.05}$', r'$sin(10x)$']\n", + "interval_numbers = [2, 5, 10, 50, 100, 500, 1000]\n", + "\n", + "accuracy = np.zeros((len(functions), len(interval_numbers)))\n", + "for i, f in enumerate(functions):\n", + " for j, num in enumerate(interval_numbers):\n", + " mesh = np.linspace(-5, 5, num)\n", + " accuracy[i, j] = get_residual_1D(f, L2_projection_1D(f, mesh), mesh)\n", + " print(\"Function\", labels[i], \"with mesh size\", num, \"gave accuracy\", accuracy[i, j])\n", + "\n", + "for i, vals in enumerate(accuracy):\n", + " plt.loglog(interval_numbers, vals, label=labels[i])\n", + "\n", + "plt.legend()\n", + "plt.show()" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Function $sin^2(x)$ with mesh size 2 gave accuracy 0.27742253322433674\n", + "Function $sin^2(x)$ with mesh size 5 gave accuracy 0.1093835061017417\n", + "Function $sin^2(x)$ with mesh size 10 gave accuracy 0.052567007420567136\n", + "Function $sin^2(x)$ with mesh size 50 gave accuracy 0.0007171763286917718\n", + "Function $sin^2(x)$ with mesh size 100 gave accuracy 0.0001233801988185115\n", + "Function $sin^2(x)$ with mesh size 500 gave accuracy 2.1653094948691343e-06\n", + "Function $sin^2(x)$ with mesh size 1000 gave accuracy 3.8192486201771117e-07\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 2 gave accuracy 9.235876613406887\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 5 gave accuracy 3.2561593260091257\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 10 gave accuracy 1.3833701599606856\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 50 gave accuracy 0.12906586047870536\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 100 gave accuracy 0.035981217420079925\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 500 gave accuracy 0.0009135454090306532\n", + "Function $\\frac{1}{x + 5.05}$ with mesh size 1000 gave accuracy 0.00015530450968646677\n", + "Function $sin(10x)$ with mesh size 2 gave accuracy 0.14436436447927065\n", + "Function $sin(10x)$ with mesh size 5 gave accuracy 0.05943160773353292\n", + "Function $sin(10x)$ with mesh size 10 gave accuracy 0.19966119256450449\n", + "Function $sin(10x)$ with mesh size 50 gave accuracy 0.03565784526934418\n", + "Function $sin(10x)$ with mesh size 100 gave accuracy 0.006185924892980186\n", + "Function $sin(10x)$ with mesh size 500 gave accuracy 0.00010615346901383772\n", + "Function $sin(10x)$ with mesh size 1000 gave accuracy 1.871678059778438e-05\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "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\nCUn4QghRS/w/UAfji/7DKkwAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KNzrZ4r96AEB", + "colab_type": "text" + }, + "source": [ + "## 2D Tests" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "wLrvP_m86CrN", + "colab_type": "code", + "outputId": "16dc63a0-8e5f-4315-d5a2-3ea2e41c521e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 524 + } + }, + "source": [ + "functions = [lambda x, y : np.sin(x + y)**2, lambda x, y : 1./(x + y + 10.05), lambda x, y : np.sin(10 * x * y)]\n", + "labels = [r'$sin^2(x + y)$', r'$\\frac{1}{x + y + 5.05}$', r'$sin(10xy)$']\n", + "interval_numbers = [2, 5, 7, 10, 15]\n", + "\n", + "accuracy = np.zeros((len(functions), len(interval_numbers)))\n", + "for i, f in enumerate(functions):\n", + " for j, num in enumerate(interval_numbers):\n", + " mesh = generate_mesh(-5, 5, -5, 5, num, num)\n", + " accuracy[i, j] = get_residual_2D(f, L2_projection_2D(f, mesh), mesh)\n", + " print(\"Function\", labels[i], \"with mesh size\", num, \"x\", num, \"gave accuracy\", accuracy[i, j])\n", + "\n", + "for i, vals in enumerate(accuracy):\n", + " plt.loglog(interval_numbers, vals, label=labels[i])\n", + "\n", + "plt.legend()\n", + "plt.show()" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Function $sin^2(x + y)$ with mesh size 2 x 2 gave accuracy 0.184443240786157\n", + "Function $sin^2(x + y)$ with mesh size 5 x 5 gave accuracy 0.0654777836406967\n", + "Function $sin^2(x + y)$ with mesh size 7 x 7 gave accuracy 0.04276065096920366\n", + "Function $sin^2(x + y)$ with mesh size 10 x 10 gave accuracy 0.024989227351175\n", + "Function $sin^2(x + y)$ with mesh size 15 x 15 gave accuracy 0.008565943925999579\n", + "Function $\\frac{1}{x + y + 5.05}$ with mesh size 2 x 2 gave accuracy 4.900950757132564\n", + "Function $\\frac{1}{x + y + 5.05}$ with mesh size 5 x 5 gave accuracy 0.7266744353960017\n", + "Function $\\frac{1}{x + y + 5.05}$ with mesh size 7 x 7 gave accuracy 0.3550837926813382\n", + "Function $\\frac{1}{x + y + 5.05}$ with mesh size 10 x 10 gave accuracy 0.1635791594427046\n", + "Function $\\frac{1}{x + y + 5.05}$ with mesh size 15 x 15 gave accuracy 0.06617932071912112\n", + "Function $sin(10xy)$ with mesh size 2 x 2 gave accuracy 0.48601239477347735\n", + "Function $sin(10xy)$ with mesh size 5 x 5 gave accuracy 0.10755399064019251\n", + "Function $sin(10xy)$ with mesh size 7 x 7 gave accuracy 0.09521951834440648\n", + "Function $sin(10xy)$ with mesh size 10 x 10 gave accuracy 0.06568840721367857\n", + "Function $sin(10xy)$ with mesh size 15 x 15 gave accuracy 0.046977420269774826\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "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\nAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_4GLBv0zWr7m", + "colab_type": "text" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "46CHaBQN6uJg", + "colab_type": "text" + }, + "source": [ + "As expected the error dropped when a finer mesh was used, both in the 1D and 2D examples. This is easy to understand since the mesh can be fitted to adabt better to fast curvature in the function, when finer.\n", + "\n", + "Sadly the pre calculated values of the inner products of the lagrangian functions in two dimensions did not give the desired result, so those were replaced with normal integrals over the mesh triangles resulting in bad time-complexity and absolute time." + ] + } + ] +} \ No newline at end of file diff --git a/Lab-5/.ipynb_checkpoints/ejemyr_lab5-checkpoint.ipynb b/Lab-5/.ipynb_checkpoints/ejemyr_lab5-checkpoint.ipynb new file mode 100644 index 0000000..8e19045 --- /dev/null +++ b/Lab-5/.ipynb_checkpoints/ejemyr_lab5-checkpoint.ipynb @@ -0,0 +1,523 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 5: Title**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "This lab describes and shows some of the integrationsmaethods that can beused to approximate integrals to a certain degree of accurasy. They are shown to meat the requirements expected." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "ce1a945e-2dae-4530-cb4d-1236274284c0" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "# from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "from typing import Callable, Tuple\n", + "\n", + "%matplotlib inline\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.integrate import quad\n", + "from scipy.optimize import curve_fit\n", + "\n", + "class Tests(unittest.TestCase):\n", + " @staticmethod\n", + " def check_accuracy(est: np.ndarray, true: np.ndarray, decimal: int):\n", + " np.testing.assert_almost_equal(est, true, decimal=decimal)\n", + "\n", + " @staticmethod\n", + " def check_accuracy_multiple_random(num_of_tests, generating_func, decimal):\n", + " for i in range(num_of_tests):\n", + " est, true = generating_func()\n", + " Tests.check_accuracy(est, true, decimal)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will investigate some of the different methods of integration that can be ussed to approximate an integral." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Usefull stuff" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "Interval = Tuple[float, float]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "uwVrMhGQ4viZ" + }, + "source": [ + "### 2-point Gauss quadrature\n", + "To understand explcitly how our very limited method should work we use equation (11.4) in the lecture notes. With example 11.3 we have that the 2-point Gauss quadrature should span $P^3([a, b])$. On the interval $(0,1)$ we have\n", + "\n", + "\\begin{align}\n", + " \\int_0^1 p(x) &= p_0(x_0) w_0 + p_1(x_1) w_1 \\\\\n", + " \\int_0^1 \\sum_{i=0}^3 c_i x^i &= w_0\\sum_{i=0}^3 c_i x_0^i + w_1\\sum_{i=0}^3 c_i x_1^i \\\\\n", + " \\sum_{i=0}^3 \\frac{c_i}{i+1} - w_0\\sum_{i=0}^3 c_i x_0^i - w_1\\sum_{i=0}^3 c_i x_1^i &= 0 \\\\\n", + " \\sum_{i=0}^3 c_i(\\frac{1}{i+1} - w_0 x_0^i - w_1 x_1^i) &= 0.\n", + "\\end{align}\n", + "Which for arbitrary $c_i$ ($i=0,1,2,3$) gives\n", + "\n", + "$$\\frac{1}{i+1} - w_0 x_0^i - w_1 x_1^i = 0.$$\n", + "\n", + "We get $w_0 = \\frac{1}{2}$, $x_0 = \\frac{3 - \\sqrt{3}}{6}$, $w_1 = \\frac{1}{2}$ and $x_1 = \\frac{3 + \\sqrt{3}}{6}$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def two_point_gauss_quad_on_unit_ival(f: Callable[[float], float]) -> float:\n", + " w0 = 0.5\n", + " x0 = (3 - np.sqrt(3)) / 6\n", + " w1 = 0.5\n", + " x1 = (3 + np.sqrt(3)) / 6\n", + " \n", + " return w0 * f(x0) + w1 * f(x1)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CDxXDYtrrQ-a" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_two_point_gauss_quad(self):\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " c = np.random.rand(4)\n", + " f: Callable[[float], float] = lambda x: c[0] * x**3 + c[1] * x**2 + c[2]* x + c[3]\n", + " return two_point_gauss_quad_on_unit_ival(f), c[0]/4. + c[1]/3. + c[2]/2. + c[3]\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator, 7)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "FNWnhrnD4jmh" + }, + "source": [ + "### 3-point edge midpoint quadrature" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In example 11.5 in the lecture notes the 3-point midpoint quadrature is given by the mid-points on each side of the reference triangle with equal weights of $\\frac{1}{6}$.\n", + "\n", + "The eqplicit solution of a 2D-polynomial on the reference triangle is given by\n", + "\n", + "\\begin{equation}\n", + " \\int_0^1 \\int_0^{1-x} c_1x^2 + c_2y^2 + c_3xy + c_4x + c_5y + c_6 \\ dydx = \\frac{c_1}{12} + \\frac{c_2}{12} + \\frac{c_3}{24} + \\frac{c_4}{6} + \\frac{c_5}{6} + \\frac{c_6}{2}\n", + "\\end{equation}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def three_point_quad_on_ref_trig(f: Callable[[float, float], float]) -> float:\n", + " w = 1/6\n", + " p0 = (0.5, 0)\n", + " p1 = (0, 0.5)\n", + " p2 = (0.5, 0.5)\n", + " \n", + " return w * f(p0[0], p0[1]) + w * f(p1[0], p1[1]) + w * f(p2[0], p2[1])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_three_point_quad(self):\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " c = np.random.rand(6)\n", + " f: Callable[[float, float], float] = lambda x, y: c[0] * x**2 + c[1] * y**2 + c[2]* x*y + c[3]* x + c[4] * y + c[5]\n", + " return three_point_quad_on_ref_trig(f), c[0]/12 + c[1]/12 + c[2]/24 + c[3]/6 + c[4]/6 + c[5]/2\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator, 7)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monte Carlo quadrature" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The montecarlo method is another method based on principles of statistics and probability. By randomly picking $n$ points in the region, $I$, and evaluating their value using $f$ we can get an estimate of the mean value of $f$ on $I$. Then multiplying by the region length/area/volume (depending on region) we have an estimate of the integral\n", + "\n", + "$$\\int_{I} f dI$$\n", + "\n", + "Using more points $n$ will be more likely to give a more accurete result, but is of course more demanding forom a computational point of view." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def monte_carlo_on_unit_interval(f: Callable[[np.ndarray], np.ndarray], n: int) -> float:\n", + " return f(np.random.rand(n)).mean()\n", + "\n", + "def monte_carlo_on_ref_trig(f: Callable[[np.ndarray, np.ndarray], np.ndarray], n: int) -> float:\n", + " x = np.random.rand(n)\n", + " x_temp = x.copy()\n", + " y = np.random.rand(n)\n", + " args = np.argwhere((1 - x) < y)\n", + " x.put(args, 1 - y[args])\n", + " y.put(args, 1 - x_temp[args])\n", + " \n", + " area = 1/2\n", + " \n", + " return area * f(x, y).mean()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To test the methods and to receive a result we test the accuracy of the methods. Since we have a known solution to the expresions tested we compare with this solution wether our method is exact enought. It is difficult to know where the limit of an \"accurate\" result lie. In this lab i've chosen to check that all result lie within a 7 decimal margin.\n", + "\n", + "Also a curvefit was done to show that the monte-carlo method had the expected behaviour." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "G1hVxfti4Ib-", + "outputId": "159525fa-e6f3-4acb-c401-f8b9d8ab9928" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "..\n", + "----------------------------------------------------------------------\n", + "Ran 2 tests in 0.162s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "suite = unittest.TestSuite()\n", + "suite.addTest(Tests('test_two_point_gauss_quad'))\n", + "suite.addTest(Tests('test_three_point_quad'))\n", + "\n", + "if __name__ == '__main__':\n", + " runner = unittest.TextTestRunner()\n", + " runner.run(suite)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "num_of_polys = 100\n", + "num_of_points = 100\n", + "\n", + "n = np.linspace(10, 1000, num_of_points, dtype=int)\n", + "error = np.zeros(num_of_points)\n", + "for j in range(num_of_polys):\n", + " c = np.random.rand(4)\n", + " f = lambda x: c[0] * x**3 + c[1] * x**2 + c[2]* x + c[3]\n", + " true = c[0]/4. + c[1]/3. + c[2]/2. + c[3]\n", + " \n", + " for i in range(num_of_points):\n", + " error[i] += abs(monte_carlo_on_unit_interval(f, n[i]) - true)/float(num_of_polys)\n", + "\n", + "\n", + "inv_sqrt = lambda x, a: a / np.sqrt(x)\n", + "a_opt = curve_fit(inv_sqrt, n, error, 1)[0][0]\n", + "plt.plot(n, error)\n", + "plt.plot(n, inv_sqrt(n, a_opt), 'r--')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "num_of_polys = 100\n", + "num_of_points = 100\n", + "\n", + "n = np.linspace(10, 1000, num_of_points, dtype=int)\n", + "error = np.zeros(num_of_points)\n", + "for j in range(num_of_polys):\n", + " c = np.random.rand(6)\n", + " f = lambda x, y: c[0] * (x**2) + c[1] * (y**2) + c[2]* (np.multiply(x, y)) + c[3]* x + c[4] * y + c[5]\n", + " true = c[0]/12 + c[1]/12 + c[2]/24 + c[3]/6 + c[4]/6 + c[5]/2\n", + " \n", + " for i in range(num_of_points):\n", + " error[i] += abs(monte_carlo_on_ref_trig(f, n[i]) - true)/float(num_of_polys)\n", + "\n", + "\n", + "inv_sqrt = lambda x, a: a / np.sqrt(x)\n", + "a_opt = curve_fit(inv_sqrt, n, error, 1)[0][0]\n", + "plt.plot(n, error)\n", + "plt.plot(n, inv_sqrt(n, a_opt), 'r--')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All the test passes, meaning that the potential floating-point-errors is within an acceptable level for those methods. Also the monte-carlo methods accuracy was propotional to $\\frac{1}{\\sqrt{n}}$, as expected." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results are not too supricing since most methods are mathematically exact and should only be able to give the exact and correct answer. Intresting is though how the Monte-Carlo methods follows the accuracy proportional to $\\frac{1}{\\sqrt{n}}$ strikingly well." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "ejemyr_lab1.ipynb", + "provenance": [], + "toc_visible": true + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-5/ejemyr_lab5.ipynb b/Lab-5/ejemyr_lab5.ipynb new file mode 100644 index 0000000..8e19045 --- /dev/null +++ b/Lab-5/ejemyr_lab5.ipynb @@ -0,0 +1,523 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 5: Title**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "This lab describes and shows some of the integrationsmaethods that can beused to approximate integrals to a certain degree of accurasy. They are shown to meat the requirements expected." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "ce1a945e-2dae-4530-cb4d-1236274284c0" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "# from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "from typing import Callable, Tuple\n", + "\n", + "%matplotlib inline\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.integrate import quad\n", + "from scipy.optimize import curve_fit\n", + "\n", + "class Tests(unittest.TestCase):\n", + " @staticmethod\n", + " def check_accuracy(est: np.ndarray, true: np.ndarray, decimal: int):\n", + " np.testing.assert_almost_equal(est, true, decimal=decimal)\n", + "\n", + " @staticmethod\n", + " def check_accuracy_multiple_random(num_of_tests, generating_func, decimal):\n", + " for i in range(num_of_tests):\n", + " est, true = generating_func()\n", + " Tests.check_accuracy(est, true, decimal)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will investigate some of the different methods of integration that can be ussed to approximate an integral." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Usefull stuff" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "Interval = Tuple[float, float]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "uwVrMhGQ4viZ" + }, + "source": [ + "### 2-point Gauss quadrature\n", + "To understand explcitly how our very limited method should work we use equation (11.4) in the lecture notes. With example 11.3 we have that the 2-point Gauss quadrature should span $P^3([a, b])$. On the interval $(0,1)$ we have\n", + "\n", + "\\begin{align}\n", + " \\int_0^1 p(x) &= p_0(x_0) w_0 + p_1(x_1) w_1 \\\\\n", + " \\int_0^1 \\sum_{i=0}^3 c_i x^i &= w_0\\sum_{i=0}^3 c_i x_0^i + w_1\\sum_{i=0}^3 c_i x_1^i \\\\\n", + " \\sum_{i=0}^3 \\frac{c_i}{i+1} - w_0\\sum_{i=0}^3 c_i x_0^i - w_1\\sum_{i=0}^3 c_i x_1^i &= 0 \\\\\n", + " \\sum_{i=0}^3 c_i(\\frac{1}{i+1} - w_0 x_0^i - w_1 x_1^i) &= 0.\n", + "\\end{align}\n", + "Which for arbitrary $c_i$ ($i=0,1,2,3$) gives\n", + "\n", + "$$\\frac{1}{i+1} - w_0 x_0^i - w_1 x_1^i = 0.$$\n", + "\n", + "We get $w_0 = \\frac{1}{2}$, $x_0 = \\frac{3 - \\sqrt{3}}{6}$, $w_1 = \\frac{1}{2}$ and $x_1 = \\frac{3 + \\sqrt{3}}{6}$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def two_point_gauss_quad_on_unit_ival(f: Callable[[float], float]) -> float:\n", + " w0 = 0.5\n", + " x0 = (3 - np.sqrt(3)) / 6\n", + " w1 = 0.5\n", + " x1 = (3 + np.sqrt(3)) / 6\n", + " \n", + " return w0 * f(x0) + w1 * f(x1)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CDxXDYtrrQ-a" + }, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_two_point_gauss_quad(self):\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " c = np.random.rand(4)\n", + " f: Callable[[float], float] = lambda x: c[0] * x**3 + c[1] * x**2 + c[2]* x + c[3]\n", + " return two_point_gauss_quad_on_unit_ival(f), c[0]/4. + c[1]/3. + c[2]/2. + c[3]\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator, 7)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "FNWnhrnD4jmh" + }, + "source": [ + "### 3-point edge midpoint quadrature" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In example 11.5 in the lecture notes the 3-point midpoint quadrature is given by the mid-points on each side of the reference triangle with equal weights of $\\frac{1}{6}$.\n", + "\n", + "The eqplicit solution of a 2D-polynomial on the reference triangle is given by\n", + "\n", + "\\begin{equation}\n", + " \\int_0^1 \\int_0^{1-x} c_1x^2 + c_2y^2 + c_3xy + c_4x + c_5y + c_6 \\ dydx = \\frac{c_1}{12} + \\frac{c_2}{12} + \\frac{c_3}{24} + \\frac{c_4}{6} + \\frac{c_5}{6} + \\frac{c_6}{2}\n", + "\\end{equation}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def three_point_quad_on_ref_trig(f: Callable[[float, float], float]) -> float:\n", + " w = 1/6\n", + " p0 = (0.5, 0)\n", + " p1 = (0, 0.5)\n", + " p2 = (0.5, 0.5)\n", + " \n", + " return w * f(p0[0], p0[1]) + w * f(p1[0], p1[1]) + w * f(p2[0], p2[1])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class Tests(Tests):\n", + " def test_three_point_quad(self):\n", + " min_length = 1\n", + " max_length = 100\n", + " def genetator():\n", + " c = np.random.rand(6)\n", + " f: Callable[[float, float], float] = lambda x, y: c[0] * x**2 + c[1] * y**2 + c[2]* x*y + c[3]* x + c[4] * y + c[5]\n", + " return three_point_quad_on_ref_trig(f), c[0]/12 + c[1]/12 + c[2]/24 + c[3]/6 + c[4]/6 + c[5]/2\n", + "\n", + " Tests.check_accuracy_multiple_random(1000, genetator, 7)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monte Carlo quadrature" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The montecarlo method is another method based on principles of statistics and probability. By randomly picking $n$ points in the region, $I$, and evaluating their value using $f$ we can get an estimate of the mean value of $f$ on $I$. Then multiplying by the region length/area/volume (depending on region) we have an estimate of the integral\n", + "\n", + "$$\\int_{I} f dI$$\n", + "\n", + "Using more points $n$ will be more likely to give a more accurete result, but is of course more demanding forom a computational point of view." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def monte_carlo_on_unit_interval(f: Callable[[np.ndarray], np.ndarray], n: int) -> float:\n", + " return f(np.random.rand(n)).mean()\n", + "\n", + "def monte_carlo_on_ref_trig(f: Callable[[np.ndarray, np.ndarray], np.ndarray], n: int) -> float:\n", + " x = np.random.rand(n)\n", + " x_temp = x.copy()\n", + " y = np.random.rand(n)\n", + " args = np.argwhere((1 - x) < y)\n", + " x.put(args, 1 - y[args])\n", + " y.put(args, 1 - x_temp[args])\n", + " \n", + " area = 1/2\n", + " \n", + " return area * f(x, y).mean()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To test the methods and to receive a result we test the accuracy of the methods. Since we have a known solution to the expresions tested we compare with this solution wether our method is exact enought. It is difficult to know where the limit of an \"accurate\" result lie. In this lab i've chosen to check that all result lie within a 7 decimal margin.\n", + "\n", + "Also a curvefit was done to show that the monte-carlo method had the expected behaviour." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "G1hVxfti4Ib-", + "outputId": "159525fa-e6f3-4acb-c401-f8b9d8ab9928" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "..\n", + "----------------------------------------------------------------------\n", + "Ran 2 tests in 0.162s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "suite = unittest.TestSuite()\n", + "suite.addTest(Tests('test_two_point_gauss_quad'))\n", + "suite.addTest(Tests('test_three_point_quad'))\n", + "\n", + "if __name__ == '__main__':\n", + " runner = unittest.TextTestRunner()\n", + " runner.run(suite)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "num_of_polys = 100\n", + "num_of_points = 100\n", + "\n", + "n = np.linspace(10, 1000, num_of_points, dtype=int)\n", + "error = np.zeros(num_of_points)\n", + "for j in range(num_of_polys):\n", + " c = np.random.rand(4)\n", + " f = lambda x: c[0] * x**3 + c[1] * x**2 + c[2]* x + c[3]\n", + " true = c[0]/4. + c[1]/3. + c[2]/2. + c[3]\n", + " \n", + " for i in range(num_of_points):\n", + " error[i] += abs(monte_carlo_on_unit_interval(f, n[i]) - true)/float(num_of_polys)\n", + "\n", + "\n", + "inv_sqrt = lambda x, a: a / np.sqrt(x)\n", + "a_opt = curve_fit(inv_sqrt, n, error, 1)[0][0]\n", + "plt.plot(n, error)\n", + "plt.plot(n, inv_sqrt(n, a_opt), 'r--')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "num_of_polys = 100\n", + "num_of_points = 100\n", + "\n", + "n = np.linspace(10, 1000, num_of_points, dtype=int)\n", + "error = np.zeros(num_of_points)\n", + "for j in range(num_of_polys):\n", + " c = np.random.rand(6)\n", + " f = lambda x, y: c[0] * (x**2) + c[1] * (y**2) + c[2]* (np.multiply(x, y)) + c[3]* x + c[4] * y + c[5]\n", + " true = c[0]/12 + c[1]/12 + c[2]/24 + c[3]/6 + c[4]/6 + c[5]/2\n", + " \n", + " for i in range(num_of_points):\n", + " error[i] += abs(monte_carlo_on_ref_trig(f, n[i]) - true)/float(num_of_polys)\n", + "\n", + "\n", + "inv_sqrt = lambda x, a: a / np.sqrt(x)\n", + "a_opt = curve_fit(inv_sqrt, n, error, 1)[0][0]\n", + "plt.plot(n, error)\n", + "plt.plot(n, inv_sqrt(n, a_opt), 'r--')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All the test passes, meaning that the potential floating-point-errors is within an acceptable level for those methods. Also the monte-carlo methods accuracy was propotional to $\\frac{1}{\\sqrt{n}}$, as expected." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The results are not too supricing since most methods are mathematically exact and should only be able to give the exact and correct answer. Intresting is though how the Monte-Carlo methods follows the accuracy proportional to $\\frac{1}{\\sqrt{n}}$ strikingly well." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "ejemyr_lab1.ipynb", + "provenance": [], + "toc_visible": true + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-6/.ipynb_checkpoints/ejemyr_lab6-checkpoint.ipynb b/Lab-6/.ipynb_checkpoints/ejemyr_lab6-checkpoint.ipynb new file mode 100644 index 0000000..5d77333 --- /dev/null +++ b/Lab-6/.ipynb_checkpoints/ejemyr_lab6-checkpoint.ipynb @@ -0,0 +1,330 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 6: Differential equations**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "OkT8J7uOWpT3" + }, + "source": [ + "This lab describes and shows some of the integrationsmaethods that can beused to approximate integrals to a certain degree of accurasy. They are shown to meat the requirements expected." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "ce1a945e-2dae-4530-cb4d-1236274284c0" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a template for lab reports in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2019 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS" + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "# from google.colab import files\n", + "\n", + "import time\n", + "import numpy as np\n", + "import unittest\n", + "from typing import Callable, Tuple\n", + "\n", + "%matplotlib inline\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.integrate import quad\n", + "from scipy.optimize import curve_fit\n", + "\n", + "class Tests(unittest.TestCase):\n", + " @staticmethod\n", + " def check_accuracy(est: np.ndarray, true: np.ndarray, decimal: int):\n", + " np.testing.assert_almost_equal(est, true, decimal=decimal)\n", + "\n", + " @staticmethod\n", + " def check_accuracy_multiple_random(num_of_tests, generating_func, decimal):\n", + " for i in range(num_of_tests):\n", + " est, true = generating_func()\n", + " Tests.check_accuracy(est, true, decimal)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will investigate some of the different methods of integration that can be ussed to approximate an integral." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Usefull stuff" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "Interval = Tuple[float, float]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Time stepping for scalar initial value problem $\\frac{du}{dt} = f(u,t)$" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "def explicit_euler(f: Callable[[float, float], float], u_0: float, dt: float, t_max: float=10000):\n", + " num_of_steps = int(t_max/dt) - 1\n", + " t = dt * np.arange(num_of_steps)\n", + " print(t)\n", + " u = np.zeros(num_of_steps)\n", + " u[0] = u_0\n", + " for i in range(1, num_of_steps):\n", + " u[i] = u[i - 1] + dt * f(u[i - i], t[i])\n", + " \n", + " return u, t" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.00000e+00 1.00000e-02 2.00000e-02 ... 9.99996e+03 9.99997e+03\n", + " 9.99998e+03]\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "f: Callable[[float, float], float] = lambda u, t: u\n", + "u, t = explicit_euler(f, 1.0, 0.01)\n", + "\n", + "plt.plot(t, u)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SsQLT38gVbn_" + }, + "source": [ + "# **Results**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To test the methods and to receive a result we test the accuracy of the methods. Since we have a known solution to the expresions tested we compare with this solution wether our method is exact enought. It is difficult to know where the limit of an \"accurate\" result lie. In this lab i've chosen to check that all result lie within a 7 decimal margin.\n", + "\n", + "Also a curvefit was done to show that the monte-carlo method had the expected behaviour." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + }, + "colab_type": "code", + "id": "G1hVxfti4Ib-", + "outputId": "159525fa-e6f3-4acb-c401-f8b9d8ab9928" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "..\n", + "----------------------------------------------------------------------\n", + "Ran 2 tests in 0.171s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "suite = unittest.TestSuite()\n", + "# suite.addTest(Tests('test_two_point_gauss_quad'))\n", + "\n", + "if __name__ == '__main__':\n", + " runner = unittest.TextTestRunner()\n", + " runner.run(suite)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_4GLBv0zWr7m" + }, + "source": [ + "# **Discussion**" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "include_colab_link": true, + "name": "ejemyr_lab1.ipynb", + "provenance": [], + "toc_visible": true + }, + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-6/ejemyr_lab6.ipynb b/Lab-6/ejemyr_lab6.ipynb new file mode 100644 index 0000000..65db0ac --- /dev/null +++ b/Lab-6/ejemyr_lab6.ipynb @@ -0,0 +1,544 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 6: Differential Equations**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "af578c46-6747-4d24-f123-5ec3d5b4e02e", + "pycharm": { + "is_executing": false + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a lab report in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2020 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this lab the accuracy and stability of solvers of ordinary differential equations were investigated. It was found that the implicit euler method was stabile and accurate in both scalar and vector situations." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "\n", + "import time\n", + "import numpy as np\n", + "\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.optimize import newton\n", + "from scipy.linalg import expm\n", + "\n", + "from typing import Callable, Union" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we will investigate the accuracy of solvers of ordinary differential methods (ODE). We will focus on the equation\n", + "\n", + "$$\\frac{du}{dt} = Au(t), $$\n", + "\n", + "both in the case of scalar as well as vecor functions $u$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eD6V_AZXBRaF" + }, + "source": [ + "## Scalar implicit Euler method\n", + "\n", + "We choose to implement the numeric solver with th implicit euler method using the algorithm given in the lecturenote and the newton solver of the scipy package." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "8C1t6EY4F-mB", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "def scalar_implicit_euler(func: Callable[[float, float], float], u0: float, t_max: float = 100):\n", + " dt = 1e-1\n", + " \n", + " steps = int(t_max / dt) + 1\n", + " t, dt = np.linspace(0, t_max, num=steps, retstep=True)\n", + " u = np.r_[u0, np.zeros(steps - 1)]\n", + " \n", + " for i in range(1, len(t)):\n", + " u[i] = newton(lambda x: x - u[i - 1] - dt * func(x, t[i]), u[i - 1])\n", + " \n", + " return u, t" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "lpZ2U_FRF_gm", + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "## Test scalar solver\n", + "To test the solver we compare the itterated solution with a true solution. We test for the differential equation\n", + "\n", + "$$\\frac{du}{dt} = \\lambda u(t),$$\n", + "\n", + "where $\\lambda < 0$. This ODE has the exact solution $u(t) = u_0 e^{\\lambda t}$ for some initial value $u_0$. We plot both the exact solution and the estimated solution as well as the absolute relative error of the solution. The relative error is defined by\n", + "\n", + "$$\\frac{|u_{est} - u_{true}|}{|u_0|}.$$\n", + "\n", + "If the estimated solution stabilises over time the solver is stable and if the error approches zero the solution is said to be accurate." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "D9selxBMtIne", + "pycharm": { + "is_executing": false, + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "def get_f(A: Union[int, float, np.ndarray]) -> Callable[[float, float], float]:\n", + " if isinstance(A, int) or isinstance(A, float):\n", + " def f(u: float, t: float) -> float:\n", + " return A * u\n", + " return f\n", + " elif isinstance(A, np.ndarray):\n", + " if A.ndim == 2:\n", + " def f(u: np.ndarray, t: np.ndarray) -> np.ndarray:\n", + " return A.dot(u)\n", + " return f\n", + " else:\n", + " raise TypeError(get_f.__name__ + \" can't take non-matrix array\")\n", + " else:\n", + " raise TypeError(get_f.__name__ + \" can't take input of type \" + str(type(A)))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 265 + }, + "colab_type": "code", + "id": "apLaADoowcXV", + "outputId": "98a575c2-bde4-4a1e-8640-a4723aab693c", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "def test_scalar():\n", + " u0_list = [-10, -1, 0, 1, 10]\n", + " T = 10\n", + " \n", + " const = -1\n", + " f = get_f(const)\n", + " \n", + " true_sols, est_sols = [], []\n", + " \n", + " fig, ax = plt.subplots(2, 1)\n", + " \n", + " for u0 in u0_list:\n", + " est, t = scalar_implicit_euler(f, u0, T)\n", + " est_sols.append(est)\n", + " true_sols.append(u0 * np.exp(const * t))\n", + " \n", + " ax[0].set_title(r\"Solutions with estimate\")\n", + " for true, est in zip(true_sols, est_sols):\n", + " ax[0].plot(t, true, 'k')\n", + " ax[0].plot(t, est, 'r--')\n", + "\n", + "\n", + " ax[1].set_title(r\"Relative error $\\left(\\frac{|est - true|}{u_0}\\right)$\")\n", + " for true, est, u0 in zip(true_sols, est_sols, u0_list):\n", + " error = abs(est - true)\n", + " rel_error = error\n", + " if u0 != 0:\n", + " rel_error /= abs(u0)\n", + " ax[1].plot(t, rel_error, label=r\"$u_0=$\" + str(u0))\n", + " ax[1].legend()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8Fv78S_-BVxf" + }, + "source": [ + "## Implicit Euler for system of equations\n", + "\n", + "As with the scalar solution we use the implicit euler method." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "q5ul2HixBcFf", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "def system_implicit_euler(func: Callable[[np.ndarray, np.ndarray], np.ndarray], u0: np.ndarray, t_max: float):\n", + " dt = 1e-1\n", + " \n", + " steps = int(round(t_max / dt)) + 1\n", + " t, dt = np.linspace(0, t_max, num=steps, retstep=True)\n", + " u = np.zeros((u0.size, steps))\n", + " u[:, 0] = u0\n", + " \n", + " for i in range(0, len(t) - 1):\n", + " u[:, i+1] = newton(lambda x: x - u[:, i] - dt * func(x, t[i]), u[:, i])\n", + " \n", + " return u, t\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test system solver\n", + "\n", + "Similar to the scalar case we study the equation\n", + "\n", + "$$\\frac{du}{dt} = Au(t).$$\n", + "\n", + "To easily be able to have an explicit solution we constrain the matrix $A$ to being diagonalizable ($A = XDX^{-1}$). We also impose the condition that $diag(D) < 0$. Therefor we find the folowing solution:\n", + "\n", + "$$\\frac{du}{dt} = Au(t) \\Rightarrow \\frac{du}{dt} = XDX^{-1}u(t) \\Rightarrow u(t) = Xe^{Dt}X^{-1}u_0,$$\n", + "\n", + "for some initial vector $u_0$.\n", + "\n", + "In the same manner as before we plot the true solution, the estimation and the error. This time we plot the norm of the estimate/solution and the norm of the error. This is done for solutions of random initial vectors $u_0$." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "JyGRSq_cMkCI", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "def get_true_sol(X: np.ndarray, D: np.ndarray, u0: np.ndarray, t_list: np.ndarray):\n", + " return np.array([X.dot(np.diag(np.exp(np.diag(D) * t))).dot(np.linalg.inv(X)).dot(u0) for t in t_list]).T\n", + "\n", + "def test_system():\n", + " D = np.diag(np.array([-1,-2,-3]))\n", + " X = np.array([[1/np.sqrt(2), 0, -1/np.sqrt(2)],[1/np.sqrt(2), 0, 1/np.sqrt(2)],[0, 1, 0]])\n", + " A = X.dot(D).dot(np.linalg.inv(X))\n", + "\n", + " u0_list = 10 * (np.random.rand(5, D.shape[0]) - 0.5)\n", + " T = 10\n", + " \n", + " f = lambda u, t: A.dot(u)\n", + " \n", + " fig, ax = plt.subplots(2, 1)\n", + " \n", + " for u0 in u0_list:\n", + " est, t = system_implicit_euler(f, u0, T)\n", + " true = get_true_sol(X, D, u0, t)\n", + " \n", + " ax[0].set_title(\"Solution\")\n", + " ax[0].plot(t, np.linalg.norm(est, axis=0))\n", + " ax[1].set_title(\"Error\")\n", + " ax[1].plot(t, np.linalg.norm(est - true, axis=0))\n", + " \n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random stuff" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def stochastic_trajectory(mu, sigma, t_max:float, dt: float = 1e-1):\n", + " steps = int(round(t_max / dt)) + 1\n", + " t, dt = np.linspace(0, t_max, num=steps, retstep=True)\n", + " x = np.zeros(steps)\n", + " for i in range(1, steps):\n", + " x[i] = x[i - 1] + mu * dt + sigma * np.random.normal() * np.sqrt(dt)\n", + " \n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "mu = 1\n", + "sigma = 1\n", + "t_max = 200\n", + "num_of_traj = 100\n", + "trajectories = np.array([stochastic_trajectory(mu, sigma, t_max) for _ in range(num_of_traj)])\n", + "plt.figure()\n", + "plt.plot(np.mean(trajectories, axis=0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Result" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "test_scalar()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "test_system()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Discussion\n", + "\n", + "Looking at the resulting graphs above it is clear that in both the scalar and the systems case the solutions are both accurate (small errors approching zero) and stable (approching constant value)." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "leoenge_lab6.ipynb", + "provenance": [] + }, + "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.8.1" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-7/.ipynb_checkpoints/ejemyr_lab7-checkpoint.ipynb b/Lab-7/.ipynb_checkpoints/ejemyr_lab7-checkpoint.ipynb new file mode 100644 index 0000000..dd21804 --- /dev/null +++ b/Lab-7/.ipynb_checkpoints/ejemyr_lab7-checkpoint.ipynb @@ -0,0 +1,5161 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 7: Optimization and learning**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **About the code**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "af578c46-6747-4d24-f123-5ec3d5b4e02e", + "pycharm": { + "is_executing": false + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a lab report in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2020 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "\n", + "import time\n", + "import numpy as np\n", + "\n", + "%matplotlib notebook\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.optimize import newton\n", + "from scipy.linalg import expm\n", + "\n", + "from typing import Callable, Union" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we aim to implement two algorithms to find a local minimum/maximum of a function $f: R^n \\rightarrow R$ for an arbitrary $n$. The algorithms are _Gradient decent_ and _Newton minimization_. Finding the minimum/maximum of a function has many applications in optimization, which in turn can be useful in a variety of feilds." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**\n", + "\n", + "### Gradient\n", + "We implement the gradient, $\\nabla f(x) = \\left(\\frac{\\partial f}{\\partial x_1}, \\ldots, \\frac{\\partial f}{\\partial x_n} \\right)$, by letting $\\frac{\\partial f}{\\partial x_i} = \\frac{f(x + dx_i) - f(x - dx_i)}{2dx}$ where $dx_i$ is the vector with $dx$ at index $i$ and zeros elsewhere.\n", + "\n", + "### Hessian\n", + "The hessian in some manner represent the second derivative of a general function. It is implemented forward derivative for simplicity.\n", + "\n", + "### Line-search\n", + "The\n", + "\n", + "### Gradient decent\n", + "This is implementet using method 15.1 in the lecture notes and the gradient explanied above. Also a maximum number of itterations is used.\n", + "\n", + "### Newton minimization\n", + "This is implementet using method 15.3 in the lecture notes and the gradient and hessian explanied above. Also a maximum number of itterations is used." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "def grad(f, x, dx=1e-3):\n", + " return np.array([(f(x + dx_i) - f(x - dx_i)) / (2 * dx) for dx_i in np.diag(np.full(x.size, dx))])\n", + "\n", + "def hessian(f, x, grad_f_x, dx=1e-3):\n", + " return np.array([(grad(f, x + h) - grad_f_x) / dx for h in np.diag(np.full(x.size, dx))])\n", + "\n", + "def get_minima_on_line(f, grad_f_x0, x):\n", + " return np.power(10., -np.argmin([f(x - np.power(10., -i) * grad_f_x0) for i in range(2, 6)]))\n", + "\n", + "def gradient_descent_method(f, x0, TOL=1e-6, maxitter=40):\n", + " x = [x0]\n", + " val = [f(x0)]\n", + " grad_f_x = grad(f, x[-1])\n", + " i = 0\n", + " while (maxitter is None) or (i < maxitter):\n", + " x.append(x[-1] - get_minima_on_line(f, grad_f_x, x[-1]) * grad_f_x)\n", + " val.append(f(x[-1]))\n", + " grad_f_x = grad(f, x[-1])\n", + " if np.linalg.norm(grad_f_x) <= TOL:\n", + " print(\"Minima found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " break\n", + " i += 1\n", + " else:\n", + " print(\"Minima not found within specified number of itterations.\")\n", + " return None, x, val\n", + " \n", + " return x[-1], x, val\n", + "\n", + "def newton_minimization(f, x0, TOL=1e-6, maxitter=40):\n", + " x = [x0]\n", + " val = [f(x0)]\n", + " grad_f_x = grad(f, x[-1])\n", + " hess_f_x = hessian(f, x[-1], grad_f_x)\n", + " i = 0\n", + " while (maxitter is None) or (i < maxitter):\n", + " try:\n", + " if len(x0) == 1:\n", + " x.append(x[-1] - np.linalg.solve(hess_f_x, grad_f_x)[0])\n", + " else:\n", + " x.append(x[-1] - np.linalg.solve(hess_f_x, grad_f_x))\n", + " except:\n", + " break\n", + " val.append(f(x[-1]))\n", + " grad_f_x = grad(f, x[-1])\n", + " hess_f_x = hessian(f, x[-1], grad_f_x)\n", + " if np.linalg.norm(grad_f_x) <= TOL:\n", + " print(\"Minima found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " return x[-1], x, val\n", + " i += 1\n", + " else:\n", + " print(\"Minima not found within specified number of itterations.\")\n", + " return None, x, val\n", + " \n", + " print(\"Hessian became singular, which we treat as found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " return x[-1], x, val" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Results**\n", + "\n", + "To test the methods we use som intresing functions and investigate wether the methods converge to a minima/maxima. For resons of visualization we only use functions $f: R \\rightarrow R$ and $f: R^2 \\rightarrow R$.\n", + "\n", + "The norm of the input vector/value is plotted as well as the value on the path. This shows posible convergense of the respective values." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_function_from_f(f, f_name, x_dim, size_min=10):\n", + " num_of_points = 100\n", + " linspace = np.linspace(size_min, -size_min, num_of_points)\n", + " \n", + " X, Y, Z, fig, ax = None, None, None, None, None\n", + " if x_dim == 1 or x_dim == 2:\n", + " fig = plt.figure()\n", + " if x_dim == 1:\n", + " X = linspace\n", + " Y = np.array([f(x) for x in X])\n", + " ax = plt.axes()\n", + " ax.plot(X, Y)\n", + " ax.set_ylabel('f(x)')\n", + " elif x_dim == 2:\n", + " X, Y = np.meshgrid(linspace, linspace)\n", + " Z = np.array([[f([x, y]) for x in linspace] for y in linspace])\n", + " ax = plt.axes(projection='3d')\n", + " ax.plot_surface(X, Y, Z, cmap='viridis', edgecolor='none', alpha=0.3)\n", + " ax.set_ylabel('y')\n", + " ax.set_zlabel('f(x)')\n", + " \n", + " if x_dim == 1 or x_dim == 2:\n", + " ax.set_title(f_name)\n", + " ax.set_xlabel('x')\n", + " plt.show()\n", + " \n", + " return fig\n", + "\n", + "def plot_path_on_surface(method_name, path, fig):\n", + " plt.figure(fig.number)\n", + " ax = None\n", + " if len(path[:, 0]) == 2:\n", + " ax = plt.gca()\n", + " ax.plot(path[0], path[1], label=method_name)\n", + " ax.scatter(path[0][0], path[1][0], color='g')\n", + " ax.scatter(path[0][-1], path[1][-1], color='r')\n", + " elif len(path[:, 0]) == 3:\n", + " ax = plt.gca(projection='3d')\n", + " ax.plot(path[0], path[1], path[2], label=method_name)\n", + " ax.scatter(path[0][0], path[1][0], path[2][0], color='g')\n", + " ax.scatter(path[0][-1], path[1][-1], path[2][-1], color='r')\n", + " if ax is not None:\n", + " ax.legend()\n", + " plt.show()\n", + " \n", + "def plot_convergence(method_name, path, fig):\n", + " x = None\n", + " if len(path[:, 0]) == 2:\n", + " x = [abs(path[:-1, i]) for i in range(len(path[0, :]))]\n", + " elif len(path[:, 0]) == 3:\n", + " x = np.linalg.norm(path[:-1, :], axis=0)\n", + " \n", + " plt.figure(fig.number)\n", + " axes = fig.get_axes()\n", + " ax1, ax2 = None, None\n", + " if len(axes) == 0:\n", + " ax1, ax2 = fig.subplots(nrows=2, ncols=1, sharex=True)\n", + " else:\n", + " ax1, ax2 = axes\n", + " ax2.set_xlabel(\"Step\")\n", + " ax1.set_title(\"Norm of x\")\n", + " ax2.set_title(\"Function value\")\n", + " ax1.set_ylabel(r\"$||x||$\")\n", + " ax2.set_ylabel(r\"$f(x)$\")\n", + " \n", + " ax1.plot(np.arange(len(x)), x, '-o', label=method_name)\n", + " ax2.plot(np.arange(len(path[-1, :])), path[-1, :], '-o', label=method_name)\n", + " ax1.legend()\n", + " ax2.legend()\n", + " plt.show()\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "def test_function(f, f_name, x0):\n", + " function_fig = plot_function_from_f(f, f_name, len(x0), 7.5)\n", + " convergence_fig = plt.figure()\n", + " for (x_min, path_x, path_val), method_name in zip([gradient_descent_method(f, x0), newton_minimization(f, x0)], [\"Grad decent\", \"Newton min\"]):\n", + " path = np.array([[*x, f] for x, f in zip(path_x, path_val)]).T\n", + " plot_path_on_surface(method_name, path, function_fig)\n", + " plot_convergence(method_name, path, convergence_fig)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima not found within specified number of itterations.\n", + "Minima found! Norm of gradient was 8.740395393937189e-07 and x is given by [ 1.57079652e+00 -1.24987264e-03]\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima found! Norm of gradient was 0.0 and x is given by [-5.79446622e+18 1.54814009e+17]\n", + "Minima found! Norm of gradient was 3.7003889607780755e-08 and x is given by [9.13351722e-09 7.19408323e+00]\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima found! Norm of gradient was 0.0 and x is given by [[[[[[1.61304233e+25]]]]]]\n", + "Minima found! Norm of gradient was 5.444089623551918e-09 and x is given by [1.58113851]\n" + ] + } + ], + "source": [ + "name_list = []\n", + "f_list = []\n", + "x0_list = []\n", + "\n", + "name_list.append(r\"$sin(||x||) + sin(x_0)$\")\n", + "f_list.append(lambda x: np.sin(np.linalg.norm(x)) + np.sin(x[0]))\n", + "x0_list.append(np.array([2.5, -1]))\n", + "\n", + "name_list.append(r\"$||x||^2 sin(\\sqrt{x_0^2 + 0.5 * x_1^2})$\")\n", + "f_list.append(lambda x: np.linalg.norm(x)**2 * np.sin(np.sqrt(x[0]**2 + 0.5 * x[1]**2)))\n", + "x0_list.append(np.array([2, 2]))\n", + "\n", + "name_list.append(r\"$x^4 - 5 * x^2 + 2.5$\")\n", + "f_list.append(lambda x: x**4 - 5 * x**2 + 2.5)\n", + "x0_list.append(np.array([1]))\n", + "\n", + "for f, f_name, x0 in zip(f_list, name_list, x0_list):\n", + " test_function(f, f_name, x0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Discussion\n", + "\n", + "Both methods converge in all cases, but generally the Newton method converges faster. I the fist case the mathods converge to different points since the newton method can find any minima/maxima while the gradient decent only finds local minima och sadel points. It is also intresting to note that the gradient decent often converges quickly, but often never acctually gets inside the tolerance." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "leoenge_lab6.ipynb", + "provenance": [] + }, + "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.8.1" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/Lab-7/ejemyr_lab7.ipynb b/Lab-7/ejemyr_lab7.ipynb new file mode 100644 index 0000000..7c76d89 --- /dev/null +++ b/Lab-7/ejemyr_lab7.ipynb @@ -0,0 +1,5169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6RgtXlfYO_i7" + }, + "source": [ + "# **Lab 7: Optimization and learning**\n", + "**Christoffer Ejemyr**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9x_J5FVuPzbm" + }, + "source": [ + "# **Abstract**\n", + "\n", + "The purpose of this report is to implement methods to find stationary points of functions. _Gradient decent_ and _Newton minimization method_ were used. Both methods were tested for three different funcitons and converged in all cases. The Newtons method generally converged quicker and " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **About the code**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "id": "Pdll1Xc9WP0e", + "outputId": "af578c46-6747-4d24-f123-5ec3d5b4e02e", + "pycharm": { + "is_executing": false + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'KTH Royal Institute of Technology, Stockholm, Sweden.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"This program is a lab report in the course\"\"\"\n", + "\"\"\"DD2363 Methods in Scientific Computing, \"\"\"\n", + "\"\"\"KTH Royal Institute of Technology, Stockholm, Sweden.\"\"\"\n", + "\n", + "# Copyright (C) 2020 Christoffer Ejemyr (ejemyr@kth.se)\n", + "\n", + "# This file is part of the course DD2363 Methods in Scientific Computing\n", + "# KTH Royal Institute of Technology, Stockholm, Sweden\n", + "#\n", + "# This is free software: you can redistribute it and/or modify\n", + "# it under the terms of the GNU Lesser General Public License as published by\n", + "# the Free Software Foundation, either version 3 of the License, or\n", + "# (at your option) any later version." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Problems discussed with Leo Enge." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "28xLGz8JX3Hh" + }, + "source": [ + "# **Set up environment**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "D2PYNusD08Wa" + }, + "source": [ + "To have access to the neccessary modules you have to run this cell. If you need additional modules, this is where you add them. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Xw7VlErAX7NS", + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "# Load neccessary modules.\n", + "\n", + "import time\n", + "import numpy as np\n", + "\n", + "%matplotlib notebook\n", + "from matplotlib import pyplot as plt\n", + "from matplotlib import tri\n", + "from matplotlib import axes\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "from scipy.optimize import newton\n", + "from scipy.linalg import expm\n", + "\n", + "from typing import Callable, Union" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gnO3lhAigLev" + }, + "source": [ + "# **Introduction**\n", + "\n", + "In this lab we aim to implement two algorithms to find a local minimum/maximum of a function $f: R^n \\rightarrow R$ for an arbitrary $n$. The algorithms are _Gradient decent_ and _Newton minimization_. Finding the minimum/maximum of a function has many applications in optimization, which in turn can be useful in a variety of feilds." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "WeFO9QMeUOAu" + }, + "source": [ + "# **Methods**\n", + "\n", + "### Gradient\n", + "We implement the gradient, $\\nabla f(x) = \\left(\\frac{\\partial f}{\\partial x_1}, \\ldots, \\frac{\\partial f}{\\partial x_n} \\right)$, by letting $\\frac{\\partial f}{\\partial x_i} = \\frac{f(x + dx_i) - f(x - dx_i)}{2dx}$ where $dx_i$ is the vector with $dx$ at index $i$ and zeros elsewhere.\n", + "\n", + "### Hessian\n", + "The hessian in some manner represent the second derivative of a general function. It is implemented forward derivative for simplicity.\n", + "\n", + "### Line-search\n", + "The search finds the value for $\\alpha = 10^(-i), i \\in {1, 2, 3, 4, 5}$ where $f(x - \\alpha \\nabla f(x))$ is as small as posible.\n", + "\n", + "### Gradient decent\n", + "This is implementet using method 15.1 in the lecture notes and the gradient explanied above. Also a maximum number of itterations is used.\n", + "\n", + "### Newton minimization\n", + "This is implementet using method 15.3 in the lecture notes and the gradient and hessian explanied above. Also a maximum number of itterations is used." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "def grad(f, x, dx=1e-3):\n", + " return np.array([(f(x + dx_i) - f(x - dx_i)) / (2 * dx) for dx_i in np.diag(np.full(x.size, dx))])\n", + "\n", + "def hessian(f, x, grad_f_x, dx=1e-3):\n", + " return np.array([(grad(f, x + h) - grad_f_x) / dx for h in np.diag(np.full(x.size, dx))])\n", + "\n", + "def get_minima_on_line(f, grad_f_x0, x):\n", + " return np.power(10., -np.argmin([f(x - np.power(10., -i) * grad_f_x0) for i in range(1, 6)]) - 1)\n", + "\n", + "def gradient_descent_method(f, x0, TOL=1e-6, maxitter=100):\n", + " x = [x0]\n", + " val = [f(x0)]\n", + " grad_f_x = grad(f, x[-1])\n", + " i = 0\n", + " while (maxitter is None) or (i < maxitter):\n", + " x.append(x[-1] - get_minima_on_line(f, grad_f_x, x[-1]) * grad_f_x)\n", + " val.append(f(x[-1]))\n", + " grad_f_x = grad(f, x[-1])\n", + " if np.linalg.norm(grad_f_x) <= TOL:\n", + " print(\"Minima found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " break\n", + " i += 1\n", + " else:\n", + " print(\"Minima not found within specified number of itterations.\")\n", + " return None, x, val\n", + " \n", + " return x[-1], x, val\n", + "\n", + "def newton_minimization(f, x0, TOL=1e-6, maxitter=100):\n", + " x = [x0]\n", + " val = [f(x0)]\n", + " grad_f_x = grad(f, x[-1])\n", + " hess_f_x = hessian(f, x[-1], grad_f_x)\n", + " i = 0\n", + " while (maxitter is None) or (i < maxitter):\n", + " try:\n", + " if len(x0) == 1:\n", + " x.append(x[-1] - np.linalg.solve(hess_f_x, grad_f_x)[0])\n", + " else:\n", + " x.append(x[-1] - np.linalg.solve(hess_f_x, grad_f_x))\n", + " except:\n", + " break\n", + " val.append(f(x[-1]))\n", + " grad_f_x = grad(f, x[-1])\n", + " hess_f_x = hessian(f, x[-1], grad_f_x)\n", + " if np.linalg.norm(grad_f_x) <= TOL:\n", + " print(\"Minima found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " return x[-1], x, val\n", + " i += 1\n", + " else:\n", + " print(\"Minima not found within specified number of itterations.\")\n", + " return None, x, val\n", + " \n", + " print(\"Hessian became singular, which we treat as found! Norm of gradient was\", np.linalg.norm(grad_f_x), \"and x is given by\", x[-1])\n", + " return x[-1], x, val" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **Results**\n", + "\n", + "To test the methods we use som intresing functions and investigate wether the methods converge to a minima/maxima. For resons of visualization we only use functions $f: R \\rightarrow R$ and $f: R^2 \\rightarrow R$.\n", + "\n", + "The norm of the input vector/value is plotted as well as the value on the path. This shows posible convergense of the respective values." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "def plot_function_from_f(f, f_name, x_dim, size_min=10):\n", + " num_of_points = 100\n", + " linspace = np.linspace(size_min, -size_min, num_of_points)\n", + " \n", + " X, Y, Z, fig, ax = None, None, None, None, None\n", + " if x_dim == 1 or x_dim == 2:\n", + " fig = plt.figure()\n", + " if x_dim == 1:\n", + " X = linspace\n", + " Y = np.array([f(x) for x in X])\n", + " ax = plt.axes()\n", + " ax.plot(X, Y)\n", + " ax.set_ylabel('f(x)')\n", + " elif x_dim == 2:\n", + " X, Y = np.meshgrid(linspace, linspace)\n", + " Z = np.array([[f([x, y]) for x in linspace] for y in linspace])\n", + " ax = plt.axes(projection='3d')\n", + " ax.plot_surface(X, Y, Z, cmap='viridis', edgecolor='none', alpha=0.3)\n", + " ax.set_ylabel('y')\n", + " ax.set_zlabel('f(x)')\n", + " \n", + " if x_dim == 1 or x_dim == 2:\n", + " ax.set_title(f_name)\n", + " ax.set_xlabel('x')\n", + " plt.show()\n", + " \n", + " return fig\n", + "\n", + "def plot_path_on_surface(method_name, path, fig):\n", + " plt.figure(fig.number)\n", + " ax = None\n", + " if len(path[:, 0]) == 2:\n", + " ax = plt.gca()\n", + " ax.plot(path[0], path[1], label=method_name)\n", + " ax.scatter(path[0][0], path[1][0], color='g')\n", + " ax.scatter(path[0][-1], path[1][-1], color='r')\n", + " elif len(path[:, 0]) == 3:\n", + " ax = plt.gca(projection='3d')\n", + " ax.plot(path[0], path[1], path[2], label=method_name)\n", + " ax.scatter(path[0][0], path[1][0], path[2][0], color='g')\n", + " ax.scatter(path[0][-1], path[1][-1], path[2][-1], color='r')\n", + " if ax is not None:\n", + " ax.legend()\n", + " plt.show()\n", + " \n", + "def plot_convergence(method_name, path, fig):\n", + " x = None\n", + " if len(path[:, 0]) == 2:\n", + " x = [abs(path[:-1, i]) for i in range(len(path[0, :]))]\n", + " elif len(path[:, 0]) == 3:\n", + " x = np.linalg.norm(path[:-1, :], axis=0)\n", + " \n", + " plt.figure(fig.number)\n", + " axes = fig.get_axes()\n", + " ax1, ax2 = None, None\n", + " if len(axes) == 0:\n", + " ax1, ax2 = fig.subplots(nrows=2, ncols=1, sharex=True)\n", + " else:\n", + " ax1, ax2 = axes\n", + " ax2.set_xlabel(\"Step\")\n", + " ax1.set_title(\"Norm of x\")\n", + " ax2.set_title(\"Function value\")\n", + " ax1.set_ylabel(r\"$||x||$\")\n", + " ax2.set_ylabel(r\"$f(x)$\")\n", + " \n", + " ax1.plot(np.arange(len(x)), x, '-o', label=method_name)\n", + " ax2.plot(np.arange(len(path[-1, :])), path[-1, :], '-o', label=method_name)\n", + " ax1.legend()\n", + " ax2.legend()\n", + " plt.show()\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "def test_function(f, f_name, x0):\n", + " function_fig = plot_function_from_f(f, f_name, len(x0), 7.5)\n", + " convergence_fig = plt.figure()\n", + " for (x_min, path_x, path_val), method_name in zip([gradient_descent_method(f, x0), newton_minimization(f, x0)], [\"Grad decent\", \"Newton min\"]):\n", + " path = np.array([[*x, f] for x, f in zip(path_x, path_val)]).T\n", + " plot_path_on_surface(method_name, path, function_fig)\n", + " plot_convergence(method_name, path, convergence_fig)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima not found within specified number of itterations.\n", + "Minima found! Norm of gradient was 8.740395393937189e-07 and x is given by [ 1.57079652e+00 -1.24987264e-03]\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima not found within specified number of itterations.\n", + "Minima found! Norm of gradient was 3.7003889607780755e-08 and x is given by [9.13351722e-09 7.19408323e+00]\n" + ] + }, + { + "data": { + "application/javascript": [ + "/* Put everything inside the global mpl namespace */\n", + "window.mpl = {};\n", + "\n", + "\n", + "mpl.get_websocket_type = function() {\n", + " if (typeof(WebSocket) !== 'undefined') {\n", + " return WebSocket;\n", + " } else if (typeof(MozWebSocket) !== 'undefined') {\n", + " return MozWebSocket;\n", + " } else {\n", + " alert('Your browser does not have WebSocket support. ' +\n", + " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", + " 'Firefox 4 and 5 are also supported but you ' +\n", + " 'have to enable WebSockets in about:config.');\n", + " };\n", + "}\n", + "\n", + "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", + " this.id = figure_id;\n", + "\n", + " this.ws = websocket;\n", + "\n", + " this.supports_binary = (this.ws.binaryType != undefined);\n", + "\n", + " if (!this.supports_binary) {\n", + " var warnings = document.getElementById(\"mpl-warnings\");\n", + " if (warnings) {\n", + " warnings.style.display = 'block';\n", + " warnings.textContent = (\n", + " \"This browser does not support binary websocket messages. \" +\n", + " \"Performance may be slow.\");\n", + " }\n", + " }\n", + "\n", + " this.imageObj = new Image();\n", + "\n", + " this.context = undefined;\n", + " this.message = undefined;\n", + " this.canvas = undefined;\n", + " this.rubberband_canvas = undefined;\n", + " this.rubberband_context = undefined;\n", + " this.format_dropdown = undefined;\n", + "\n", + " this.image_mode = 'full';\n", + "\n", + " this.root = $('
');\n", + " this._root_extra_style(this.root)\n", + " this.root.attr('style', 'display: inline-block');\n", + "\n", + " $(parent_element).append(this.root);\n", + "\n", + " this._init_header(this);\n", + " this._init_canvas(this);\n", + " this._init_toolbar(this);\n", + "\n", + " var fig = this;\n", + "\n", + " this.waiting = false;\n", + "\n", + " this.ws.onopen = function () {\n", + " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", + " fig.send_message(\"send_image_mode\", {});\n", + " if (mpl.ratio != 1) {\n", + " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", + " }\n", + " fig.send_message(\"refresh\", {});\n", + " }\n", + "\n", + " this.imageObj.onload = function() {\n", + " if (fig.image_mode == 'full') {\n", + " // Full images could contain transparency (where diff images\n", + " // almost always do), so we need to clear the canvas so that\n", + " // there is no ghosting.\n", + " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", + " }\n", + " fig.context.drawImage(fig.imageObj, 0, 0);\n", + " };\n", + "\n", + " this.imageObj.onunload = function() {\n", + " fig.ws.close();\n", + " }\n", + "\n", + " this.ws.onmessage = this._make_on_message_function(this);\n", + "\n", + " this.ondownload = ondownload;\n", + "}\n", + "\n", + "mpl.figure.prototype._init_header = function() {\n", + " var titlebar = $(\n", + " '
');\n", + " var titletext = $(\n", + " '
');\n", + " titlebar.append(titletext)\n", + " this.root.append(titlebar);\n", + " this.header = titletext[0];\n", + "}\n", + "\n", + "\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._init_canvas = function() {\n", + " var fig = this;\n", + "\n", + " var canvas_div = $('
');\n", + "\n", + " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", + "\n", + " function canvas_keyboard_event(event) {\n", + " return fig.key_event(event, event['data']);\n", + " }\n", + "\n", + " canvas_div.keydown('key_press', canvas_keyboard_event);\n", + " canvas_div.keyup('key_release', canvas_keyboard_event);\n", + " this.canvas_div = canvas_div\n", + " this._canvas_extra_style(canvas_div)\n", + " this.root.append(canvas_div);\n", + "\n", + " var canvas = $('');\n", + " canvas.addClass('mpl-canvas');\n", + " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", + "\n", + " this.canvas = canvas[0];\n", + " this.context = canvas[0].getContext(\"2d\");\n", + "\n", + " var backingStore = this.context.backingStorePixelRatio ||\n", + "\tthis.context.webkitBackingStorePixelRatio ||\n", + "\tthis.context.mozBackingStorePixelRatio ||\n", + "\tthis.context.msBackingStorePixelRatio ||\n", + "\tthis.context.oBackingStorePixelRatio ||\n", + "\tthis.context.backingStorePixelRatio || 1;\n", + "\n", + " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", + "\n", + " var rubberband = $('');\n", + " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", + "\n", + " var pass_mouse_events = true;\n", + "\n", + " canvas_div.resizable({\n", + " start: function(event, ui) {\n", + " pass_mouse_events = false;\n", + " },\n", + " resize: function(event, ui) {\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " stop: function(event, ui) {\n", + " pass_mouse_events = true;\n", + " fig.request_resize(ui.size.width, ui.size.height);\n", + " },\n", + " });\n", + "\n", + " function mouse_event_fn(event) {\n", + " if (pass_mouse_events)\n", + " return fig.mouse_event(event, event['data']);\n", + " }\n", + "\n", + " rubberband.mousedown('button_press', mouse_event_fn);\n", + " rubberband.mouseup('button_release', mouse_event_fn);\n", + " // Throttle sequential mouse events to 1 every 20ms.\n", + " rubberband.mousemove('motion_notify', mouse_event_fn);\n", + "\n", + " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", + " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", + "\n", + " canvas_div.on(\"wheel\", function (event) {\n", + " event = event.originalEvent;\n", + " event['data'] = 'scroll'\n", + " if (event.deltaY < 0) {\n", + " event.step = 1;\n", + " } else {\n", + " event.step = -1;\n", + " }\n", + " mouse_event_fn(event);\n", + " });\n", + "\n", + " canvas_div.append(canvas);\n", + " canvas_div.append(rubberband);\n", + "\n", + " this.rubberband = rubberband;\n", + " this.rubberband_canvas = rubberband[0];\n", + " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", + " this.rubberband_context.strokeStyle = \"#000000\";\n", + "\n", + " this._resize_canvas = function(width, height) {\n", + " // Keep the size of the canvas, canvas container, and rubber band\n", + " // canvas in synch.\n", + " canvas_div.css('width', width)\n", + " canvas_div.css('height', height)\n", + "\n", + " canvas.attr('width', width * mpl.ratio);\n", + " canvas.attr('height', height * mpl.ratio);\n", + " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", + "\n", + " rubberband.attr('width', width);\n", + " rubberband.attr('height', height);\n", + " }\n", + "\n", + " // Set the figure to an initial 600x600px, this will subsequently be updated\n", + " // upon first draw.\n", + " this._resize_canvas(600, 600);\n", + "\n", + " // Disable right mouse context menu.\n", + " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", + " return false;\n", + " });\n", + "\n", + " function set_focus () {\n", + " canvas.focus();\n", + " canvas_div.focus();\n", + " }\n", + "\n", + " window.setTimeout(set_focus, 100);\n", + "}\n", + "\n", + "mpl.figure.prototype._init_toolbar = function() {\n", + " var fig = this;\n", + "\n", + " var nav_element = $('
');\n", + " nav_element.attr('style', 'width: 100%');\n", + " this.root.append(nav_element);\n", + "\n", + " // Define a callback function for later on.\n", + " function toolbar_event(event) {\n", + " return fig.toolbar_button_onclick(event['data']);\n", + " }\n", + " function toolbar_mouse_event(event) {\n", + " return fig.toolbar_button_onmouseover(event['data']);\n", + " }\n", + "\n", + " for(var toolbar_ind in mpl.toolbar_items) {\n", + " var name = mpl.toolbar_items[toolbar_ind][0];\n", + " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", + " var image = mpl.toolbar_items[toolbar_ind][2];\n", + " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", + "\n", + " if (!name) {\n", + " // put a spacer in here.\n", + " continue;\n", + " }\n", + " var button = $('');\n", + " button.click(method_name, toolbar_event);\n", + " button.mouseover(tooltip, toolbar_mouse_event);\n", + " nav_element.append(button);\n", + " }\n", + "\n", + " // Add the status bar.\n", + " var status_bar = $('');\n", + " nav_element.append(status_bar);\n", + " this.message = status_bar[0];\n", + "\n", + " // Add the close button to the window.\n", + " var buttongrp = $('
');\n", + " var button = $('');\n", + " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", + " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", + " buttongrp.append(button);\n", + " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", + " titlebar.prepend(buttongrp);\n", + "}\n", + "\n", + "mpl.figure.prototype._root_extra_style = function(el){\n", + " var fig = this\n", + " el.on(\"remove\", function(){\n", + "\tfig.close_ws(fig, {});\n", + " });\n", + "}\n", + "\n", + "mpl.figure.prototype._canvas_extra_style = function(el){\n", + " // this is important to make the div 'focusable\n", + " el.attr('tabindex', 0)\n", + " // reach out to IPython and tell the keyboard manager to turn it's self\n", + " // off when our div gets focus\n", + "\n", + " // location in version 3\n", + " if (IPython.notebook.keyboard_manager) {\n", + " IPython.notebook.keyboard_manager.register_events(el);\n", + " }\n", + " else {\n", + " // location in version 2\n", + " IPython.keyboard_manager.register_events(el);\n", + " }\n", + "\n", + "}\n", + "\n", + "mpl.figure.prototype._key_event_extra = function(event, name) {\n", + " var manager = IPython.notebook.keyboard_manager;\n", + " if (!manager)\n", + " manager = IPython.keyboard_manager;\n", + "\n", + " // Check for shift+enter\n", + " if (event.shiftKey && event.which == 13) {\n", + " this.canvas_div.blur();\n", + " // select the cell after this one\n", + " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", + " IPython.notebook.select(index + 1);\n", + " }\n", + "}\n", + "\n", + "mpl.figure.prototype.handle_save = function(fig, msg) {\n", + " fig.ondownload(fig, null);\n", + "}\n", + "\n", + "\n", + "mpl.find_output_cell = function(html_output) {\n", + " // Return the cell and output element which can be found *uniquely* in the notebook.\n", + " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", + " // IPython event is triggered only after the cells have been serialised, which for\n", + " // our purposes (turning an active figure into a static one), is too late.\n", + " var cells = IPython.notebook.get_cells();\n", + " var ncells = cells.length;\n", + " for (var i=0; i= 3 moved mimebundle to data attribute of output\n", + " data = data.data;\n", + " }\n", + " if (data['text/html'] == html_output) {\n", + " return [cell, data, j];\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "// Register the function which deals with the matplotlib target/channel.\n", + "// The kernel may be null if the page has been refreshed.\n", + "if (IPython.notebook.kernel != null) {\n", + " IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);\n", + "}\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Minima not found within specified number of itterations.\n", + "Minima found! Norm of gradient was 5.444089623551918e-09 and x is given by [1.58113851]\n" + ] + } + ], + "source": [ + "name_list = []\n", + "f_list = []\n", + "x0_list = []\n", + "\n", + "name_list.append(r\"$sin(||x||) + sin(x_0)$\")\n", + "f_list.append(lambda x: np.sin(np.linalg.norm(x)) + np.sin(x[0]))\n", + "x0_list.append(np.array([2.5, -1]))\n", + "\n", + "name_list.append(r\"$||x||^2 sin(\\sqrt{x_0^2 + 0.5 * x_1^2})$\")\n", + "f_list.append(lambda x: np.linalg.norm(x)**2 * np.sin(np.sqrt(x[0]**2 + 0.5 * x[1]**2)))\n", + "x0_list.append(np.array([2, 2]))\n", + "\n", + "name_list.append(r\"$x^4 - 5 * x^2 + 2.5$\")\n", + "f_list.append(lambda x: x**4 - 5 * x**2 + 2.5)\n", + "x0_list.append(np.array([1]))\n", + "\n", + "for f, f_name, x0 in zip(f_list, name_list, x0_list):\n", + " test_function(f, f_name, x0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Discussion\n", + "\n", + "Both methods converge in all cases, but generally the Newton method converges faster. I the fist case the mathods converge to different points since the newton method can find any minima/maxima while the gradient decent only finds local minima och sadel points. It is also intresting to note that the gradient decent often converges quickly, but often never acctually gets inside the tolerance." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "leoenge_lab6.ipynb", + "provenance": [] + }, + "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.8.1" + }, + "pycharm": { + "stem_cell": { + "cell_type": "raw", + "metadata": { + "collapsed": false + }, + "source": [] + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +}