{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "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]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Raising an AnalysisError\n",
    "\n",
    "This example demonstrates the effect of raising an `AnalysisError` in your Component's `compute` function.  The result depends on which driver and optimizer is used.  The `SNOPT` and `IPOPT` optimizers, used in conjunction with [pyOptSparseDriver](../../features/building_blocks/drivers/pyoptsparse_driver.ipynb), are good options if your model has invalid regions.\n",
    "\n",
    "\n",
    "## Model\n",
    "\n",
    "\n",
    "For this somewhat contrived case, we will assume some range of input values to our Component is invalid and raise an `AnalysisError` if those inputs are encountered.  We will use the [Paraboloid](../../basic_user_guide/single_disciplinary_optimization/first_analysis) as the basis for our example, modifying it so that it will raise an AnalysisError if the x or y inputs are within a specified range."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.notebook_utils import get_code\n",
    "from myst_nb import glue\n",
    "glue(\"code_paraboloid_invalid_region\", get_code(\"openmdao.test_suite.components.paraboloid_invalid_region.Paraboloid\"), display=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ":::{Admonition} `Paraboloid` class definition \n",
    ":class: dropdown\n",
    "\n",
    "{glue:}`code_paraboloid_invalid_region`\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First, we will define a function to create a Problem instance while allowing us to specify the optimizer and the invalid region:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "from openmdao.test_suite.components.paraboloid_invalid_region import Paraboloid\n",
    "\n",
    "\n",
    "def setup_problem(optimizer, invalid_x=None, invalid_y=None):\n",
    "    # Paraboloid model with optional AnalysisErrors\n",
    "    model = om.Group()\n",
    "\n",
    "    model.add_subsystem('p1', om.IndepVarComp('x', 50.0), promotes=['*'])\n",
    "    model.add_subsystem('p2', om.IndepVarComp('y', 50.0), promotes=['*'])\n",
    "\n",
    "    comp = model.add_subsystem('comp',\n",
    "                               Paraboloid(invalid_x, invalid_y),\n",
    "                               promotes=['*'])\n",
    "\n",
    "    model.add_subsystem('con', om.ExecComp('c = - x + y'), promotes=['*'])\n",
    "\n",
    "    model.add_design_var('x', lower=-50.0, upper=50.0)\n",
    "    model.add_design_var('y', lower=-50.0, upper=50.0)\n",
    "\n",
    "    model.add_objective('f_xy')\n",
    "    model.add_constraint('c', upper=-15.)\n",
    "\n",
    "    # pyOptSparseDriver with selected optimizer\n",
    "    driver = om.pyOptSparseDriver(optimizer=optimizer)\n",
    "    if optimizer == 'IPOPT':\n",
    "        driver.opt_settings['file_print_level'] = 5\n",
    "    driver.options['print_results'] = False\n",
    "    driver.options['output_dir'] = None  # will put the optimizer output file in the current directory\n",
    "\n",
    "    # setup problem & initialize values\n",
    "    prob = om.Problem(model, driver)\n",
    "    prob.setup()\n",
    "\n",
    "    prob.set_val('x', 50)\n",
    "    prob.set_val('y', 50)\n",
    "\n",
    "    return prob, comp"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example using IPOPT\n",
    "\n",
    "First we will run the Paraboloid optimization as normal, without raising any errors. In doing this, we can see the nominal path that the optimizer follows throught solution space to arrive at the optimum.  For this initial case, we will use the `IPOPT` optimizer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob, comp = setup_problem('IPOPT')\n",
    "prob.run_driver()\n",
    "\n",
    "for (x, y, f_xy) in comp.eval_history:\n",
    "    print(f\"x: {x:9.5f}  y: {y:9.5f}  f_xy: {f_xy:10.5f}\")"
   ]
  },
  {
   "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['x'], 7.166667, 1e-6)\n",
    "assert_near_equal(prob['y'], -7.833334, 1e-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we will define our invalid region as `x` between 7.2 and 10.2 and `y` between -50 and -10.  This region was chosen as it is crossed in the course of the nominal optimization from our chosen starting point at `x=50`, `y=50`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "invalid_x = (7.2, 10.2)\n",
    "invalid_y = (-50., -40.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will recreate the problem using this invalid region and see that the optimizer's path to the optimum now must reroute around the invalid values. It will take many more iterations to get to the solution, but IPOPT still gets there in the end:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "prob, comp = setup_problem('IPOPT', invalid_x, invalid_y)\n",
    "prob.run_driver()\n",
    "\n",
    "for i, (x, y, f_xy) in enumerate(comp.eval_history):\n",
    "    print(f\"{i:2d}  x: {x:9.5f}  y: {y:9.5f}  f_xy: {f_xy:10.5f}\")"
   ]
  },
  {
   "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['x'], 7.166667, 1e-6)\n",
    "assert_near_equal(prob['y'], -7.833334, 1e-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see how many times our Component raised an AnalysisError and at which iteration they occurred:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Number of errors: {len(comp.raised_eval_errors)}\")\n",
    "print(f\"Iterations:{comp.raised_eval_errors}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looking at the IPOPT output file (`IPOPT.out`) will reveal what happened when the optimizer encountered these bad points. Here we just show a relevant subsection of the file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(prob.get_outputs_dir() / \"IPOPT.out\", encoding=\"utf-8\") as f:\n",
    "    IPOPT_history = f.read()\n",
    "beg = IPOPT_history.find(\"iter    objective\")\n",
    "end = IPOPT_history.find(\"(scaled)\", beg)\n",
    "print(IPOPT_history[beg:end])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Specifically, we can see the following message when IPOPT changes its search in response to the bad point:\n",
    "\n",
    "    Warning: Cutting back alpha due to evaluation error"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count = 0\n",
    "\n",
    "for line in IPOPT_history.split('\\n'):\n",
    "    if 'Cutting back alpha' in line:\n",
    "        print(line)\n",
    "        count = count + 1\n",
    "\n",
    "print(\"\\nNumber of times IPOPT encountered an evaluation error:\", count)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example using SNOPT\n",
    "\n",
    "We can exercise the same model using `SNOPT` as our optimizer, with similar results. First we will run the nominal case, and then again with the invalid region:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob, comp = setup_problem('SNOPT')\n",
    "prob.run_driver()\n",
    "\n",
    "for i, (x, y, f_xy) in enumerate(comp.eval_history):\n",
    "    print(f\"{i:2d}  x: {x:9.5f}  y: {y:9.5f}  f_xy: {f_xy:10.5f}\")"
   ]
  },
  {
   "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['x'], 7.166667, 1e-6)\n",
    "assert_near_equal(prob['y'], -7.833334, 1e-6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "prob, comp = setup_problem('SNOPT', invalid_x, invalid_y)\n",
    "prob.run_driver()\n",
    "\n",
    "for i, (x, y, f_xy) in enumerate(comp.eval_history):\n",
    "    print(f\"{i:2d}  x: {x:9.5f}  y: {y:9.5f}  f_xy: {f_xy:10.5f}\")"
   ]
  },
  {
   "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['x'], 7.166667, 1e-6)\n",
    "assert_near_equal(prob['y'], -7.833334, 1e-6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Number of errors: {len(comp.raised_eval_errors)}\")\n",
    "print(f\"Iterations:{comp.raised_eval_errors}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(len(comp.raised_eval_errors) == 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case we can see that we raised a single AnalysisError.  We can again find evidence of SNOPT encountering this evaluation error in the `SNOPT_print.out` file, but still finding the solution. For SNOPT, we are looking for the `D` code at the end of an iteration. Here again we just show a relevant subsection of the file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(prob.get_outputs_dir() / \"SNOPT_print.out\", encoding=\"utf-8\", errors='ignore') as f:\n",
    "    SNOPT_history = f.read()\n",
    "beg = SNOPT_history.find(\"   Itns Major Minor\")\n",
    "end = SNOPT_history.find(\"Problem name\", beg)\n",
    "print(SNOPT_history[beg:end])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "count = 0\n",
    "\n",
    "for line in SNOPT_history.split('\\n'):\n",
    "    if line.endswith(' D'):\n",
    "        print(line)\n",
    "        count = count + 1\n",
    "\n",
    "print(\"\\nNumber of times SNOPT encountered an evaluation error:\", count)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{Note}\n",
    "Not all optimizers will respond as nicely to an AnalysisError as the two demonstrated here (`IPOPT` and `SNOPT`).  Some optimizers may fail to navigate around the bad region and find a solution at all.  Other may find an incorrect solution.  It is important to understand the capabilities of your chosen optimizer when working with a model that may raise an AnalysisError.\n",
    "```\n"
   ]
  }
 ],
 "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.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
