{
 "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": [
    "# The `set_input_defaults` function\n",
    "\n",
    "\n",
    "The `set_input_defaults` function in OpenMDAO is used to specify metadata for inputs that are promoted to the same name within a Group. This is necessary when multiple inputs within a Group are promoted to the same name, but their units or initial values differ. If `set_input_defaults` is not used in this scenario, OpenMDAO will raise an error during setup.\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.core.group.Group.set_input_defaults\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "This function does not set the actual values of the inputs, only the metadata that will be used to populate the [AutoIVC](../../../other_useful_docs/auto_ivc_api_translation.ipynb) output connected to them. The metadata specified via `set_input_defaults` is applied during the model setup phase, when the AutoIVC connections are resolved.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example #1\n",
    "\n",
    "In this example, we have two components that promote the variable `x` but use different units.  This will result in an error during `setup` if left unresolved:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "p1 = om.Problem()\n",
    "model = p1.model\n",
    "\n",
    "# Note that units and value to use for the promoted variable 'x' are ambiguous due to having different defaults\n",
    "model.add_subsystem('C1', om.ExecComp('y = 3.*x', x={'val': 3000., 'units': 'mm'}), promotes=['x'])\n",
    "model.add_subsystem('C2', om.ExecComp('y = 4.*x', x={'val': 400., 'units': 'cm'}), promotes=['x'])\n",
    "\n",
    "p1.setup()\n",
    "try:\n",
    "    p1.final_setup()\n",
    "except Exception as err:\n",
    "    print(str(err))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output",
     "allow-assert"
    ]
   },
   "outputs": [],
   "source": [
    "p1.setup()\n",
    "try:\n",
    "    p1.final_setup()\n",
    "except Exception as err:\n",
    "    assert(\"<model> <class Group>: The following inputs promoted to 'x' have different units:\"\n",
    "           \"\\n  \"\n",
    "           \"\\n   C1.x  mm\"\n",
    "           \"\\n   C2.x  cm\"\n",
    "           \"\\n  \"\n",
    "           \"\\n   Call model.set_input_defaults('x', units=?) to remove the ambiguity.\" in str(err))\n",
    "else:\n",
    "    raise RuntimeError(\"Exception expected.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ambiguity can be resolved by setting the desired default values for the promoted variable at the group level by calling `set_input_defaults`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "model.set_input_defaults('x', val=1., units='m')\n",
    "\n",
    "# Note that the we have specified the default metadata for the AutoIVC output that will supply values\n",
    "# for the promoted 'x' (_auto_ivc.v0) but it has not been applied yet. It will be applied during setup.\n",
    "\n",
    "# Calling list_vars (or list_inputs/list_outputs) at this point will show the default values from the component definitions\n",
    "model.list_vars(units=True, list_autoivcs=True);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p1.setup()\n",
    "\n",
    "# now the default value and units for the AutoIVC have been applied\n",
    "model.list_vars(units=True, list_autoivcs=True);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p1.run_model()\n",
    "\n",
    "model.list_vars(units=True, list_autoivcs=True);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output",
     "allow-assert"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(p1.get_val(\"C1.y\"), 3000.)\n",
    "assert_near_equal(p1.get_val(\"C2.y\"), 400.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example #2\n",
    "\n",
    "For nested groups, `set_input_defaults` can be called in the sub-group's `configure` method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "class MyGroup(om.Group):\n",
    "    def setup(self):\n",
    "        self.add_subsystem('C1', om.ExecComp('y = 3.*x', x={'val': 3000., 'units': 'mm'}), promotes=['x'])\n",
    "        self.add_subsystem('C2', om.ExecComp('y = 4.*x', x={'val': 400., 'units': 'cm'}), promotes=['x'])\n",
    "\n",
    "    def configure(self):\n",
    "        self.set_input_defaults('x', val=1., units='m')\n",
    "\n",
    "p2 = om.Problem()\n",
    "model = p2.model\n",
    "\n",
    "model.add_subsystem('G', MyGroup())\n",
    "\n",
    "p2.setup()\n",
    "p2.run_model()\n",
    "\n",
    "model.list_vars(units=True, list_autoivcs=True);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output",
     "allow-assert"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(p2.get_val(\"G.C1.y\"), 3000.)\n",
    "assert_near_equal(p2.get_val(\"G.C2.y\"), 400.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example #3\n",
    "\n",
    "The above examples demonstrate the use of `set_input_defaults` to disambiguate default values and units. The following example shows how to use the `src_shape` argument to specify the shape of an input that may be different then the promoted variables:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import openmdao.api as om\n",
    "\n",
    "class MyComp1(om.ExplicitComponent):\n",
    "    \"\"\" multiplies input array by 2. \"\"\"\n",
    "    def setup(self):\n",
    "        self.add_input('x', np.ones(3))\n",
    "        self.add_output('y', 1.0)\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = np.sum(inputs['x'])*2.0\n",
    "\n",
    "class MyComp2(om.ExplicitComponent):\n",
    "    \"\"\" multiplies input array by 4. \"\"\"\n",
    "    def setup(self):\n",
    "        self.add_input('x', np.ones(2))\n",
    "        self.add_output('y', 1.0)\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = np.sum(inputs['x'])*4.0\n",
    "\n",
    "class MyGroup(om.Group):\n",
    "    def setup(self):\n",
    "        self.add_subsystem('comp1', MyComp1())\n",
    "        self.add_subsystem('comp2', MyComp2())\n",
    "\n",
    "    def configure(self):\n",
    "        # splits input via promotes using src_indices\n",
    "        self.promotes('comp1', inputs=['x'], src_indices=[0, 1, 2])\n",
    "        self.promotes('comp2', inputs=['x'], src_indices=[3, 4])\n",
    "\n",
    "p3 = om.Problem()\n",
    "\n",
    "# Note: src_shape is different that the shape of either target\n",
    "p3.model.set_input_defaults('x', src_shape=(5,), val=1.)\n",
    "\n",
    "p3.model.add_subsystem('G1', MyGroup(), promotes_inputs=['x'])\n",
    "\n",
    "p3.setup()\n",
    "p3.final_setup()\n",
    "\n",
    "p3.get_val('x')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output",
     "allow-assert"
    ]
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(p3.get_val('x'), np.ones((5,)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Example #4\n",
    "\n",
    "The `set_input_defaults` function can be used to set default values for discrete variables as well.\n",
    "\n",
    "In the following example, the intent is to set the material for all objects by promoting it as a discrete variable from each component and setting it for the whole model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import math\n",
    "import openmdao.api as om\n",
    "\n",
    "density = {\n",
    "    'steel': 7.85,  # g/cm^3\n",
    "    'aluminum': 2.7  # g/cm^3\n",
    "}\n",
    "\n",
    "class SquarePlate(om.ExplicitComponent):\n",
    "    \"\"\"\n",
    "    Calculate the weight of a square plate.\n",
    "    material is a discrete input (default: steel)\n",
    "    \"\"\"\n",
    "    def setup(self):\n",
    "        self.add_discrete_input('material', 'steel')\n",
    "\n",
    "        self.add_input('length', 1.0, units='cm')\n",
    "        self.add_input('width', 1.0, units='cm')\n",
    "        self.add_input('thickness', 1.0, units='cm')\n",
    "\n",
    "        self.add_output('weight', 1.0, units='g')\n",
    "\n",
    "    def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):\n",
    "        length = inputs['length']\n",
    "        width = inputs['width']\n",
    "        thickness = inputs['thickness']\n",
    "        material = discrete_inputs['material']\n",
    "\n",
    "        outputs['weight'] = length * width * thickness * density[material]\n",
    "\n",
    "class CirclePlate(om.ExplicitComponent):\n",
    "    \"\"\"\n",
    "    Calculate the weight of a circular plate.\n",
    "    material is a discrete input (default: aluminum)\n",
    "    \"\"\"\n",
    "    def setup(self):\n",
    "        self.add_discrete_input('material', 'aluminum')\n",
    "\n",
    "        self.add_input('radius', 1.0, units='cm')\n",
    "        self.add_input('thickness', 1.0, units='g')\n",
    "\n",
    "        self.add_output('weight', 1.0, units='g')\n",
    "\n",
    "    def compute(self, inputs, outputs, discrete_inputs, discrete_output):\n",
    "        radius = inputs['radius']\n",
    "        thickness = inputs['thickness']\n",
    "        material = discrete_inputs['material']\n",
    "\n",
    "        outputs['weight'] =  math.pi * radius**2 * thickness * density[material]\n",
    "\n",
    "p4 = om.Problem()\n",
    "model = p4.model\n",
    "\n",
    "model.add_subsystem('square', SquarePlate(), promotes_inputs=['material'])\n",
    "model.add_subsystem('circle', CirclePlate(), promotes_inputs=['material'])\n",
    "\n",
    "model.set_input_defaults('material', 'steel')\n",
    "\n",
    "p4.setup()\n",
    "\n",
    "p4.run_model()\n",
    "\n",
    "model.list_vars(units=True);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output",
     "allow-assert"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(p4.get_val(\"square.weight\"), 7.85)\n",
    "assert_near_equal(p4.get_val(\"circle.weight\"), 24.66150233, tolerance=1e-8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that setting `units` or `src_shape` is not valid for a discrete variable and will result in an error if either of those arguments are supplied:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "p5 = om.Problem()\n",
    "model = p5.model\n",
    "\n",
    "model.add_subsystem('square', SquarePlate(), promotes_inputs=['material'])\n",
    "model.add_subsystem('circle', CirclePlate(), promotes_inputs=['material'])\n",
    "\n",
    "model.set_input_defaults('material', 'steel', units='g', src_shape=(1,))\n",
    "\n",
    "try:\n",
    "    p5.setup()\n",
    "except Exception as err:\n",
    "    print(str(err))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## How the `set_input_defaults` function differs from the `set_val` function\n",
    "\n",
    "While both `set_input_defaults` and `set_val` deal with variable management in OpenMDAO, they have distinct purposes and are used in different contexts.\n",
    "\n",
    "- `set_input_defaults` is used at the group level to define default metadata (units and initial value) for promoted inputs, specifically to resolve ambiguity when multiple inputs are promoted to the same name. This is crucial for inputs connected to the automatically generated `_auto_ivc` component.\n",
    "\n",
    "  - Used to resolve inconsistencies between Auto-IVC values.\n",
    "\n",
    "  - Specifically used at the group level to specify metadata to be assumed when multiple inputs are promoted to the same name. This is required when the promoted inputs have differing units or values.\n",
    "\n",
    "\n",
    "- `set_val` is used at the problem level to set the actual value of a variable, including inputs, outputs, and implicit state variables. It can handle unit conversions and set values for specific indices in array variables.\n",
    "\n",
    "  - Used at the run script level to set the value of an input variable.\n",
    "\n",
    "  - Can be used to set the value of a variable in a different unit than its declared unit, and OpenMDAO will perform the conversion.\n",
    "\n",
    "  - Can be used to set specific indices or index ranges of array variables.\n",
    "\n",
    "In essence, `set_input_defaults` helps OpenMDAO correctly determine the units and initial values of connected inputs during the setup phase, while `set_val` is used to directly manipulate variable values before or during a run.\n",
    "\n",
    "*Key Differences*\n",
    "\n",
    "-  *Scope*: \n",
    "   `set_input_defaults` is used at the group level to define default metadata for promoted inputs, while `set_val` is used at the problem level to set specific values for variables.\n",
    "\n",
    "- *Purpose*: \n",
    "  `set_input_defaults` resolves ambiguities when multiple inputs are promoted to the same name, while `set_val` is used to assign values to variables.\n",
    "\n",
    "- *Timing*: \n",
    "  `set_input_defaults` is typically called during the model setup phase, while `set_val` can be called before or during a run of the model."
   ]
  }
 ],
 "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.12.6"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
