{
 "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": [
    "# Linking Variables with Promotion vs. Connection\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the previous tutorial, we built up a model of the Sellar problem using two disciplinary components and a few `ExecComps`.\n",
    "In order to get OpenMDAO to pass the data between all the components,\n",
    "we linked everything up using promoted variables so that data passed from outputs to inputs with the same promoted name.\n",
    "\n",
    "Promoting variables is often a convenient way to establish the data passing links from outputs to inputs.\n",
    "However, you can also use calls to the `connect` method in order to link outputs to inputs without having to\n",
    "promote anything.\n",
    "Here is how you would define the same Sellar model using:\n",
    "\n",
    "1. Variable promotion\n",
    "2. Connect statements\n",
    "3. Both variable promotion and connect statements\n",
    "\n",
    "All three will give the exact same answer, but the way you address the variables will be slightly different in each one."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variable Promotion\n",
    "Input and output variables can be promoted when a subsystem is added:"
   ]
  },
  {
   "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_src5\", get_code(\"openmdao.test_suite.components.sellar.SellarDis1\"), display=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ":::{Admonition} `SellarDis1` class definition \n",
    ":class: dropdown\n",
    "\n",
    "{glue:}`code_src5`\n",
    ":::"
   ]
  },
  {
   "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_src6\", get_code(\"openmdao.test_suite.components.sellar.SellarDis2\"), display=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ":::{Admonition} `SellarDis2` class definition \n",
    ":class: dropdown\n",
    "\n",
    "{glue:}`code_src6`\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import openmdao.api as om\n",
    "\n",
    "from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2\n",
    "\n",
    "class SellarMDA(om.Group):\n",
    "    \"\"\"\n",
    "    Group containing the Sellar MDA.\n",
    "    \"\"\"\n",
    "\n",
    "    def setup(self):\n",
    "        cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])\n",
    "        cycle.add_subsystem('d1', SellarDis1(),\n",
    "                            promotes_inputs=['x', 'z', 'y2'],\n",
    "                            promotes_outputs=['y1'])\n",
    "        cycle.add_subsystem('d2', SellarDis2(),\n",
    "                            promotes_inputs=['z', 'y1'],\n",
    "                            promotes_outputs=['y2'])\n",
    "\n",
    "        cycle.set_input_defaults('x', 1.0)\n",
    "        cycle.set_input_defaults('z', np.array([5.0, 2.0]))\n",
    "\n",
    "        # Nonlinear Block Gauss Seidel is a gradient free solver\n",
    "        cycle.nonlinear_solver = om. NonlinearBlockGS()\n",
    "\n",
    "        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',\n",
    "                                                  z=np.array([0.0, 0.0]), x=0.0),\n",
    "                           promotes=['x', 'z', 'y1', 'y2', 'obj'])\n",
    "\n",
    "        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'),\n",
    "                           promotes=['con1', 'y1'])\n",
    "        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'),\n",
    "                           promotes=['con2', 'y2'])\n",
    "\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model = SellarMDA()\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('x', 2.0)\n",
    "prob.set_val('z', [-1., -1.])\n",
    "\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output",
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "assert_near_equal((prob.get_val('y1')[0], prob.get_val('y2')[0], prob.get_val('obj')[0], prob.get_val('con1')[0], prob.get_val('con2')[0]),\n",
    "                  (2.10951651, -0.54758253,  6.8385845,  1.05048349, -24.54758253), 1e-5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Alternatively, variables can be promoted when a group is configured:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2\n",
    "\n",
    "class SellarMDA(om.Group):\n",
    "    \"\"\"\n",
    "    Group containing the Sellar MDA.\n",
    "    \"\"\"\n",
    "\n",
    "    def setup(self):\n",
    "        # set up model hierarchy\n",
    "        cycle = self.add_subsystem('cycle', om.Group())\n",
    "        cycle.add_subsystem('d1', SellarDis1())\n",
    "        cycle.add_subsystem('d2', SellarDis2())\n",
    "\n",
    "        cycle.nonlinear_solver = om. NonlinearBlockGS()\n",
    "\n",
    "        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',\n",
    "                                                  z=np.array([0.0, 0.0]), x=0.0))\n",
    "\n",
    "        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'))\n",
    "        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'))\n",
    "\n",
    "    def configure(self):\n",
    "        # connect everything via promotes\n",
    "        self.cycle.promotes('d1', inputs=['x', 'z', 'y2'], outputs=['y1'])\n",
    "        self.cycle.promotes('d2', inputs=['z', 'y1'], outputs=['y2'])\n",
    "\n",
    "        self.promotes('cycle', any=['*'])\n",
    "\n",
    "        self.promotes('obj_cmp', any=['x', 'z', 'y1', 'y2', 'obj'])\n",
    "        self.promotes('con_cmp1', any=['con1', 'y1'])\n",
    "        self.promotes('con_cmp2', any=['con2', 'y2'])\n",
    "\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model = SellarMDA()\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('x', 2.0)\n",
    "prob.set_val('z', [-1., -1.])\n",
    "\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output",
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "    assert_near_equal((prob.get_val('y1')[0], prob.get_val('y2')[0], prob.get_val('obj')[0], prob.get_val('con1')[0], prob.get_val('con2')[0]),\n",
    "                     (2.10951651, -0.54758253,  6.8385845,  1.05048349, -24.54758253), 1e-5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There are a few important details to note:\n",
    "\n",
    "- The promoted name of an output has to be unique within that level of the hierarchy (i.e. you can't have two outputs with the same name)\n",
    "- You are allowed to have multiple inputs promoted to the same name.\n",
    "- You can use `glob <https://en.wikipedia.org/wiki/Glob_(programming)>`_ patterns to promote lots of variables without specifying them all, but try to limit your usage of `promotes=['*']`. Though it may seem like a convenient way to do things, it can make it difficult for other people who are reading your code to understand which variables are connected to each other. It is acceptable to use `promotes=['*']` in cases where it won't cause confusion, for example with `cycle`, which only exists to allow for the nonlinear solver to converge the two components. Another example of when it would be safe to use `promotes=['*']` would be if you have `ExecComps` that make it clear what the I/O of that component is anyway.\n",
    "\n",
    "```{Note}\n",
    "For a more detailed set of examples for how to promote variables, check out the [feature doc on adding sub-systems to a group](../../features/core_features/working_with_groups/add_subsystem).There are some more advanced things you can do, such as variable name aliasing and connecting a sub-set of indices from the output array of one component to the input of another.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(sellar-connect)=\n",
    "## Connect Statements\n",
    "The exact same model results can be achieved using `connect` statements instead of promotions. However, take careful note of how the variables are addressed in those connect and print statements."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2\n",
    "\n",
    "class SellarMDAConnect(om.Group):\n",
    "    \"\"\"\n",
    "    Group containing the Sellar MDA. This version uses the disciplines without derivatives.\n",
    "    \"\"\"\n",
    "\n",
    "    def setup(self):\n",
    "        cycle = self.add_subsystem('cycle', om.Group(),\n",
    "                                   promotes_inputs=['x', 'z'])\n",
    "        cycle.add_subsystem('d1', SellarDis1(),\n",
    "                            promotes_inputs=['x', 'z'])\n",
    "        cycle.add_subsystem('d2', SellarDis2(),\n",
    "                            promotes_inputs=['z'])\n",
    "        cycle.connect('d1.y1', 'd2.y1')\n",
    "        cycle.connect('d2.y2', 'd1.y2')\n",
    "\n",
    "        cycle.set_input_defaults('x', 1.0)\n",
    "        cycle.set_input_defaults('z', np.array([5.0, 2.0]))\n",
    "\n",
    "        # Nonlinear Block Gauss Seidel is a gradient free solver\n",
    "        cycle.nonlinear_solver = om.NonlinearBlockGS()\n",
    "\n",
    "        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',\n",
    "                                                  z=np.array([0.0, 0.0]), x=0.0),\n",
    "                           promotes_inputs=['x', 'z'])\n",
    "\n",
    "        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'))\n",
    "        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'))\n",
    "\n",
    "        self.connect('cycle.d1.y1', ['obj_cmp.y1', 'con_cmp1.y1'])\n",
    "        self.connect('cycle.d2.y2', ['obj_cmp.y2', 'con_cmp2.y2'])\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model = SellarMDAConnect()\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('x', 2.0)\n",
    "prob.set_val('z', [-1., -1.])\n",
    "\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal((prob.get_val('cycle.d1.y1')[0], prob.get_val('cycle.d2.y2')[0], prob.get_val('obj_cmp.obj')[0], prob.get_val('con_cmp1.con1')[0], prob.get_val('con_cmp2.con2')[0]),\n",
    "                 (2.10951651, -0.54758253, 6.8385845, 1.05048349, -24.54758253), 1e-5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Variable Promotion and Connect Statements\n",
    "\n",
    "It is also possible to combine promotion and connection in a single model.\n",
    "Here, notice that we do not have to add \"cycle\" in front of anything, because we promoted all the variables up from that group."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "from openmdao.test_suite.components.sellar import SellarDis1, SellarDis2\n",
    "\n",
    "class SellarMDAPromoteConnect(om.Group):\n",
    "    \"\"\"\n",
    "    Group containing the Sellar MDA. This version uses the disciplines without derivatives.\n",
    "    \"\"\"\n",
    "\n",
    "    def setup(self):\n",
    "        cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])\n",
    "        cycle.add_subsystem('d1', SellarDis1(),\n",
    "                            promotes_inputs=['x', 'z'])\n",
    "        cycle.add_subsystem('d2', SellarDis2(),\n",
    "                            promotes_inputs=['z'])\n",
    "        cycle.connect('d1.y1', 'd2.y1')\n",
    "        cycle.connect('d2.y2', 'd1.y2')\n",
    "\n",
    "        cycle.set_input_defaults('x', 1.0)\n",
    "        cycle.set_input_defaults('z', np.array([5.0, 2.0]))\n",
    "\n",
    "        # Nonlinear Block Gauss Seidel is a gradient free solver\n",
    "        cycle.nonlinear_solver = om.NonlinearBlockGS()\n",
    "\n",
    "        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',\n",
    "                                                  z=np.array([0.0, 0.0]), x=0.0),\n",
    "                           promotes_inputs=['x', 'z'])\n",
    "\n",
    "        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'))\n",
    "        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'))\n",
    "\n",
    "        self.connect('d1.y1', ['con_cmp1.y1', 'obj_cmp.y1'])\n",
    "        self.connect('d2.y2', ['con_cmp2.y2', 'obj_cmp.y2'])\n",
    "\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model = SellarMDAPromoteConnect()\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('x', 2.0)\n",
    "prob.set_val('z', [-1., -1.])\n",
    "\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal((prob.get_val('cycle.d1.y1')[0], prob.get_val('cycle.d2.y2')[0], prob.get_val('obj_cmp.obj')[0], prob.get_val('con_cmp1.con1')[0], prob.get_val('con_cmp2.con2')[0]),\n",
    "                 (2.10951651, -0.54758253, 6.8385845, 1.05048349, -24.54758253), 1e-5)"
   ]
  }
 ],
 "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
}
