{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Physical simulation of the polyester rope mounting structure\n",
    "\n",
    "This file contains a simulation of an idealized model of the polyester rope mounting structure to be used to mount the 8seg display. The rope is simulated as ideal elastic hooke springs using parameters from a rope manufacturer's website, and care is taken to keep all ropes below ~20% their breaking strength to keep reality close enough to the model. For simplicity mass is simply simulated by every knot/node having a certain mass."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import scipy.optimize as opt\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "%matplotlib notebook"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Global parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "g = 9.81  # earth gravity constant [m/s^2]\n",
    "m = 10/146 # node mass [kg]\n",
    "e = 2  # initial distance between the masses\n",
    "l = 1.5  # relaxed length of the springs\n",
    "\n",
    "# data for 3mm cord\n",
    "mbreak = 298 # [kg]\n",
    "elongation_20percent_break = 0.03 # [1]\n",
    "\n",
    "Fbreak = mbreak * g\n",
    "stiffness_3mm = (Fbreak*0.2) / elongation_20percent_break  # spring stiffness\n",
    "\n",
    "# data for 4mm cord\n",
    "mbreak = 490 # [kg]\n",
    "elongation_20percent_break = 0.03 # [1]\n",
    "\n",
    "Fbreak = mbreak * g\n",
    "stiffness_4mm = (Fbreak*0.2) / elongation_20percent_break  # spring stiffness\n",
    "\n",
    "stiffness_thick = stiffness_4mm\n",
    "stiffness_thin = stiffness_3mm\n",
    "\n",
    "overall_width = 62 # [m]\n",
    "parabola_height_top = 1.0 # [m]\n",
    "parabola_height_bottom = parabola_height_top\n",
    "digit_height = 2.0 # [m]\n",
    "parabola_offset = 0.4 # [m]\n",
    "\n",
    "anchor_offset_y_top = digit_height/2\n",
    "anchor_offset_y_bottom = -digit_height/2\n",
    "parabola_offset_y_top = anchor_offset_y_top + parabola_offset\n",
    "parabola_offset_y_bottom = anchor_offset_y_bottom - parabola_offset\n",
    "\n",
    "pad_digits = 5 # pad empty digits on each side\n",
    "bracing_pad = 2\n",
    "digits = 25 + pad_digits*2\n",
    "points_top_parabola = digits + 2 # pad 2 for tree anchors\n",
    "points_top_anchor = digits + 1 # pad 1 for off-by-one\n",
    "points_bottom_parabola = points_top_parabola\n",
    "points_bottom_anchor = points_top_anchor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Node positions, overall layout"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "P_top_parabola = np.zeros((points_top_parabola, 2))\n",
    "P_top_anchor = np.zeros((points_top_anchor, 2))\n",
    "\n",
    "P_top_parabola[:,0] = np.linspace(-overall_width/2, overall_width/2, points_top_parabola)\n",
    "parabola_coeff_top = parabola_height_top/((overall_width/2)**2)\n",
    "P_top_parabola[:,1] = parabola_offset_y_top + parabola_coeff_top*(P_top_parabola[:,0]**2)\n",
    "\n",
    "P_top_anchor[:,0] = np.linspace(-overall_width/2, overall_width/2, points_top_anchor)\n",
    "P_top_anchor[:,1] = anchor_offset_y_top\n",
    "\n",
    "P_bottom_parabola = np.zeros((points_bottom_parabola, 2))\n",
    "P_bottom_anchor = np.zeros((points_bottom_anchor, 2))\n",
    "\n",
    "P_bottom_parabola[:,0] = np.linspace(-overall_width/2, overall_width/2, points_bottom_parabola)\n",
    "parabola_coeff_bottom = parabola_height_bottom/((overall_width/2)**2)\n",
    "P_bottom_parabola[:,1] = parabola_offset_y_bottom - parabola_coeff_bottom*(P_bottom_parabola[:,0]**2)\n",
    "\n",
    "P_bottom_anchor[:,0] = np.linspace(-overall_width/2, overall_width/2, points_bottom_anchor)\n",
    "P_bottom_anchor[:,1] = anchor_offset_y_bottom\n",
    "\n",
    "P0 = np.vstack([P_top_parabola, P_top_anchor, P_bottom_parabola, P_bottom_anchor])\n",
    "\n",
    "#P0[:, 0] = np.repeat(e * np.arange(n // 2), 2)\n",
    "#P0[:, 1] = np.tile((0, -e), n // 2)\n",
    "#P0[-2:, 1] = [-0.5, -0.5-e]\n",
    "n = len(P0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Some scaling parameters to put tension on the ropes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "anchor_xbrance_len = 0.99\n",
    "par_len_top = 0.98\n",
    "par_len_bottom = 0.98\n",
    "anchor_len = 0.98\n",
    "xbrace_len = 0.96\n",
    "overall_len_factor = 1.01"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Connectivity matrix A, edge length matrix L and stiffness matrix K\n",
    "\n",
    "Since this system is rather simple, we're simply using sparse connectivity matrices for everything holding two elements for every node pair."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "podoc": {
     "output_text": "<matplotlib.figure.Figure at 0x7f93deeab668>"
    },
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.7058364644977657 (36,) (37, 37)\n",
      "1.75336 (35,) (36, 36)\n",
      "1.1141728717151573 (62,) (36, 36)\n",
      "1.7058364644977657 (36,) (37, 37)\n",
      "1.75336 (35,) (36, 36)\n",
      "1.1141728717151573 (62,) (36, 36)\n",
      "2.671428768134445 (62,) (36, 36)\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 5656 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "p = bracing_pad\n",
    "\n",
    "A = np.zeros((n, n))\n",
    "#np.eye(n, n, 1) + np.eye(n, n, 2)\n",
    "i, j = points_top_parabola, points_top_parabola+points_top_anchor\n",
    "k, l = j+points_bottom_parabola, j+points_bottom_parabola+points_bottom_anchor\n",
    "\n",
    "## Top\n",
    "# parabola\n",
    "A[:i, :i] = np.eye(points_top_parabola, points_top_parabola, 1)# * par_len_top\n",
    "# anchor\n",
    "A[i:j, i:j] = np.eye(points_top_anchor, points_top_anchor, 1)# * anchor_len\n",
    "# cross-bracing\n",
    "A[1+p:i-p, i+p:j-p] = np.eye(points_top_anchor-2*p, points_top_anchor-2*p, 0)# * xbrace_len\n",
    "A[1+p:i-p, i+p:j-p] += np.eye(points_top_anchor-2*p, points_top_anchor-2*p, 1)# * xbrace_len\n",
    "A[i-1-p, j-1-p] = 0\n",
    "\n",
    "## Bottom\n",
    "# parabola\n",
    "A[j:k, j:k] = np.eye(points_bottom_parabola, points_bottom_parabola, 1)# * par_len_bottom\n",
    "# anchor\n",
    "A[k:l, k:l] = np.eye(points_bottom_anchor, points_bottom_anchor, 1)# * anchor_len\n",
    "# cross-bracing\n",
    "A[j+1+p:k-p, k+p:l-p] = np.eye(points_bottom_anchor-2*p, points_bottom_anchor-2*p, 0)# * xbrace_len\n",
    "A[j+1+p:k-p, k+p:l-p] += np.eye(points_bottom_anchor-2*p, points_bottom_anchor-2*p, 1)# * xbrace_len\n",
    "A[k-1-p, l-1-p] = 0\n",
    "\n",
    "# Anchor cross-bracing\n",
    "A[i+p:j-p, k+p:l-p] = np.eye(points_top_anchor-2*p, points_top_anchor-2*p, 1)# * anchor_xbrance_len\n",
    "A[i+p:j-p, k+p:l-p] += np.eye(points_top_anchor-2*p, points_top_anchor-2*p, -1)# * anchor_xbrance_len\n",
    "\n",
    "\n",
    "#L = l * (np.eye(n, n, 1) + np.eye(n, n, 2))\n",
    "#for i in range(n // 2 - 1):\n",
    "#    L[2 * i + 1, 2 * i + 2] *= np.sqrt(2)\n",
    "\n",
    "def spring_length(i, j):\n",
    "    return np.sqrt((P0[i,0]-P0[j,0])**2 +\n",
    "                   (P0[i,1]-P0[j,1])**2)\n",
    "L = np.zeros((n, n))\n",
    "#np.eye(n, n, 1) + np.eye(n, n, 2)\n",
    "\n",
    "for _i in range(n):\n",
    "    for _j in range(n):\n",
    "        L[_i, _j] = spring_length(_i, _j) * A[_i, _j]\n",
    "\n",
    "L[:i, :i] *= par_len_top * overall_len_factor\n",
    "L[i:j, i:j] *= anchor_len * overall_len_factor\n",
    "L[1:i, i:j] *= xbrace_len * overall_len_factor\n",
    "L[j:k, j:k] *= par_len_bottom * overall_len_factor\n",
    "L[k:l, k:l] *= anchor_len * overall_len_factor\n",
    "L[j+1:k, k:l] *= xbrace_len * overall_len_factor\n",
    "L[i:j, k:l] *= anchor_xbrance_len * overall_len_factor\n",
    "\n",
    "K = np.zeros((n, n))\n",
    "K[:i, :i] = stiffness_thick # parabola top\n",
    "K[i:j, i:j] = stiffness_thick # anchor top\n",
    "K[1:i, i:j] = stiffness_thin # cross-bracing top parabola to anchor\n",
    "K[j:k, j:k] = stiffness_thick # parabola bottom\n",
    "K[k:l, k:l] = stiffness_thick # anchor bottom\n",
    "K[j+1:k, k:l] = stiffness_thin # cross-bracing bottom parabola to anchor\n",
    "K[i:j, k:l] = stiffness_thin # anchor cross-bracing\n",
    "\n",
    "\n",
    "A = np.array([[k/l if l>0 else 0 for k, l in zip(ks, ls)] for ks, ls in zip(K, L)])\n",
    "\n",
    "#L[:i, :i] = np.eye(points_top_parabola, points_top_parabola, 1)\n",
    "#L[i:j, i:j] = np.eye(points_top_anchor, points_top_anchor, 1) * 0.5\n",
    "# cross-bracing\n",
    "#L[1:i, i:] = np.eye(points_top_anchor, points_top_anchor, 0) * 1.4142\n",
    "#L[1:i, i:] += np.eye(points_top_anchor, points_top_anchor, 1) * 1.4142\n",
    "#L[i-1, j-1] = 0\n",
    "\n",
    "# We display a graphic representation of\n",
    "# the matrix.\n",
    "f, (ax1, ax2) = plt.subplots(1, 2)\n",
    "ax1.matshow(L)\n",
    "ax1.set_axis_off()\n",
    "ax1.set_title('L')\n",
    "ax2.matshow(A)\n",
    "ax2.set_axis_off()\n",
    "ax2.set_title('A')\n",
    "\n",
    "for le_slice in [(slice(0,i), slice(0,i)), (slice(i,j), slice(i,j)), (slice(1,i), slice(i,j)),\n",
    "                 (slice(j,k), slice(j,k)), (slice(k,l), slice(k,l)), (slice(j+1,k), slice(k,l)),\n",
    "                 (slice(i,j), slice(k,l))]:\n",
    "    frob = L[le_slice]\n",
    "    print(frob[frob != 0].mean(), frob[frob != 0].shape, frob.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Result plotting infrastructure\n",
    "\n",
    "We're using my blog's nice colorscheme here, and using a hand-tweaked somewhat colorblindness-friendly colormap for edge colors that's still looking fairly nice in the overall colorscheme."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "I, J = np.nonzero(A)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def dist(P):\n",
    "    return np.sqrt((P[:,0]-P[:,0][:,np.newaxis])**2 +\n",
    "                   (P[:,1]-P[:,1][:,np.newaxis])**2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "cmap_pos = mpl.colors.LinearSegmentedColormap('energy_pos', {\n",
    "    'red': ((0., 0xfe/255, 0xfe/255), (1., 1., 1.)),\n",
    "    'green': ((0., 0x3e/255, 0x3e/255), (1., .9, .9)),\n",
    "    'blue': ((0., 0xa0/255, 0xa0/255), (1., 0., 0.))\n",
    "}).reversed()\n",
    "cmap_neg = mpl.colors.LinearSegmentedColormap('energy_neg', {\n",
    "    'red': ((0., 0x08/255, 0x08/255), (1., 0., 0.)),\n",
    "    'green': ((0., 0xbd/255, 0xbd/255), (1., 1., 1.)),\n",
    "    'blue': ((0., 0xf9/255, 0xf9/255), (1., .5, .5))\n",
    "}).reversed()\n",
    "#cmap_neg = mpl.colors.LinearSegmentedColormap('energy_neg', {\n",
    "#    'red': ((0., 0x01/255, 0x01/255), (1., 0., 0.)),\n",
    "#    'green': ((0., 0x76/255, 0x76/255), (1., 1., 1.)),\n",
    "#    'blue': ((0., 0x9D/255, 0x9D/255), (1., .5, .5))\n",
    "#})\n",
    "\n",
    "def show_bar(P, title='No title'):\n",
    "    fig, ax = plt.subplots(1, 1, figsize=(9, 4.5))\n",
    "\n",
    "    # Wall.\n",
    "    #ax.axvline(0, color='k', lw=3)\n",
    "\n",
    "    # Distance matrix.\n",
    "    D = dist(P)\n",
    "\n",
    "    # Get normalized elongation in [-1, 1].\n",
    "    elong = np.array([D[i, j] - L[i, j]\n",
    "                      for i, j in zip(I, J)])\n",
    "    elong_max = elong.max()\n",
    "    elong_min = elong.min()\n",
    "    \n",
    "    A_foo = np.array([A[i, j]\n",
    "                      for i, j in zip(I, J)])\n",
    "    a_max = A_foo[elong.argmax()]\n",
    "    \n",
    "    force = A_foo * elong\n",
    "    force_max = force.max()\n",
    "    force_min = force.min()\n",
    "\n",
    "    # The color depends on the spring tension, which\n",
    "    # is proportional to the spring elongation.\n",
    "    colors = np.zeros((len(elong), 4))\n",
    "    colors[:, -1] = 1  # alpha channel is 1\n",
    "\n",
    "    # Use two different sequentials colormaps for\n",
    "    # positive and negative elongations, to show\n",
    "    # compression and extension in different colors.\n",
    "    pos, neg = elong > 0, elong < 0\n",
    "    colors[pos] = cmap_pos(force[pos]/force_max)\n",
    "    colors[neg] = cmap_neg(elong[neg]/elong_min)\n",
    "\n",
    "    # We plot the springs.\n",
    "    for i, j, c, is_pos in zip(I, J, colors, pos):\n",
    "        if is_pos:\n",
    "            ax.plot(P[[i, j], 0], P[[i, j], 1], lw=2, color=c)\n",
    "        else:\n",
    "            ax.plot(P[[i, j], 0], P[[i, j], 1], lw=1, color=c)\n",
    "\n",
    "    edge_count = ((A + A.T) != 0).sum(axis=0)\n",
    "    single_edge = np.where(edge_count == 1)\n",
    "    not_single_edge = np.where(edge_count != 1)\n",
    "    \n",
    "    # We plot the masses.\n",
    "    ax.plot(P[not_single_edge, 0], P[not_single_edge, 1], marker='.', color='#fe3ea0')\n",
    "    ax.plot(P[single_edge, 0], P[single_edge, 1], marker='s', markersize=6, color='#fe3ea0')\n",
    "\n",
    "    # We configure the axes.\n",
    "    # Use non-equal scaling factors for x and y to improve readability since our structure is highly elongated.\n",
    "    #ax.axis('equal')\n",
    "    ax.set_xlim(P[:, 0].min() - e,\n",
    "                P[:, 0].max() + e)\n",
    "    ax.set_ylim(P[:, 1].min() - e/4,\n",
    "                P[:, 1].max() + e/4)\n",
    "    ax.grid()\n",
    "    ax.set_xlabel('x [m]')\n",
    "    ax.set_ylabel('y [m]')\n",
    "    \n",
    "    cax1, _ = mpl.colorbar.make_axes(plt.gca(), fraction=0.05, pad=0.1)\n",
    "    cbar = mpl.colorbar.ColorbarBase(cax1, cmap=cmap_pos, norm=mpl.colors.Normalize(vmin=0, vmax=force_max/g),\n",
    "                                     label='Force [kg equiv., thick lines]')\n",
    "    cax1.yaxis.set_ticks_position('left')\n",
    "    cax1.yaxis.set_label_position('left')\n",
    "    cax2, _ = mpl.colorbar.make_axes(plt.gca(), fraction=0.475)\n",
    "    cbar = mpl.colorbar.ColorbarBase(cax2, cmap=cmap_neg, norm=mpl.colors.Normalize(vmin=0, vmax=-elong_min*100),\n",
    "                                     label='Slack [cm, thin lines]')\n",
    "\n",
    "    for iax in (ax, cax1, cax2):\n",
    "        iax.spines['top'].set_visible(False)\n",
    "        iax.spines['right'].set_visible(False)\n",
    "        iax.spines['bottom'].set_color('#08bdf9')\n",
    "        iax.spines['left'].set_color('#08bdf9')\n",
    "        iax.tick_params(axis='x', colors='#01769D')\n",
    "        iax.tick_params(axis='y', colors='#01769D')\n",
    "        iax.xaxis.label.set_color('#01769D')\n",
    "        iax.yaxis.label.set_color('#01769D')\n",
    "        iax.grid(color='#08bdf9', linestyle=':')\n",
    "    \n",
    "    title = f'{title}, $F_{{max}} = {a_max*elong_max:.3g}[N] \\equiv {a_max*elong_max/g:.3g} [kg]$'\n",
    "    ax.title.set_color('#01769D')\n",
    "    ax.set_title(title)\n",
    "    \n",
    "    return fig"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Plot initial setup before simulation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "podoc": {
     "output_text": "<matplotlib.figure.Figure at 0x7157668>"
    },
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 88260 characters]\n",
      "text/plain": [
       "<Figure size 648x324 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = show_bar(P0, title='Initial configuration')\n",
    "fig.savefig('/tmp/initial_plot.svg')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Energy function for optimizer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def energy(P):\n",
    "    # The argument P is a vector (flattened matrix).\n",
    "    # We convert it to a matrix here.\n",
    "    P = P.reshape((-1, 2))\n",
    "    # We compute the distance matrix.\n",
    "    D = dist(P)\n",
    "    \n",
    "    delta_x = D - L\n",
    "    #delta_x[L!=0] /= L[L!=0]\n",
    "    delta_x = np.clip(delta_x, 0, None)\n",
    "    #fig, ax = plt.subplots()\n",
    "    #ax.imshow(A * delta_x)\n",
    "    \n",
    "    # The potential energy is the sum of the\n",
    "    # gravitational and elastic potential energies.\n",
    "    return (g * m * P[:, 1].sum() +\n",
    "            .5 * (A * delta_x**2).sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1741.2731117646306"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "energy(P0.ravel())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Bounds for optimizer\n",
    "\n",
    "The layout of this data structure is a bit funky with min/maxes separately for every coordinate. See the L-BFGS-B scipy opt doc for reference."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "#bounds = np.c_[P0[:2, :].ravel(),\n",
    "#               P0[:2, :].ravel()].tolist() + \\\n",
    "#    [[None, None]] * (2 * (n - 4)) + \\\n",
    "#    np.c_[P0[-2:, :].ravel(),\n",
    "#               P0[-2:, :].ravel()].tolist()\n",
    "\n",
    "\n",
    "bounds = np.array([[None, None]] * n*2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "i, j = points_top_parabola, points_top_parabola+points_top_anchor\n",
    "k, l = j+points_bottom_parabola, j+points_bottom_parabola+points_bottom_anchor\n",
    "par_y_top = anchor_offset_y_top + parabola_offset_y_top + parabola_height_top\n",
    "\n",
    "bounds[0, :] = [-overall_width/2, -overall_width/2] # parabola left x\n",
    "bounds[1, :] = [par_y_top, par_y_top] # parabola left y\n",
    "bounds[2*i-2, :] = [overall_width/2, overall_width/2] # parabola right x\n",
    "bounds[2*i-1, :] = [par_y_top, par_y_top] # parabola right y\n",
    "bounds[2*i, :] = [-overall_width/2, -overall_width/2] # anchor left x\n",
    "bounds[2*i+1, :] = [anchor_offset_y_top, anchor_offset_y_top] # anchor left y\n",
    "bounds[2*j-2, :] = [overall_width/2, overall_width/2] # anchor right x\n",
    "bounds[2*j-1, :] = [anchor_offset_y_top, anchor_offset_y_top] # anchor right y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "i, j = points_top_parabola, points_top_parabola+points_top_anchor\n",
    "k, l = j+points_bottom_parabola, j+points_bottom_parabola+points_bottom_anchor\n",
    "par_y_bottom = anchor_offset_y_bottom + parabola_offset_y_bottom - parabola_height_bottom\n",
    "\n",
    "bounds[2*j+0, :] = [-overall_width/2, -overall_width/2] # parabola left x\n",
    "bounds[2*j+1, :] = [par_y_bottom, par_y_bottom] # parabola left y\n",
    "bounds[2*k-2, :] = [overall_width/2, overall_width/2] # parabola right x\n",
    "bounds[2*k-1, :] = [par_y_bottom, par_y_bottom] # parabola right y\n",
    "bounds[2*k, :] = [-overall_width/2, -overall_width/2] # anchor left x\n",
    "bounds[2*k+1, :] = [anchor_offset_y_bottom, anchor_offset_y_bottom] # anchor left y\n",
    "bounds[2*l-2, :] = [overall_width/2, overall_width/2] # anchor right x\n",
    "bounds[2*l-1, :] = [anchor_offset_y_bottom, anchor_offset_y_bottom] # anchor right y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Run optimizer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "P1 = opt.minimize(energy, P0.ravel(),\n",
    "                  method='L-BFGS-B',\n",
    "                  options=dict(maxfun=100000, maxiter=100000, maxls=100, gtol=1e-6),\n",
    "                  bounds=bounds).x.reshape((-1, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Render result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 98580 characters]\n",
      "text/plain": [
       "<Figure size 648x324 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = show_bar(P1, title='Steady state')\n",
    "fig.savefig('/tmp/optimized_plot.svg')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Plot node displacements\n",
    "\n",
    "This computes and plots\n",
    "1. The distance of top and bottom nodes of the digit holding lines. To much error here would lead to the digit's vertical and diagonal segments not being taut or getting overly stressed and torn apart.\n",
    "2. The overall (common) droop of top and bottom digit holding lines. This will simply be visible as the display banner drooping down in the middle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 46820 characters]\n",
      "text/plain": [
       "<Figure size 576x324 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "i, j = points_top_parabola, points_top_parabola+points_top_anchor\n",
    "k, l = j+points_bottom_parabola, j+points_bottom_parabola+points_bottom_anchor\n",
    "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 4.5), sharex=True)\n",
    "fig.subplots_adjust(hspace=0)\n",
    "dist_err = -(P1[k:l] - P1[i:j])[:,1] - digit_height\n",
    "ax1.plot(dist_err*100, color='#fe3ea0')\n",
    "ax1.set_ylabel('distance\\nerror [cm]')\n",
    "abs_disp_bot = (P1[k:l]-anchor_offset_y_bottom)[:,1]\n",
    "abs_disp_top = (P1[i:j]-anchor_offset_y_top)[:,1]\n",
    "ax2.plot(abs_disp_bot*100, color='#fe3ea0')\n",
    "ax2.plot(abs_disp_top*100, color='#fe3ea0')\n",
    "ax2.set_ylabel('absolute\\ndisplacement [cm]')\n",
    "ax1.grid(), ax2.grid()\n",
    "ax2.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))\n",
    "ax2.set_xlabel('digit index')\n",
    "\n",
    "for ax in (ax1, ax2):\n",
    "    ax.axvline(pad_digits, color='#ffd2e9')\n",
    "    ax.axvline(digits-pad_digits, color='#ffd2e9')\n",
    "    ax.spines['top'].set_visible(False)\n",
    "    ax.spines['right'].set_visible(False)\n",
    "    ax.spines['bottom'].set_color('#08bdf9')\n",
    "    ax.spines['left'].set_color('#08bdf9')\n",
    "    ax.tick_params(axis='x', colors='#01769D')\n",
    "    ax.tick_params(axis='y', colors='#01769D')\n",
    "    ax.xaxis.label.set_color('#01769D')\n",
    "    ax.yaxis.label.set_color('#01769D')\n",
    "    ax.grid(color='#08bdf9', linestyle=':')\n",
    "\n",
    "display_part = slice(pad_digits, -pad_digits-1)\n",
    "dist_err_mean, dist_err_max = dist_err[display_part].mean(), np.abs(dist_err[display_part]).max()\n",
    "abs_disps = np.vstack([abs_disp_bot[display_part], abs_disp_top[display_part]])\n",
    "abs_disp_mean, abs_disp_max = abs_disps.mean(), np.abs(abs_disps).max()\n",
    "ax1.set_title(f'Vertical displacement, $\\overline{{\\Delta d}}={dist_err_mean*100:.3g}[cm]$, $\\Delta d_{{max}}={dist_err_max*100:.3g}[cm]$, $\\overline{{\\Delta y}}={abs_disp_mean*100:.3g}[cm]$, $\\Delta y_{{max}}={abs_disp_max*100:.3g}[cm]$')\n",
    "ax1.title.set_color('#01769D')\n",
    "fig.savefig('/tmp/displacement_plot.svg')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
