{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sascha Spors,\n",
    "Professorship Signal Theory and Digital Signal Processing,\n",
    "Institute of Communications Engineering (INT),\n",
    "Faculty of Computer Science and Electrical Engineering (IEF),\n",
    "University of Rostock,\n",
    "Germany\n",
    "\n",
    "# Data Driven Audio Signal Processing - A Tutorial with Computational Examples\n",
    "\n",
    "Winter Semester 2023/24 (Master Course #24512)\n",
    "\n",
    "- lecture: https://github.com/spatialaudio/data-driven-audio-signal-processing-lecture\n",
    "- tutorial: https://github.com/spatialaudio/data-driven-audio-signal-processing-exercise\n",
    "\n",
    "Feel free to contact lecturer frank.schultz@uni-rostock.de"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exercise 4: SVD and Right Matrix Inverse\n",
    "\n",
    "## Objectives\n",
    "\n",
    "- Matrix A of dimension (M x N)\n",
    "- SVD for a **flat/fat** matrix, we assume a matrix with **full row rank** $r=M$\n",
    "- Four subspaces in SVD domain\n",
    "- Projection matrices\n",
    "- Right inverse\n",
    "\n",
    "## Special Python Packages\n",
    "\n",
    "Some convenient functions are found in `scipy.linalg`, some in `numpy.linalg` "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Some Initial Python Stuff"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from scipy.linalg import svd, diagsvd, inv, pinv, null_space, norm\n",
    "from numpy.linalg import matrix_rank\n",
    "\n",
    "np.set_printoptions(precision=2, floatmode=\"fixed\", suppress=True)\n",
    "\n",
    "rng = np.random.default_rng(1234)\n",
    "mean, stdev = 0, 1\n",
    "\n",
    "# we might convince ourselves that all works for complex data as well\n",
    "# then the ^H operator (conj().T) needs to be used instead of just .T\n",
    "use_complex = False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## SVD of Flat/Fat, Full Row Rank Matrix A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "M = 3  # number of rows\n",
    "N = 7  # number of cols\n",
    "\n",
    "rank = min(M, N)  # set desired rank == full row rank == independent columns\n",
    "print(\"desired rank of A:\", rank)\n",
    "\n",
    "if use_complex:\n",
    "    dtype = \"complex128\"\n",
    "    A = np.zeros([M, N], dtype=dtype)\n",
    "    for i in range(rank):\n",
    "        col = rng.normal(mean, stdev, M) + 1j * rng.normal(mean, stdev, M)\n",
    "        row = rng.normal(mean, stdev, N) + 1j * rng.normal(mean, stdev, N)\n",
    "        A += np.outer(col, row)  # superposition of rank-1 matrices\n",
    "else:\n",
    "    dtype = \"float64\"\n",
    "    A = np.zeros([M, N], dtype=dtype)\n",
    "    for i in range(rank):\n",
    "        col = rng.normal(mean, stdev, M)\n",
    "        row = rng.normal(mean, stdev, N)\n",
    "        A += np.outer(col, row)  # superposition of rank-1 matrices\n",
    "# check if rng produced desired rank\n",
    "print(\"        rank of A:\", matrix_rank(A))\n",
    "print(\"flat/fat matrix with full row rank\")\n",
    "print(\"-> matrix U contains only the column space\")\n",
    "print(\"-> left null space is only the zero vector\")\n",
    "print(\"A =\\n\", A)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "[U, s, Vh] = svd(A, full_matrices=True)\n",
    "S = diagsvd(s, M, N)  # for full SVD the matrix S has same dim as A\n",
    "V = Vh.conj().T\n",
    "Uh = U.conj().T\n",
    "\n",
    "print(\"U =\\n\", U)\n",
    "# number of non-zero sing values along diag must match rank\n",
    "print(\"non-zero singular values: \", s[:rank])\n",
    "print(\"S =\\n\", S)  # contains 0 Matrix right of diagonal part\n",
    "print(\"V =\\n\", V)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Four Subspaces in SVD Domain"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# all stuff that is in matrix U\n",
    "print(\"U =\\n\", U)\n",
    "\n",
    "# column space C(A)\n",
    "print(\"\\ncolumn space (orthogonal to left null space):\")\n",
    "print(U[:, :rank])\n",
    "\n",
    "# left null space, if empty only 0 vector\n",
    "print(\"left null space (orthogonal to column space):\")\n",
    "print(U[:, rank:])  # for full row rank this is only the zero vector\n",
    "\n",
    "print(\"###\")\n",
    "\n",
    "# all stuff that is in matrix V\n",
    "print(\"\\nV =\\n\", V)\n",
    "\n",
    "# row space\n",
    "print(\"\\nrow space (orthogonal to null space):\")\n",
    "print(V[:, :rank])\n",
    "\n",
    "# null space N(A), if empty only 0 vector\n",
    "print(\"null space (orthogonal to row space):\")\n",
    "print(V[:, rank:])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Right Inverse via SVD"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "[U, s, Vh] = svd(A, full_matrices=True)\n",
    "V = Vh.conj().T\n",
    "Uh = U.conj().T\n",
    "\n",
    "Si = diagsvd(1 / s, N, M)  # works if array s has only non-zero entries\n",
    "print(\"Inverse singular value matrix with bottom zero block\")\n",
    "print(\"Si =\\n\", Si)\n",
    "# right inverse using 'inverse' SVD:\n",
    "Ari = V @ Si @ U.conj().T\n",
    "# right inverse using a dedicated pinv algorithm\n",
    "# proper choice is done by pinv() itself\n",
    "Ari_pinv = pinv(A)\n",
    "print(\"pinv() == right inverse via SVD?\", np.allclose(Ari, Ari_pinv))\n",
    "print(\"S @ Si = \\n\", S @ Si, \"\\nyields MxM identity matrix\")\n",
    "print(\"A @ Ari = \\n\", A @ Ari, \"\\nyields MxM identity matrix\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Projection Matrices for the Right Inverse Problem"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "v_row = 1 * V[:, 0, None]\n",
    "v_null = 2 * V[:, rank, None]\n",
    "v_tmp = v_row + v_null\n",
    "\n",
    "# projection onto row space\n",
    "P_CAH = Ari @ A\n",
    "print(\n",
    "    \"projection matrix P_CAH projects V-space stuff to row space:\\n\",\n",
    "    \"P_CAH @ v_tmp == v_row:\",\n",
    "    np.allclose(P_CAH @ v_tmp, v_row),\n",
    ")\n",
    "\n",
    "# projection onto column space\n",
    "# full rank and identity since we don't have a left null space\n",
    "# so each column space vector is projected onto itself\n",
    "P_CA = A @ Ari  # = I_MxM\n",
    "print(\n",
    "    \"projection matrix P_CA projects a column space vector onto itself:\\n\",\n",
    "    \"P_CA @ U[:, 0] == U[:, 0]:\",\n",
    "    np.allclose(P_CA @ U[:, 0], U[:, 0]),\n",
    ")\n",
    "\n",
    "# projection onto null space, for flat/fat this space might be very large\n",
    "P_NA = np.eye(N, N) - P_CAH\n",
    "print(\n",
    "    \"projection matrix P_NA projects V-space stuff to null space:\\n\",\n",
    "    \"P_NA @ v_tmp == v_null:\",\n",
    "    np.allclose(P_NA @ v_tmp, v_null),\n",
    ")\n",
    "\n",
    "# projection onto left null space\n",
    "P_NAH = np.eye(M, M) - P_CA  # == null matrix\n",
    "print(\"projection matrix P_NAH is the MxM zero matrix\\n\", P_NAH)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Copyright\n",
    "\n",
    "- the notebooks are provided as [Open Educational Resources](https://en.wikipedia.org/wiki/Open_educational_resources)\n",
    "- the text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/)\n",
    "- the code of the IPython examples is licensed under the [MIT license](https://opensource.org/licenses/MIT)\n",
    "- feel free to use the notebooks for your own purposes\n",
    "- please attribute the work as follows: *Frank Schultz, Data Driven Audio Signal Processing - A Tutorial Featuring Computational Examples, University of Rostock* ideally with relevant file(s), github URL https://github.com/spatialaudio/data-driven-audio-signal-processing-exercise, commit number and/or version tag, year."
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "1743232f157dd0954c61aae30535e75a2972519a625c7e796bafe0cd9a07bf7e"
  },
  "kernelspec": {
   "display_name": "myddasp",
   "language": "python",
   "name": "myddasp"
  },
  "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.10.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}