{
 "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\n",
    "except ImportError:\n",
    "    !python -m pip install openmdao[notebooks]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ExternalCodeImplicitComp\n",
    "\n",
    "`ExternalCodeImplicitComp` is very similar to `ExternalCodeComp` in that it runs an external program in a subprocess on your operating system. But it treats the `Component` as an `ImplicitComponent` rather than an `ExplicitComponent`. See [ExternalCodeComp](external_code_comp.ipynb) for basic information about how `ExternalCodeComp` works.\n",
    "\n",
    "`ExternalCodeImplicitComp` has most of the same options as `ExternalCodeComp`, but there is one major difference."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "om.show_options_table(\"openmdao.components.external_code_comp.ExternalCodeImplicitComp\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When using an `ExternalCodeImplicitComp`, you have the option to define two external programs rather than one. The\n",
    "first of these is \"command_apply\", which is the command that you want to run to evaluate the residuals. You should\n",
    "always specify a value for this option. The second is \"command_solve\", which is the command that you want to run\n",
    "to let the external program solve its own states. This is optional, but you should specify it if your code can\n",
    "solve itself, and if you want it to do so (for example, while using a Newton solver with \"solve_subsystems\" turned\n",
    "on in a higher-level `Group`.)\n",
    "\n",
    "## ExternalCodeImplicitComp Constructor\n",
    "\n",
    "The call signature for the `ExternalCodeImplicitComp` constructor is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.components.external_code_comp.ExternalCodeImplicitComp.__init__\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "## ExternalCodeImplicitComp Example\n",
    "\n",
    "Here is a simple example of the use of an `ExternalCodeImplicitComp` Component. The external code in the example\n",
    "is a Python script that evaluates the output and residual for the implicit relationship between the area ratio and\n",
    "mach number in an isentropic flow. We use the same external code for both \"command_apply\" and \"command_solve\", but\n",
    "in each case we pass it different flags.\n",
    "\n",
    "```python\n",
    "#!/usr/bin/env python\n",
    "#\n",
    "# usage: extcode_mach.py input_filename output_filename\n",
    "#\n",
    "# Evaluates the output and residual for the implicit relationship\n",
    "#     between the area ratio and mach number.\n",
    "#\n",
    "# Read the value of `area_ratio` from input file\n",
    "# and writes the values or residuals of `mach` to output file depending on what is requested.\n",
    "# What is requested is given by the first line in the file read. It can be either 'residuals' or\n",
    "# 'outputs'.\n",
    "\n",
    "def area_ratio_explicit(mach):\n",
    "    \"\"\"Explicit isentropic relationship between area ratio and Mach number\"\"\"\n",
    "    gamma = 1.4\n",
    "    gamma_p_1 = gamma + 1\n",
    "    gamma_m_1 = gamma - 1\n",
    "    exponent = gamma_p_1 / (2 * gamma_m_1)\n",
    "    return (gamma_p_1 / 2.) ** -exponent * (\n",
    "            (1 + gamma_m_1 / 2. * mach ** 2) ** exponent) / mach\n",
    "\n",
    "def mach_residual(mach, area_ratio_target):\n",
    "    \"\"\"If area_ratio is known, then finding Mach is an implicit relationship\"\"\"\n",
    "    return area_ratio_target - area_ratio_explicit(mach)\n",
    "\n",
    "def mach_solve(area_ratio, super_sonic=False):\n",
    "    \"\"\"Solve for mach, given area ratio\"\"\"\n",
    "    if super_sonic:\n",
    "        initial_guess = 4\n",
    "    else:\n",
    "        initial_guess = .1\n",
    "    mach = fsolve(func=mach_residual, x0=initial_guess, args=(area_ratio,))[0]\n",
    "    return mach\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    import sys\n",
    "    from scipy.optimize import fsolve\n",
    "\n",
    "    input_filename = sys.argv[1]\n",
    "    output_filename = sys.argv[2]\n",
    "\n",
    "    with open(input_filename, 'r') as input_file:\n",
    "        output_or_resids = input_file.readline().strip()\n",
    "        area_ratio = float(input_file.readline())\n",
    "        if output_or_resids == 'residuals':\n",
    "            mach = float(input_file.readline())\n",
    "        else: # outputs\n",
    "            super_sonic = (input_file.readline().strip() == \"True\")\n",
    "\n",
    "    if output_or_resids == 'outputs':\n",
    "        mach_output = mach_solve(area_ratio, super_sonic=super_sonic)\n",
    "        with open(output_filename, 'w') as output_file:\n",
    "            output_file.write('%.16f\\n' % mach_output)\n",
    "\n",
    "    elif output_or_resids == 'residuals':\n",
    "        mach_resid = mach_residual(mach, area_ratio)\n",
    "        with open(output_filename, 'w') as output_file:\n",
    "            output_file.write('%.16f\\n' % mach_resid)\n",
    "\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from scipy.optimize import fsolve\n",
    "\n",
    "def area_ratio_explicit(Mach):\n",
    "    \"\"\"isentropic relationship between area ratio and Mach number\"\"\"\n",
    "    gamma = 1.4\n",
    "    gamma_p_1 = gamma + 1\n",
    "    gamma_m_1 = gamma - 1\n",
    "    exponent = gamma_p_1 / (2 * gamma_m_1)\n",
    "\n",
    "    return (gamma_p_1 / 2.) ** -exponent * ((1 + gamma_m_1 / 2. * Mach ** 2) ** exponent) / Mach\n",
    "\n",
    "def mach_residual(Mach, area_ratio_target):\n",
    "    return area_ratio_target - area_ratio_explicit(Mach)\n",
    "\n",
    "def mach_solve(area_ratio, super_sonic=False):\n",
    "    if super_sonic:\n",
    "        initial_guess = 4\n",
    "    else:\n",
    "        initial_guess = .1\n",
    "\n",
    "    mach = fsolve(func=mach_residual, x0=initial_guess, args=(area_ratio,))[0]\n",
    "\n",
    "    return mach"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "import openmdao.api as om\n",
    "\n",
    "\n",
    "class MachExternalCodeComp(om.ExternalCodeImplicitComp):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('super_sonic', types=bool)\n",
    "\n",
    "    def setup(self):\n",
    "        self.add_input('area_ratio', val=1.0, units=None)\n",
    "        self.add_output('mach', val=1., units=None)\n",
    "\n",
    "        self.input_file = 'mach_input.dat'\n",
    "        self.output_file = 'mach_output.dat'\n",
    "\n",
    "        # providing these are optional; the component will verify that any input\n",
    "        # files exist before execution and that the output files exist after.\n",
    "        self.options['external_input_files'] = [self.input_file]\n",
    "        self.options['external_output_files'] = [self.output_file]\n",
    "\n",
    "\n",
    "        self.options['command_apply'] = [\n",
    "            sys.executable, 'extcode_mach.py', self.input_file, self.output_file,\n",
    "        ]\n",
    "        self.options['command_solve'] = [\n",
    "            sys.executable, 'extcode_mach.py', self.input_file, self.output_file,\n",
    "        ]\n",
    "\n",
    "        # If you want to write your own string command, the code below will also work.\n",
    "        # self.options['command_apply'] = ('python extcode_mach.py {} {}').format(self.input_file, self.output_file)\n",
    "\n",
    "    def setup_partials(self):\n",
    "        self.declare_partials(of='mach', wrt='area_ratio', method='fd')\n",
    "\n",
    "    def apply_nonlinear(self, inputs, outputs, residuals):\n",
    "        with open(self.input_file, 'w') as input_file:\n",
    "            input_file.write('residuals\\n')\n",
    "            input_file.write('{}\\n'.format(inputs['area_ratio'][0]))\n",
    "            input_file.write('{}\\n'.format(outputs['mach'][0]))\n",
    "\n",
    "        # the parent apply_nonlinear function actually runs the external code\n",
    "        super().apply_nonlinear(inputs, outputs, residuals)\n",
    "\n",
    "        # parse the output file from the external code and set the value of mach\n",
    "        with open(self.output_file, 'r') as output_file:\n",
    "            mach = float(output_file.read())\n",
    "        residuals['mach'] = mach\n",
    "\n",
    "    def solve_nonlinear(self, inputs, outputs):\n",
    "        with open(self.input_file, 'w') as input_file:\n",
    "            input_file.write('outputs\\n')\n",
    "            input_file.write('{}\\n'.format(inputs['area_ratio'][0]))\n",
    "            input_file.write('{}\\n'.format(self.options['super_sonic']))\n",
    "        # the parent apply_nonlinear function actually runs the external code\n",
    "        super().solve_nonlinear(inputs, outputs)\n",
    "\n",
    "        # parse the output file from the external code and set the value of mach\n",
    "        with open(self.output_file, 'r') as output_file:\n",
    "            mach = float(output_file.read())\n",
    "        outputs['mach'] = mach\n",
    "\n",
    "group = om.Group()\n",
    "mach_comp = group.add_subsystem('comp', MachExternalCodeComp(), promotes=['*'])\n",
    "prob = om.Problem(model=group)\n",
    "group.nonlinear_solver = om.NewtonSolver()\n",
    "group.nonlinear_solver.options['solve_subsystems'] = True\n",
    "group.nonlinear_solver.options['iprint'] = 0\n",
    "group.nonlinear_solver.options['maxiter'] = 20\n",
    "group.linear_solver = om.DirectSolver()\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "area_ratio = 1.3\n",
    "super_sonic = False\n",
    "prob.set_val('area_ratio', area_ratio)\n",
    "mach_comp.options['super_sonic'] = super_sonic\n",
    "prob.run_model()\n",
    "print(prob.get_val('mach'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "assert_near_equal(prob.get_val('mach'), mach_solve(area_ratio, super_sonic=super_sonic), 1e-8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "area_ratio = 1.3\n",
    "super_sonic = True\n",
    "prob.set_val('area_ratio', area_ratio)\n",
    "mach_comp.options['super_sonic'] = super_sonic\n",
    "prob.run_model()\n",
    "print(prob.get_val('mach'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('mach'), mach_solve(area_ratio, super_sonic=super_sonic), 1e-8)"
   ]
  }
 ],
 "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.8.5"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
