{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Projections "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This notebook gives an introduction on simple objects in *SupPy* and how they can be projected onto. In general all objects derive from the `Projection` class and have a `project` function that, for a given point `x`, calculates the projection onto the object.\n",
    "In this notebook the following objects/classes are explored that can all be imported from the `suppy.projections` module.\n",
    "1. Halfspaces $(\\langle a,x \\rangle \\leq b )$ `HalfspaceProjection`\n",
    "2. Bands/Hyperslabs $( lb \\leq \\langle a,x \\rangle \\leq ub )$ `HyperslabProjection`\n",
    "3. Spheres/Balls `Ball Projection`\n",
    "4. Box constraints $( lb \\leq x \\leq ub )$  `BoxProjection`\n",
    "4. Dose Volume Histogram (DVH) projections `MinDVHProjection` and `MaxDVHProjection`\n",
    "5. Subgradient projections $(f(x) \\leq f_{max})$ `SubgradientProjection`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "#standard imports\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## General terminology\n",
    "Projection objects can be simply instantialized by importing the corresponding object from the `suppy.projections` module.\n",
    "\n",
    "Once a Projection (e.g. `Ball_1`) has been instantialized we can perform a projection from point `x` onto it by simply calling `x = Ball_1.project(x)`.\n",
    "\n",
    "This will, where possible, modify the `x` vector in place.\n",
    "\n",
    "For simple 2D objects it is also possible to get a 2d visualization. E.g for the ball: `Ball_1.visualize(ax)` with `ax` being a `matplotlib.pyplot.axis` object."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Projection onto a simple halfspace"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import HalfspaceProjection"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Projection of a point $x$ onto the halfspace given by $\\langle a,x \\rangle \\leq b$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 14500 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Halfspace defined through <a, x>  <= b\n",
    "# Define the normal vector a\n",
    "a = np.array([1,1])\n",
    "b = 0\n",
    "Halfspace = HalfspaceProjection(a, b)\n",
    "x = np.array([1,1],dtype = float)\n",
    "# Define the point to project\n",
    "x = np.array([1,1],dtype = float)\n",
    "fig,ax = plt.subplots()\n",
    "ax.scatter(*x,color = 'xkcd:blue')\n",
    "\n",
    "proj = Halfspace.project(x)\n",
    "\n",
    "ax.scatter(*proj,color = 'xkcd:red')\n",
    "# Visualize the halfspace\n",
    "xlin = np.linspace(-2, 2, 100)\n",
    "Halfspace.visualize(ax,xlin)\n",
    "# Set the x and y axis labels\n",
    "plt.xlabel('x')\n",
    "plt.ylabel('y')\n",
    "# Show the plot\n",
    "plt.axis('equal')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Projection onto a simple ball"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Orthogonal projection of a point $\\mathbf{x}$ onto a circle/2D ball with center $\\mathbf{c}$ and radius $r$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import BallProjection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 28560 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "c = np.array([1,1])\n",
    "r = 1\n",
    "Ball = BallProjection(c, r)\n",
    "\n",
    "# Define the point to project\n",
    "x = np.array([-2,-2],dtype = float)\n",
    "fig, ax = plt.subplots(figsize=(6,6))\n",
    "\n",
    "plt.scatter(*x, color='xkcd:blue', label='Point to project')\n",
    "plt.scatter(*c, color='xkcd:black', label='Center of the circle')\n",
    "plt.plot([c[0], x[0]], [c[1], x[1]], color='xkcd:orange', label='Connecting line')\n",
    "proj = Ball.project(x)\n",
    "plt.scatter(*proj, color='xkcd:red', label='Projected point')\n",
    "\n",
    "# Visualize the circle\n",
    "Ball.visualize(ax)\n",
    "\n",
    "# circle_1 = plt.Circle(c, r, color='beige', alpha=0.5)\n",
    "# ax.add_patch(circle_1)\n",
    "ax.set_aspect('equal')\n",
    "\n",
    "# Add legend\n",
    "plt.legend()\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Projection onto a band\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Projecting the point $\\mathbf{x}$ onto the single band given by $lb \\leq \\langle a,x \\rangle \\leq ub $"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import BandProjection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 9876 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "a = np.array([-1,-1])\n",
    "lb = -1\n",
    "ub = 1\n",
    "x_i  = np.linspace(-2,2,100)\n",
    "\n",
    "# y1 = (lb-a[0]*x_i)/a[1]\n",
    "# y2 = (ub-a[0]*x_i)/a[1]\n",
    "\n",
    "# plt.plot(x_i,y1, color = 'black')\n",
    "# plt.plot(x_i,y2, color = 'black')\n",
    "# plt.fill_between(x_i, y1, y2, color='beige',label = 'Band',alpha = 0.5)\n",
    "fig,ax = plt.subplots()\n",
    "Band_proj = BandProjection(a, lb, ub)\n",
    "\n",
    "\n",
    "Band_proj.visualize(ax,x_i)\n",
    "\n",
    "x = np.array([2,2],dtype=float)\n",
    "plt.scatter(*x,color = 'xkcd:blue')\n",
    "proj = Band_proj.project(x)\n",
    "plt.scatter(*proj,color = 'xkcd:red')\n",
    "#plt.xlim(-2,2)\n",
    "#plt.ylim(-2,2)\n",
    "\n",
    "ax.set_aspect('equal')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Box constraint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import BoxProjection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "lb = np.array([0,0])\n",
    "ub = np.array([1,1])\n",
    "Box_proj = BoxProjection(lb, ub)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 18848 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Create a figure and axis\n",
    "fig, ax = plt.subplots()\n",
    "\n",
    "Box_proj.visualize(ax)\n",
    "\n",
    "x = np.array([2,2.5],dtype = float)\n",
    "plt.scatter(*x, color='xkcd:blue', label='Point to project')\n",
    "proj = Box_proj.project(x)\n",
    "plt.scatter(*proj, color='xkcd:red', label='Projected point')\n",
    "ax.set_xlim(-0.5,3)\n",
    "ax.set_ylim(-0.5,3)\n",
    "plt.legend()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Subgradient projections"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Besides the orthogonal projections we can also use subgradient projections to project onto sublevel sets $\\{\\mathbf{x} | f(\\mathbf{x})\\leq \\alpha \\}$.\n",
    "Given that $f(\\mathbf{x} )$ is a convex function and has a computable subgradient, the projection is given by:\n",
    "\\begin{equation}\n",
    "P_{f,\\alpha}(x) = x - \\frac{(f(x)-\\alpha)_{+}}{||{\\mathbf{g}(x)||^2}} \\mathbf{g}(x)\n",
    "\\end{equation}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Projection onto a circle using subgradient projections"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To demonstrate the method we want to project a point on a circle expressed through a sublevel set $\\{\\mathbf{x} | f(\\mathbf{x})\\leq r \\}$ where $r$ is the radius of the circle and $f(\\mathbf{x})$ the distance of point $\\mathbf{x}$ to the center. Therefore for function and gradient it holds that:\n",
    "\n",
    "\\begin{align}\n",
    "f(\\mathbf{x}) & = |\\mathbf{x}-\\mathbf{c}| \\\\\n",
    "g(\\mathbf{x}) & = \\frac{\\mathbf{x}-\\mathbf{c}}{|\\mathbf{x}-\\mathbf{c}|}\\\\\n",
    "\\end{align}\n",
    "\n",
    "And the projection has the form \n",
    "\\begin{align*}\n",
    "P_{f,\\alpha}(x) & = \\mathbf{x} - \\frac{(f(\\mathbf{x})-r)_{+}}{||{\\mathbf{g}(\\mathbf{x})||^2}} \\mathbf{g}(\\mathbf{x}) \\\\\n",
    "                & = \\mathbf{x} - \\frac{(|\\mathbf{x}-\\mathbf{c}| -r)_{+}}{1}\\frac{\\mathbf{x}-\\mathbf{c}}{|\\mathbf{x}-\\mathbf{c}|}\n",
    "\\end{align*}\n",
    "which can be reformulated to the orthogonal projection onto a circle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import SubgradientProjection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 28220 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def func(x,c):\n",
    "    return np.linalg.norm(x-c)\n",
    "\n",
    "def grad(x,c):\n",
    "    return (x-c)/np.linalg.norm(x-c)\n",
    "\n",
    "# Create the Projection\n",
    "c = np.array([1,1])\n",
    "r = 1\n",
    "func_args = [c]\n",
    "grad_args = [c]\n",
    "\n",
    "Circle_Subgradient = SubgradientProjection(func,grad,level = r,func_args = func_args,grad_args = grad_args)\n",
    "x = np.array([-2,-2],dtype = float)\n",
    "\n",
    "\n",
    "fig,ax = plt.subplots()\n",
    "\n",
    "plt.scatter(*x,color = 'xkcd:blue',label = 'Point to project')\n",
    "plt.scatter(*c,color = 'xkcd:black',label = 'Center of the circle')\n",
    "\n",
    "plt.plot([c[0],x[0]],[c[1],x[1]],color = 'xkcd:orange', label = 'Connecting line')\n",
    "\n",
    "proj = Circle_Subgradient.project(x)\n",
    "\n",
    "plt.scatter(*proj,color = 'xkcd:red',label = 'Projected point')\n",
    "\n",
    "# Visualize the circle\n",
    "circle_1 = plt.Circle(c,r, color='beige',alpha = 0.5)\n",
    "ax.add_patch(circle_1)\n",
    "ax.set_aspect('equal')\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### EUD"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another example for the subgradient projection is the in Radiotherapy used Equivalent Uniform Dose (EUD) \n",
    "\\begin{align*}\n",
    "f(\\mathbf{d}) & = \\left(\\frac{1}{N} \\sum^{N}_{i} {d_i^a}  \\right)^{1/a}\\\\\n",
    "g(\\mathbf{d}) & = \\left(\\frac{1}{N}\\right)^{1/a} \\left( \\sum^{N}_{i} {d_i^a} \\right)^{1/a-1} \\mathbf{d}^{a-1}\\\\\n",
    "\\end{align*}\n",
    "with $\\mathbf{d}\\geq0$ and $a$ a parameter depending on the structure,"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Visualization of the general profil for a 2D dose vector:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "from suppy.projections import EUDProjection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Text(0.5, 1.0, 'EUD with a = 10')"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 26952 characters]",
      "text/plain": [
       "<Figure size 640x480 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "EUD_1 = EUDProjection(1)\n",
    "EUD_2 = EUDProjection(2)\n",
    "EUD_3 = EUDProjection(3)\n",
    "EUD_10 = EUDProjection(10)\n",
    "\n",
    "fig,ax = plt.subplots(2,2)\n",
    "for i in range(2):\n",
    "    for j in range(2):\n",
    "        ax[i,j].axis('equal')\n",
    "        ax[i,j].set_xlabel('d_1')\n",
    "        ax[i,j].set_ylabel('d_2')\n",
    "\n",
    "xlin = np.linspace(0, 5, 1000)\n",
    "ylin = np.linspace(0, 5, 1000)      \n",
    "X, Y = np.meshgrid(xlin, ylin)\n",
    "XY = np.array([X,Y])\n",
    "\n",
    "Z1 = EUD_1._func(XY)\n",
    "Z2 = EUD_2._func(XY)\n",
    "Z3 = EUD_3._func(XY)\n",
    "Z10 = EUD_10._func(XY)\n",
    "\n",
    "ax[0,0].contourf(X,Y,Z1,levels = 10)\n",
    "ax[0,0].set_title('EUD with p = 1')\n",
    "\n",
    "ax[0,1].contourf(X,Y,Z2,levels = 10)\n",
    "ax[0,1].set_title('EUD with a = 2')\n",
    "\n",
    "ax[1,0].contourf(X,Y,Z3,levels = 10)\n",
    "ax[1,0].set_title('EUD with a = 3')\n",
    "\n",
    "ax[1,1].contourf(X,Y,Z10,levels = 10)\n",
    "ax[1,1].set_title('EUD with a = 10')\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Projection onto the sublevel set of the EUD with $EUD\\leq 0.75$:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.legend.Legend at 0x2380c7f1b50>"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 29984 characters]",
      "text/plain": [
       "<Figure size 640x480 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "\n",
    "EUD_2 = EUDProjection(2,0.75)\n",
    "EUD_4 = EUDProjection(4,0.75)\n",
    "\n",
    "# plot contours of the EUD\n",
    "fig,ax = plt.subplots(1,2)\n",
    "\n",
    "for i in range(2):\n",
    "    ax[i].set_xlabel('d$_1$')\n",
    "    ax[i].set_ylabel('d$_2$')\n",
    "\n",
    "xlin = np.linspace(0, 2, 1000)\n",
    "ylin = np.linspace(0, 2, 1000)\n",
    "X, Y = np.meshgrid(xlin, ylin)\n",
    "XY = np.array([X,Y]) #transform into array for function evaluation\n",
    "\n",
    "\n",
    "#EUD with a = 2\n",
    "Z_2 = EUD_2._func(XY)\n",
    "\n",
    "ax[0].contourf(X,Y,Z_2,levels = [np.min(Z_2),EUD_2.level],colors = ['beige','white'])\n",
    "ax[0].set_title('EUD with a = 2')\n",
    "\n",
    "x = np.array([2,4],dtype = float)\n",
    "ax[0].scatter(*x,color = 'xkcd:blue',label = 'Point to project')\n",
    "proj = EUD_2.project(x)\n",
    "ax[0].scatter(*proj,color = 'xkcd:red',label = 'First projection')\n",
    "proj = EUD_2.project(proj)\n",
    "ax[0].scatter(*proj,color = 'xkcd:green',label = 'Second projection')\n",
    "\n",
    "\n",
    "#EUD with a = 4\n",
    "Z_4 = EUD_4._func(XY)\n",
    "ax[1].contourf(X,Y,Z_4,levels = [np.min(Z_4),EUD_4.level],colors = ['beige','white'])\n",
    "ax[1].set_title('EUD with a = 4')\n",
    "\n",
    "x = np.array([2,4],dtype = float)\n",
    "ax[1].scatter(*x,color = 'xkcd:blue',label = 'Point to project')\n",
    "proj = EUD_4.project(x)\n",
    "ax[1].scatter(*proj,color = 'xkcd:red',label = 'First projection')\n",
    "proj = EUD_4.project(proj)\n",
    "ax[1].scatter(*proj,color = 'xkcd:green',label = 'Second projection')\n",
    "\n",
    "proj = EUD_4.project(proj)\n",
    "ax[1].scatter(*proj,color = 'xkcd:purple',label = 'Third projection')\n",
    "plt.legend(loc = 'upper left')\n",
    "#ax[0].set_aspect('equal')\n",
    "#ax[1].set_aspect('equal')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  DVH Projections"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## TBD"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "suppy",
   "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.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
