{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f9f2925f",
   "metadata": {
    "tags": [
     "remove-input",
     "active-ipynb",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    from openmdao.utils.notebook_utils import notebook_mode  # noqa: F401\n",
    "except ImportError:\n",
    "    !python -m pip install openmdao[notebooks]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "ee8e8873",
   "metadata": {},
   "source": [
    "# Using the Case Viewer\n",
    "\n",
    "The CaseViewer is a tool for interactive viewing of recorded cases data in Jupyter notebooks.\n",
    "This is currently an experimental feature of OpenMDAO but it's useful enough in its current state that we've included it in the package."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "c48021c2",
   "metadata": {},
   "source": [
    "## Example: Queen Dido's Problem\n",
    "\n",
    "Queen Dido's problem is an optimization problem that is also a legend about the founding of Carthage.\n",
    "Supposedly Queen Dido was allowed all the land she could enclose within the hide of an ox.\n",
    "Cutting the hide into very thin strips provided her with a long, but finite, length of \"fencing.\"\n",
    "What arrangement of this fencing provides the greatest possible enclosed area?\n",
    "\n",
    "If we assume a straight edge along one side of the fence (such as a riverbank or shoreline), how should the fencing be arranged to capture the greatest possible area?\n",
    "\n",
    "The following component takes the distance of the river bank, $L$, as well as the distance of the \"fence posts\" from the riverbank, $y$, and computes the enclosed area and the perimeter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab5dfa22",
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "83f44aa7",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Fence(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('n', types=int, default=50, desc='number of fenceposts')\n",
    "\n",
    "    def setup(self):\n",
    "        n = self.options['n']\n",
    "        self.add_input('L', val=10.0, units='m')\n",
    "        self.add_input('y', val=np.zeros(n), units='m')\n",
    "\n",
    "        self.add_output('perimeter', val=0.0, units='m')\n",
    "        self.add_output('area', val=0.0, units='m**2')\n",
    "        self.add_output('x', val=np.linspace(0, 100, n), units='m')\n",
    "\n",
    "        self.declare_partials(of='*', wrt='*', method='cs')\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        n = self.options['n']\n",
    "        L = inputs['L']\n",
    "        y = inputs['y']\n",
    "\n",
    "        x = np.linspace(0, L, n)\n",
    "\n",
    "        dx = x[1:] - x[:-1]\n",
    "        dy = y[1:] - y[:-1]\n",
    "\n",
    "        mean_y = 0.5 * (y[1:] + y[:-1])\n",
    "\n",
    "        outputs['perimeter'] = np.sum(np.sqrt(dx**2+dy**2))\n",
    "        outputs['area'] = np.sum(dx * mean_y)\n",
    "        outputs['x'] = x"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "6c9a5aa8",
   "metadata": {},
   "source": [
    "The optimization problem is to arrange these fence posts such that the perimeter $p$ does not exceed some value while maximizing the enclosed area $A$.\n",
    "\n",
    "In addition, the first and last fenceposts should be placed along the river's edge.\n",
    "\n",
    "\\begin{align}\n",
    "\\mathrm{Minimize}\\;J &= -A \\\\\n",
    "\\mathrm{Subject\\;to:} \\\\\n",
    "& 0 \\le y_i \\quad \\mathrm{for} \\quad i=1 ... N \\\\\n",
    "& y[0] = 0 \\; \\mathrm{m}\\\\\n",
    "& y[N] = 0 \\; \\mathrm{m}\\\\\n",
    "& p = 300 \\; \\mathrm{m}\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98d0c683",
   "metadata": {
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "def plot_fence(p, idx=0, fig=None, axes=None):\n",
    "    if axes is None:\n",
    "        fig, axes = plt.subplots(3, 1, figsize=(12, 6), tight_layout=True, sharex=True)\n",
    "    axes[idx].plot(p.get_val('fence.x'), p.get_val('fence.y'), 'ko-')\n",
    "    axes[idx].fill_between(p.get_val('fence.x'), p.get_val('fence.y'), color='tab:green')\n",
    "    axes[idx].fill_between([-1, 12], [-2, -2], color='tab:blue')\n",
    "    A = p.get_val('fence.area', units='m**2')[0]\n",
    "    perimeter = p.get_val('fence.perimeter', units='m')[0]\n",
    "    L = p.get_val('fence.L', units='m')[0]\n",
    "    axes[idx].annotate(f'p = {perimeter:6.2f} $m$', (4, 2.5), fontsize=16, color='white')\n",
    "    axes[idx].annotate(f'A = {A:6.2f} $m^2$', (4, 0.5), fontsize=16, color='white')\n",
    "    ht = -1\n",
    "    w = L\n",
    "    axes[idx].annotate('', xy=(0, ht), xytext=(w, ht), textcoords=axes[idx].transData, arrowprops=dict(arrowstyle='<->', color='white'))\n",
    "    axes[idx].annotate('', xy=(0, ht), xytext=(w, ht), textcoords=axes[idx].transData, arrowprops=dict(arrowstyle='|-|', color='white'))\n",
    "    bbox=dict(fc='tab:blue', ec='none')\n",
    "    axes[idx].text(w/2, ht, f'L={L:6.2f} m', ha='center', va='center', color='white', fontsize=16, bbox=bbox)\n",
    "    return fig, axes\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "959c9662",
   "metadata": {
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "N = 4\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "prob.model.add_subsystem('fence', Fence(n=N))\n",
    "\n",
    "prob.model.add_design_var('fence.L', lower=1)\n",
    "prob.model.add_design_var('fence.y', lower=0, indices=om.slicer[1:-1])\n",
    "prob.model.add_constraint('fence.perimeter', equals=100 * np.pi, ref=100 * np.pi)\n",
    "prob.model.add_objective('fence.area', ref=-1000.0)\n",
    "\n",
    "prob.driver = om.pyOptSparseDriver(optimizer='SLSQP')\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "\n",
    "prob.set_val('fence.L', val=10)\n",
    "\n",
    "fig = None\n",
    "axes = None\n",
    "\n",
    "for i, inp in enumerate(((10, [0, 3, 5, 0]),\n",
    "                         (11, [0, 4, 7, 0]),\n",
    "                         (9, [0, 7, 7, 0]))):\n",
    "    L, ys = inp\n",
    "    prob.set_val('fence.L', val=L)\n",
    "    prob.set_val('fence.y', val=ys)\n",
    "    prob.run_model()\n",
    "    fig, axes = plot_fence(prob, idx=i, fig=fig, axes=axes)\n",
    "\n",
    "fig.suptitle('Potential arrangements of 4 Fence posts and the resulting perimeter and enclosed area.', fontsize=16)\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "20413b8b",
   "metadata": {},
   "source": [
    "## Solving the Optimization Problem and Recording the Driver Iterations\n",
    "\n",
    "In the following code, we set up the optimization problem to find the location of 20 fence posts which maximize area while limiting perimeter to $300$ meters.\n",
    "A recorder is added to the driver that records the design variables, constraints, and objectives."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6d905fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 20\n",
    "p = om.Problem()\n",
    "\n",
    "p.model.add_subsystem('fence', Fence(n=N))\n",
    "\n",
    "p.model.add_design_var('fence.L', lower=1)\n",
    "p.model.add_design_var('fence.y', lower=0, indices=om.slicer[1:-1])\n",
    "p.model.add_constraint('fence.perimeter', equals=100 * np.pi, ref=100 * np.pi)\n",
    "p.model.add_objective('fence.area', ref=-1000.0)\n",
    "\n",
    "p.driver = om.pyOptSparseDriver(optimizer='SLSQP')\n",
    "\n",
    "p.driver.add_recorder(om.SqliteRecorder('driver_cases.db'))\n",
    "p.driver.recording_options['includes'] = ['*']\n",
    "p.driver.recording_options['record_desvars'] = True\n",
    "p.driver.recording_options['record_constraints'] = True\n",
    "p.driver.recording_options['record_objectives'] = True\n",
    "\n",
    "p.setup(force_alloc_complex=True)\n",
    "\n",
    "p.set_val('fence.y', val=np.zeros(N))\n",
    "\n",
    "p.run_driver();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0395ddec",
   "metadata": {
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "assert_near_equal(p.get_val('fence.area'), 821.85, tolerance=1.0E-2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "ee1f3c8b",
   "metadata": {},
   "source": [
    "## Invoking the CaseViewer\n",
    "\n",
    "In the following line we invoke the CaseViewer.\n",
    "It will not be displayed in the static documentation here, but executing this notebook in Jupyter will provide a GUI like that shown in the figure below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "40f6ef90",
   "metadata": {
    "scrolled": false,
    "tags": [
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "om.CaseViewer(p.get_outputs_dir() / 'driver_cases.db');"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "d4b829b8",
   "metadata": {},
   "source": [
    "## Plotting a scalar vs. the case index\n",
    "\n",
    "To see how the value of a variable evolves over the iteration history, use `Case Index` as the X-Axis variable.\n",
    "\n",
    "For instance, to see how the area (the objective function) changes over iterations, choose `outputs -> fence.area` as the Y-Axis variable.\n",
    "\n",
    "![Figure 1](./images/case_viewer_area.png)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "e249b073",
   "metadata": {},
   "source": [
    "## Plotting an array vs. the case index\n",
    "\n",
    "When an array-valued variable is plotted against the Case Index, each value in the array is plotted at the X-Axis value corresponding to each case.\n",
    "The color of each point represents its index in the array, with warmer colors being higher indices.\n",
    "\n",
    "For instance, to see how the values of `fence.y` at each iteration, choose `inputs -> fence.y` as the Y-Axis variable.\n",
    "\n",
    "If this type of plot is too noisy for your data, you can use the `Y Transform` options to retrieve things like the maximum value in the array, the minimum value in the array, or the norm of the array.\n",
    "\n",
    "![Figure 2](./images/case_viewer_y_vs_case.png)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "4cac5a8d",
   "metadata": {},
   "source": [
    "## Plotting one array vs. another\n",
    "\n",
    "Sometimes it's useful to visualize the evolution of a solution as the plot of one array-shaped variable vs. another.\n",
    "In our case, the shape of the fence is represented by plotting `outputs -> fence.x` on the X-Axis and `inputs -> fence.y` on the Y-Axis.\n",
    "\n",
    "\n",
    "When plotting arrays, the X-Axis is required to be viewable as a flattened array.\n",
    "The Y-Axis variable must have the same number of points as the X-Axis variable in its first dimension.\n",
    "In our use-case here, `fence.x` and `fence.y` have the same shape.\n",
    "If they did not, then the slice text boxes could be used to reshape the variable in the appropriate way.\n",
    "The default value, `[...]`, plots the entire array.\n",
    "\n",
    "When plotting one array vs. another, the CaseViewer will connect the points in each case using a line, and color that line based on the index of the case in the selected cases box at the top of the GUI.\n",
    "Scrolling the slider will highlight a particular case.\n",
    "\n",
    "![Figure 1](./images/case_viewer_x_vs_y.png)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "613caffd",
   "metadata": {},
   "source": [
    "## Current limitations of the CaseViewer\n",
    "\n",
    "CaseViewer is currently limited to one instance per notebook.  Instantiating more than one will cause the plotting interface to fail."
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.4"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
