{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "hide_input": true,
    "tags": [
     "active-ipynb",
     "remove-input",
     "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]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Simple Optimization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Paraboloid\n",
    "\n",
    "This example finds the minimum point in a paraboloid defined by:\n",
    "\n",
    "\\begin{align}\n",
    "f(x,y) &= (x-3)^2 + x y + (y+4)^2 - 3\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "hide_input": false,
    "scrolled": true,
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "x = np.linspace(-50, 50, 100)\n",
    "y = np.linspace(-50, 50, 100)\n",
    "XX, YY = np.meshgrid(x, y)\n",
    "ZZ = (XX - 3)**2 + XX * YY + (YY + 4)**2 - 3\n",
    "\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.contour(XX, YY, ZZ, levels=np.linspace(np.min(ZZ), np.max(ZZ), 50))\n",
    "plt.xlabel('x')\n",
    "plt.ylabel('y')\n",
    "plt.colorbar(label='f')\n",
    "plt.suptitle('Contour of the paraboloid', fontsize=18);\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Finding the minimum with OpenMDAO\n",
    "\n",
    "The following script uses OpenMDAO to find the minimum of the paraboloid."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "# build the model\n",
    "prob = om.Problem()\n",
    "\n",
    "prob.model.add_subsystem('paraboloid', om.ExecComp('f = (x-3)**2 + x*y + (y+4)**2 - 3'))\n",
    "\n",
    "# setup the optimization\n",
    "prob.driver = om.ScipyOptimizeDriver()\n",
    "prob.driver.options['optimizer'] = 'SLSQP'\n",
    "\n",
    "prob.model.add_design_var('paraboloid.x', lower=-50, upper=50)\n",
    "prob.model.add_design_var('paraboloid.y', lower=-50, upper=50)\n",
    "prob.model.add_objective('paraboloid.f')\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "# Set initial values.\n",
    "prob.set_val('paraboloid.x', 3.0)\n",
    "prob.set_val('paraboloid.y', -4.0)\n",
    "\n",
    "# run the optimization\n",
    "prob.run_driver();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "def test_1():\n",
    "    from openmdao.utils.assert_utils import assert_near_equal\n",
    "    assert_near_equal(prob.get_val('paraboloid.x'), 6.66666666, tolerance=1.0E-5)\n",
    "    assert_near_equal(prob.get_val('paraboloid.y'), -7.33333333, tolerance=1.0E-5)\n",
    "    assert_near_equal(prob.get_val('paraboloid.f'), -27.33333333, tolerance=1.0E-5)\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step-by-step explanation\n",
    "\n",
    "First, we import the OpenMDAO API into the namespace `om`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Instantiating a Problem\n",
    "\n",
    "An OpenMDAO `Problem` couples a computational _model_ with some sort of _driver_ that executes that model.\n",
    "\n",
    "The default model is an empty _Group_, to which we can add _Components_ or _Groups_ of _Components_.\n",
    "\n",
    "The default driver simply executes the model a single time.  More commonly, you'll use drivers that perform optimization (such as ScipyOptimizeDriver or pyOptSparseDriver) or execute the model to perform a design-of-experiments (DOEDriver)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building our Model\n",
    "\n",
    "In this case, our model consists of a single component; an `ExecComp` that executes a user-defined function.  We've given it the mathematical equation for our paraboloid."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob.model.add_subsystem('paraboloid', om.ExecComp('f = (x-3)**2 + x*y + (y+4)**2 - 3'));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Specifying the Driver\n",
    "\n",
    "In this case we wish to find the minimum value on the contour of the paraboloid.  To do this, we use an optimization driver.  Optimization drivers vary the values of some _design variables_ to minimize the value of some _objective function_.  Many optimiation techniques can also respect some set of _constraints_ imposed upon the optimization, but this particular problem is unconstrained.  We're going to use OpenMDAO's built in `ScipyOptimizeDriver`, which uses the optimizers from [scipy.optimize.minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html) to perform the optimization.  In this case, the design variables are `x` and `y`, and the objective is `f`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# setup the optimization\n",
    "prob.driver = om.ScipyOptimizeDriver()\n",
    "prob.driver.options['optimizer'] = 'SLSQP'\n",
    "\n",
    "prob.model.add_design_var('paraboloid.x', lower=-50, upper=50)\n",
    "prob.model.add_design_var('paraboloid.y', lower=-50, upper=50)\n",
    "prob.model.add_objective('paraboloid.f')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup the Problem\n",
    "\n",
    "The next step is setting up the problem.  This is a bit analogous to compiling source code when programming.  OpenMDAO allocates the necessary data structures that enable pass data between different parts of our model and to compute the derivatives of our model for the optimizer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "prob.setup()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(examples_paraboloid:specifying_initial_values)=\n",
    "### Specifying Initial Values\n",
    "\n",
    "During the course of optimization, OpenMDAO is going to vary the value of the design variables (`x` and `y`) in an attempt to find the minimum value of `f`.  The optimization algorithms involved generally need the user to specify a starting point.  This is accomplished using the `set_val` method of the `Problem` class.\n",
    "\n",
    "OpenMDAO will, by default, issue a warning if the initial values of the design variables exceed their specified bounds. This behavior may be changed by setting the driver option `invalid_desvar_behavior` to one of `'warn'`, `'raise'`, or `'ignore'`, with `'warn'` being the default. Some users may find it useful to override this default value globally, which may be accomplished by setting the environment variable `OPENMDAO_INVALID_DESVAR_BEHAVIOR` to one of `'warn'`, `'raise'`, or `'ignore'`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set initial values.\n",
    "prob.set_val('paraboloid.x', 3.0)\n",
    "prob.set_val('paraboloid.y', -4.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Running the Optimization Driver\n",
    "\n",
    "Finally, we run the optimization driver to actually find the minimum value.  The ScipyOptimizeDriver will output the results of the optimization to standard output.  In addition, `run_driver` will return a `fail` flag.  If `True`, this means the optimization driver believes it failed to successfully find an optimal solution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# run the optimization\n",
    "prob.run_driver()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Getting the results\n",
    "\n",
    "We can access the values of variables in our model by using the `get_val` method of `Problem`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Optimal x value: ', prob.get_val('paraboloid.x'))\n",
    "print('Optimal y value: ', prob.get_val('paraboloid.y'))\n",
    "print('Objective value: ', prob.get_val('paraboloid.f'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(prob.get_val('paraboloid.x'), 6.66666666, tolerance=1.0E-5)\n",
    "assert_near_equal(prob.get_val('paraboloid.y'), -7.33333333, tolerance=1.0E-5)\n",
    "assert_near_equal(prob.get_val('paraboloid.f'), -27.33333333, tolerance=1.0E-5)"
   ]
  }
 ],
 "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.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
