{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "hide_input": false,
    "tags": [
     "active-ipynb",
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    from openmdao.utils.notebook_utils import notebook_mode\n",
    "except ImportError:\n",
    "    !python -m pip install openmdao[notebooks]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Kepler’s Equation\n",
    "\n",
    "This example will demonstrate the use of OpenMDAO for solving an implicit equation commonly found in astrodynamics, Kepler’s Equation:\n",
    "\n",
    "\\begin{align}\n",
    "E - e \\sin{E} = M\n",
    "\\end{align}\n",
    "\n",
    "Here $M$ is the mean anomaly, $E$ is the eccentric anomaly, and $e$ is the eccentricity of the orbit.\n",
    "\n",
    "If we know the eccentric anomaly, computing the mean anomaly is trivial. However, solving for the eccentric anomaly when given the mean anomaly must be done numerically. We’ll do so using a nonlinear solver. In OpenMDAO, solvers converge all implicit state variables in a Group by driving their residuals to zero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## Using a BalanceComp and NewtonSolver\n",
    "\n",
    "In an effort to simplify things for users, OpenMDAO features a Balance component. For each implicit state variable we assign to the balance, it solves the following equation:\n",
    "\n",
    "\\begin{align}\n",
    "lhs(var) \\cdot mult(var) = rhs(var)\n",
    "\\end{align}\n",
    "\n",
    "The _mult_ term is an optional multiplier than can be applied to the left-hand side (LHS) of the equation. For our example, we will assign the right-hand side (RHS) to the mean anomaly ($M$), and the left-hand side to $E - e \\sin{E}$.\n",
    "\n",
    "In this implementation, we rely on an ExecComp to compute the value of the LHS.\n",
    "\n",
    "BalanceComp also provides a way to supply the starting value for the implicit state variable ($E$ in this case), via the guess_func argument. The supplied function should have a similar signature to the guess_nonlinear function of ImplicitComponent. When solving Kepler’s equation, using $M$ as the initial guess for $E$ is a good starting point.\n",
    "\n",
    "In summary, the recipe for solving Kepler’s equation with a NewtonSolver is as follows:\n",
    "\n",
    "1. Define a Group to contain the implicit system.\n",
    "2. To that Group, add components which provide, $M$, $e$, and the left-hand side of Kepler’s equation.\n",
    "3. Add a linear and nonlinear solver to the Group, since the default solvers do not iterate.\n",
    "4. Setup the problem, set values for the inputs, and run the model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "bal = om.BalanceComp()\n",
    "\n",
    "bal.add_balance(name='E', val=0.0, units='rad', eq_units='rad', rhs_name='M')\n",
    "\n",
    "# Use M (mean anomaly) as the initial guess for E (eccentric anomaly)\n",
    "def guess_function(inputs, outputs, residuals):\n",
    "    if np.abs(residuals['E']) > 1.0E-2:\n",
    "        outputs['E'] = inputs['M']\n",
    "\n",
    "bal.options['guess_func'] = guess_function\n",
    "\n",
    "# ExecComp used to compute the LHS of Kepler's equation.\n",
    "lhs_comp = om.ExecComp('lhs=E - ecc * sin(E)',\n",
    "                       lhs={'units': 'rad'},\n",
    "                       E={'units': 'rad'},\n",
    "                       ecc={'units': None})\n",
    "\n",
    "prob.model.set_input_defaults('M', 85.0, units='deg')\n",
    "\n",
    "prob.model.add_subsystem(name='lhs_comp', subsys=lhs_comp,\n",
    "                         promotes_inputs=['E', 'ecc'])\n",
    "\n",
    "prob.model.add_subsystem(name='balance', subsys=bal,\n",
    "                         promotes_inputs=['M'],\n",
    "                         promotes_outputs=['E'])\n",
    "\n",
    "# Explicit connections\n",
    "prob.model.connect('lhs_comp.lhs', 'balance.lhs:E')\n",
    "\n",
    "# Set up solvers\n",
    "prob.model.linear_solver = om.DirectSolver()\n",
    "prob.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=False, maxiter=100, iprint=2)\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('M', 85.0, units='deg')\n",
    "prob.set_val('E', 85.0, units='deg')\n",
    "prob.set_val('ecc', 0.6)\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "M = prob.get_val('M')\n",
    "E = prob.get_val('E')\n",
    "print(f'M = {M}')\n",
    "print(f'E = {E}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{note}\n",
    "BalanceComp should be placed after those components which compute its inputs.  Otherwise errors can arise due to timing issues.\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "# Check that results are correct.\n",
    "\n",
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "assert_near_equal(prob.get_val('E'), 2.02317564, tolerance=1.0E-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Using Fixed-Point Iteration with an ImplicitComponent\n",
    "\n",
    "One approach to solving Kepler's Equation is to use fixed-point iteration:\n",
    "\n",
    "\\begin{align}\n",
    "E_{n+1} = M + e \\sin{E_{n}}\n",
    "\\end{align}\n",
    "\n",
    "We can achieve this behavior by defining a simple ImplicitComponent that \"solves itself\" by overriding its `solve_nonlinear` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class KeplerComp(om.ImplicitComponent):\n",
    "    \n",
    "    def initialize(self):\n",
    "        self.options.declare('tol', types=float, default=1.0E-12, desc='convergence tolerance')\n",
    "        self.options.declare('max_its', types=int, default=50, desc='maximum number of iterations')\n",
    "    \n",
    "    def setup(self):\n",
    "        self.add_input('M', val=1.0, units='rad', desc='Mean anomaly')\n",
    "        self.add_input('ecc', val=0.0, units=None, desc='eccentricity')\n",
    "        self.add_output('E', val=1.0, units='rad', desc='Eccentric anomaly')\n",
    "        \n",
    "        self.declare_partials(of='E', wrt='*', method='cs')\n",
    "        \n",
    "    def apply_nonlinear(self, inputs, outputs, residuals):\n",
    "        \"\"\" Compute the residual of output E. \"\"\"\n",
    "        M = inputs['M']\n",
    "        e = inputs['ecc']\n",
    "        E = outputs['E']\n",
    "        residuals['E'] = E - M - e * np.sin(E)\n",
    "        \n",
    "    def solve_nonlinear(self, inputs, outputs):\n",
    "        \"\"\" Determine the values of the implicit outputs that eliminate the residual. \"\"\"\n",
    "        M = inputs['M']\n",
    "        e = inputs['ecc']\n",
    "        E = outputs['E']\n",
    "        \n",
    "        # Compute the initial guess for iteration if the initial redisual is large.\n",
    "        E = inputs['M'] if np.abs(E - M - e * np.sin(E)) > 1.0E-2 else outputs['E']\n",
    "            \n",
    "        for i in range(self.options['max_its']):\n",
    "            E_old = E\n",
    "            E = M + e * np.sin(E)\n",
    "            if i == 0:\n",
    "                print('iter ' + '   ' + 'max abs. error')\n",
    "                print(5*'-' + '   '+ 14*'-')\n",
    "            print(f'{i:>5} {np.max(np.abs(E - E_old)):16.12f}')\n",
    "            if np.abs(E - E_old) < self.options['tol']:\n",
    "                outputs['E'] = E\n",
    "                break\n",
    "        else:\n",
    "            raise om.AnalysisError('Iteration Limit Exceeded')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here the KeplerComp uses the `solve_nonlinear` method to drive it residual to zero through fixed point iteration.\n",
    "\n",
    "```{important}\n",
    "In OpenMDAO it doesn't matter how the residuals of a system of eliminated.  Inside solve_nonlinear you're free to use a variety of techniques.  As long as the resulting residual is _reasonably close to zero_, the derivatives across the implicit system will be accurate.\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "prob.model.add_subsystem(name='kep_comp', subsys=KeplerComp(),\n",
    "                         promotes_inputs=['M', 'ecc'],\n",
    "                         promotes_outputs=['E'])\n",
    "\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('M', 85.0, units='deg')\n",
    "prob.set_val('E', 1.0, units='deg')\n",
    "prob.set_val('ecc', 0.6)\n",
    "\n",
    "prob.run_model()\n",
    "M = prob.get_val('M')\n",
    "E = prob.get_val('E')\n",
    "print(f'M = {M}')\n",
    "print(f'E = {E}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "# Check that results are correct.\n",
    "\n",
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "assert_near_equal(prob.get_val('E'), 2.02317564, tolerance=1.0E-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In contrast to the NewtonSolver approach, this approach roughly needs the following steps:\n",
    "\n",
    "1. Define an ImplicitComponent where the `apply_nonlinear` method computes the residuals for Kepler's Equation and `solve_nonlinear` find the value of the implicit output that eliminates the residuals.\n",
    "2. Add the ImplicitComponent to the problem model.\n",
    "3. Setup the problem, set values for the inputs, and run the model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Which approach should you use?\n",
    "\n",
    "It's really up to you.  In this case, the Newton approach involves slightly more complicated `System` consisting of two components (the `ExecComp` and the `BalanceComp`) but it converges in just 4 iteratoins.\n",
    "\n",
    "The ImplicitComponent involves only a single system, but writing the `solve_nonlinear` method with some error handling, initial guess generation, and print capability made for more coding on the part of the user."
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "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.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
