{
 "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": [
    "# ExecComp\n",
    "\n",
    "\n",
    "`ExecComp` is a component that provides a shortcut for building an ExplicitComponent that\n",
    "represents a set of simple mathematical relationships between inputs and outputs. The ExecComp\n",
    "automatically takes care of all of the component API methods, so you just need to instantiate\n",
    "it with an equation or a list of equations.\n",
    "\n",
    "## ExecComp Options\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "om.show_options_table(\"openmdao.components.exec_comp.ExecComp\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Constructor\n",
    "\n",
    "The call signature for the `ExecComp` constructor is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.components.exec_comp.ExecComp.__init__\n",
    "        :noindex:\n",
    "```"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The values of the `kwargs` can be `dicts` which define the initial value for the variables along with\n",
    "other metadata. For example,\n",
    "\n",
    "```\n",
    "    ExecComp('xdot=x/t', x={'units': 'ft'}, t={'units': 's'}, xdot={'units': 'ft/s')\n",
    "```\n",
    "\n",
    "Here is a list of the possible metadata that can be assigned to a variable in this way. The **Applies To** column indicates\n",
    "whether the metadata is appropriate for input variables, output variables, or both.\n",
    "\n",
    "```{eval-rst}\n",
    "================  ====================================================== ============================================================= ==============  ========\n",
    "Name              Description                                            Valid Types                                                   Applies To      Default\n",
    "================  ====================================================== ============================================================= ==============  ========\n",
    "value             Initial value in user-defined units                    float, list, tuple, ndarray                                   input & output  1\n",
    "shape             Variable shape, only needed if not an array            int, tuple, list, None                                        input & output  None\n",
    "shape_by_conn     Determine variable shape based on its connection       bool                                                          input & output  False\n",
    "copy_shape        Determine variable shape based on named variable       str                                                           input & output  None\n",
    "units             Units of variable                                      str, None                                                     input & output  None\n",
    "desc              Description of variable                                str                                                           input & output  \"\"\n",
    "res_units         Units of residuals                                     str, None                                                     output          None\n",
    "ref               Value of variable when scaled value is 1               float, ndarray                                                output          1\n",
    "ref0              Value of variable when scaled value is 0               float, ndarray                                                output          0\n",
    "res_ref           Value of residual when scaled value is 1               float, ndarray                                                output          1\n",
    "lower             Lower bound of variable                                float, list, tuple, ndarray, Iterable, None                   output          None\n",
    "upper             Lower bound of variable                                float, list, tuple, ndarray, Iterable, None                   output          None\n",
    "src_indices       Global indices of the variable                         int, list of ints, tuple of ints, int ndarray, Iterable, None input           None\n",
    "flat_src_indices  If True, src_indices are indices into flattened source bool                                                          input           None\n",
    "tags              Used to tag variables for later filtering              str, list of strs                                             input & output  None\n",
    "================  ====================================================== ============================================================= ==============  ========\n",
    "```\n",
    "\n",
    "These metadata are passed to the `Component` methods `add_input` and `add_output`.\n",
    "For more information about these metadata, see the documentation for the arguments to these methods on [Component](../../../_srcdocs/packages/core/component).\n",
    "\n",
    "## Registering User Functions\n",
    "\n",
    "To get your own functions added to the internal namespace of ExecComp so you can call them\n",
    "from within an ExecComp expression, you can use the `ExecComp.register` function.\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.components.exec_comp.ExecComp.register\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "Note that you're required, when registering a new function, to indicate whether that function\n",
    "is complex safe or not.\n",
    "\n",
    "\n",
    "## ExecComp Example: Simple\n",
    "\n",
    "For example, here is a simple component that takes the input and adds one to it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp('y=x+1.'))\n",
    "\n",
    "model.set_input_defaults('comp.x', 2.0)\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.y'))"
   ]
  },
  {
   "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('comp.y'), 3.0, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Multiple Outputs\n",
    "\n",
    "You can also create an ExecComp with multiple outputs by placing the expressions in a list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp(['y1=x+1.', 'y2=x-1.']), promotes=['x'])\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('x', 2.0)\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.y1'))\n",
    "print(prob.get_val('comp.y2'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('comp.y1'), 3.0, 0.00001)\n",
    "assert_near_equal(prob.get_val('comp.y2'), 1.0, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Arrays\n",
    "\n",
    "You can declare an ExecComp with arrays for inputs and outputs, but when you do, you must also\n",
    "pass in a correctly-sized array as an argument to the ExecComp call, or set the 'shape' metadata\n",
    "for that variable as described earlier. If specifying the value directly, it can be the initial value\n",
    "in the case of unconnected inputs, or just an empty array with the correct size."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp('y=x[1]',\n",
    "                                        x=np.array([1., 2., 3.]),\n",
    "                                        y=0.0))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('comp.y'), 2.0, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Math Functions\n",
    "\n",
    "Functions from the math library are available for use in the expression strings."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp('z = sin(x)**2 + cos(y)**2'))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('comp.x', np.pi/2.0)\n",
    "prob.set_val('comp.y', np.pi/2.0)\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.z'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('comp.z'), 1.0, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Variable Properties\n",
    "\n",
    "You can also declare properties like 'units', 'upper', or 'lower' on the inputs and outputs. In this\n",
    "example we declare all our inputs to be inches to trigger conversion from a variable expressed in feet\n",
    "in one connection source."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp('z=x+y',\n",
    "                                        x={'val': 0.0, 'units': 'inch'},\n",
    "                                        y={'val': 0.0, 'units': 'inch'},\n",
    "                                        z={'val': 0.0, 'units': 'inch'}))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('comp.x', 12.0, units='inch')\n",
    "prob.set_val('comp.y', 1.0, units='ft')\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.z'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('comp.z'), 24.0, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Diagonal Partials\n",
    "\n",
    "If all of your ExecComp's array inputs and array outputs are the same size and happen to have\n",
    "diagonal partials, you can make computation of derivatives for your ExecComp faster by specifying a\n",
    "`has_diag_partials=True` argument\n",
    "to `__init__` or via the component options. This will cause the ExecComp to solve for its partials\n",
    "by complex stepping all entries of an array input at once instead of looping over each entry individually."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "p = om.Problem()\n",
    "model = p.model\n",
    "\n",
    "model.add_subsystem('comp', om.ExecComp('y=3.0*x + 2.5',\n",
    "                                        has_diag_partials=True,\n",
    "                                        x=np.ones(5), y=np.ones(5)))\n",
    "\n",
    "p.setup()\n",
    "\n",
    "p.set_val('comp.x', np.ones(5))\n",
    "\n",
    "p.run_model()\n",
    "\n",
    "J = p.compute_totals(of=['comp.y'], wrt=['comp.x'], return_format='array')\n",
    "\n",
    "print(J)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from numpy.testing import assert_almost_equal\n",
    "\n",
    "assert_almost_equal(J, np.eye(5)*3., decimal=6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Options\n",
    "\n",
    "Other options that can apply to all the variables in the component are variable shape and units.\n",
    "These can also be set as a keyword argument in the constructor or via the component options. In the\n",
    "following example the variables all share the same shape, which is specified in the constructor, and\n",
    "common units that are specified by setting the option."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = om.Group()\n",
    "\n",
    "xcomp = model.add_subsystem('comp', om.ExecComp('y=2*x', shape=(2,)))\n",
    "\n",
    "xcomp.options['units'] = 'm'\n",
    "\n",
    "prob = om.Problem(model)\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('comp.x', [100., 200.], units='cm')\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('comp.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('comp.y'), [2., 4.], 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: User function registration\n",
    "\n",
    "If the function is complex safe, then you don't need to do anything differently than you\n",
    "would for any other ExecComp."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try: \n",
    "    om.ExecComp.register(\"myfunc\", lambda x: x * x, complex_safe=True)\n",
    "except NameError:\n",
    "    pass\n",
    "p = om.Problem()\n",
    "comp = p.model.add_subsystem(\"comp\", om.ExecComp(\"y = 2 * myfunc(x)\"))\n",
    "\n",
    "p.setup()\n",
    "p.run_model()\n",
    "J = p.compute_totals(of=['comp.y'], wrt=['comp.x'])\n",
    "print(J['comp.y', 'comp.x'][0][0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(J['comp.y', 'comp.x'][0][0], 4., 1e-10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Complex unsafe user function registration\n",
    "\n",
    "If the function isn't complex safe, then derivatives involving that function\n",
    "will have to be computed using finite difference instead of complex step.  The way to specify\n",
    "that `fd` should be used for a given derivative is to call `declare_partials`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    om.ExecComp.register(\"unsafe\", lambda x: x * x, complex_safe=False)\n",
    "except NameError:\n",
    "    pass\n",
    "p = om.Problem()\n",
    "comp = p.model.add_subsystem(\"comp\", om.ExecComp(\"y = 2 * unsafe(x)\"))\n",
    "\n",
    "# because our function is complex unsafe, we must declare that the partials\n",
    "# with respect to 'x' use 'fd' instead of 'cs'\n",
    "comp.declare_partials('*', 'x', method='fd')\n",
    "\n",
    "p.setup()\n",
    "p.run_model()\n",
    "J = p.compute_totals(of=['comp.y'], wrt=['comp.x'])\n",
    "print(J['comp.y', 'comp.x'][0][0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(J['comp.y', 'comp.x'][0][0], 4., 1e-5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Adding Expressions\n",
    "\n",
    "You can add additional expressions to an `ExecComp` with the \"add_expr\" method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "class ConfigGroup(om.Group):\n",
    "    def setup(self):\n",
    "        excomp = om.ExecComp('y=x',\n",
    "                             x={'val' : 3.0, 'units' : 'mm'},\n",
    "                             y={'shape' : (1, ), 'units' : 'cm'})\n",
    "\n",
    "        self.add_subsystem('excomp', excomp, promotes=['*'])\n",
    "\n",
    "    def configure(self):\n",
    "        self.excomp.add_expr('z = 2.9*x',\n",
    "                             z={'shape' : (1, ), 'units' : 's'})\n",
    "\n",
    "p = om.Problem()\n",
    "p.model.add_subsystem('sub', ConfigGroup(), promotes=['*'])\n",
    "p.setup()\n",
    "p.run_model()\n",
    "\n",
    "print(p.get_val('z'))\n",
    "print(p.get_val('y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_almost_equal(p.get_val('z'), 8.7, 1e-8)\n",
    "assert_almost_equal(p.get_val('y'), 3.0, 1e-8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ExecComp Example: Constants\n",
    "\n",
    "You can define variables in `ExecComp` to be constants.\n",
    "Here is a simple model in which no constants are used. Notice the names of the inputs that are printed in the two models."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "C1 = prob.model.add_subsystem('C1', om.ExecComp('x = a + b'))\n",
    "prob.setup()\n",
    "prob.set_solver_print(level=0)\n",
    "prob.run_model()\n",
    "print(list(C1._inputs._names))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert('a' in C1._inputs)\n",
    "assert('b' in C1._inputs)\n",
    "assert('x' in C1._outputs)\n",
    "assert_near_equal(C1._outputs['x'], 2.0, 0.00001)"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "Here is the same model only with one of the variables as a constant."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "C1 = prob.model.add_subsystem('C1', om.ExecComp('x = a + b', a={'val': 6, 'constant':True}))\n",
    "prob.setup()\n",
    "prob.set_solver_print(level=0)\n",
    "prob.run_model()\n",
    "print(list(C1._inputs._names))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert('a' not in C1._inputs)\n",
    "assert('b' in C1._inputs)\n",
    "assert('x' in C1._outputs)\n",
    "assert_near_equal(C1._outputs['x'], 7.0, 0.00001)"
   ]
  }
 ],
 "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
}
