{
 "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": [
    "# System Options (Arguments to Components and Groups)\n",
    "\n",
    "The primary jobs of a component, whether explicit or implicit, are to define inputs and outputs and to do the mapping that computes the outputs given the inputs. Often, however, there are incidental parameters that affect the behavior of the component, but which are not considered input variables in the sense of being computed as an output of another component.\n",
    "\n",
    "OpenMDAO provides a way of declaring these parameters, which are contained in an *OptionsDictionary* named `options` that is available in every system. Options associated with a particular component or group must be declared in the *initialize* method of the system definition. A default value can be provided as well as various checks for validity, such as a list of acceptable values or types.\n",
    "\n",
    "The attributes that can be specified when declaring an option are enumerated and described below:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.utils.options_dictionary.OptionsDictionary.declare\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "When using the `check_valid` argument, the expected function signature is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.utils.options_dictionary.check_valid\n",
    "        :noindex:\n",
    "```\n",
    "Option values are typically passed at component instantiation time as keyword arguments, which are automatically assigned into the option dictionary. The options are then available for use in the component’s other methods, such as *setup* and *compute*.\n",
    "\n",
    "Alternatively, values can be set at a later time, in another method of the component (except for *initialize*) or outside of the component definition after the component is instantiated.\n",
    "\n",
    "## A Simple Example\n",
    "\n",
    "Options are commonly used to specify the shape or size of the component’s input and output variables, such as in this simple example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "A component that multiplies a vector by 2, where the\n",
    "size of the vector is given as an option of type 'int'.\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "\n",
    "class VectorDoublingComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('size', types=int)\n",
    "\n",
    "    def setup(self):\n",
    "        size = self.options['size']\n",
    "\n",
    "        self.add_input('x', shape=size)\n",
    "        self.add_output('y', shape=size)\n",
    "\n",
    "    def setup_partials(self):\n",
    "        size = self.options['size']\n",
    "        self.declare_partials('y', 'x', val=2.,\n",
    "                              rows=np.arange(size),\n",
    "                              cols=np.arange(size))\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = 2 * inputs['x']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('double', VectorDoublingComp(size=3))  # 'size' is an option\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('double.x', [1., 2., 3.])\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('double.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",
    "assert_near_equal(prob.get_val('double.y'), [2., 4., 6.])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Not setting a default value when declaring an option implies that the value must be set by the user.\n",
    "\n",
    "In this example, ‘size’ is required; We would have gotten an error if we:\n",
    "\n",
    "1. Did not pass in ‘size’ when instantiating *VectorDoublingComp* and\n",
    "2. Did not set its value in the code for *VectorDoublingComp*.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "from openmdao.test_suite.components.options_feature_vector import VectorDoublingComp\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('double', VectorDoublingComp())  # 'size' not specified\n",
    "\n",
    "try:\n",
    "    prob.setup()\n",
    "except RuntimeError as err:\n",
    "    print(str(err))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Option Names\n",
    "\n",
    "An option name can be any valid string. It is recommended that you use clear concise names. It is also recommended that you restrict the characters to those permitted in valid python names so that they can be passed into the system's initialization arguments, but it is not required.\n",
    "\n",
    "## Option Types\n",
    "\n",
    "Options are not limited to simple types like `int`. In the following example, the component takes a *Numpy* array as an option:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "A component that multiplies an array by an input value, where\n",
    "the array is given as an option of type 'numpy.ndarray'.\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "\n",
    "class ArrayMultiplyComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('array', types=np.ndarray)\n",
    "\n",
    "    def setup(self):\n",
    "        array = self.options['array']\n",
    "\n",
    "        self.add_input('x', 1.)\n",
    "        self.add_output('y', shape=array.shape)\n",
    "\n",
    "    def setup_partials(self):\n",
    "        self.declare_partials(of='*', wrt='*')\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = self.options['array'] * inputs['x']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('a_comp', ArrayMultiplyComp(array=np.array([1, 2, 3])))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('a_comp.x', 5.)\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('a_comp.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('a_comp.y'), [5., 10., 15.])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It is even possible to provide a function as an option:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "A component that computes y = func(x), where func\n",
    "is a function given as an option.\n",
    "\"\"\"\n",
    "\n",
    "from types import FunctionType\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "\n",
    "class UnitaryFunctionComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('func', types=FunctionType, recordable=False)\n",
    "\n",
    "    def setup(self):\n",
    "        self.add_input('x')\n",
    "        self.add_output('y')\n",
    "\n",
    "    def setup_partials(self):\n",
    "        self.declare_partials('y', 'x', method='fd')\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        func = self.options['func']\n",
    "        outputs['y'] = func(inputs['x'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "def my_func(x):\n",
    "    return x*2\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('f_comp', UnitaryFunctionComp(func=my_func))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('f_comp.x', 5.)\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('f_comp.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('f_comp.y'), 10.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Providing Default Values\n",
    "\n",
    "One reason why using options is convenient is that a default value can be specified, making it optional to pass the value in during component instantiation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "A component that computes y = a*x + b, where a and b\n",
    "are given as an option of type 'numpy.ScalarType'.\n",
    "\"\"\"\n",
    "import numpy as np\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "\n",
    "class LinearCombinationComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('a', default=1., types=np.ScalarType)\n",
    "        self.options.declare('b', default=1., types=np.ScalarType)\n",
    "\n",
    "    def setup(self):\n",
    "        self.add_input('x')\n",
    "        self.add_output('y')\n",
    "\n",
    "    def setup_partials(self):\n",
    "        self.declare_partials('y', 'x', val=self.options['a'])\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = self.options['a'] * inputs['x'] + self.options['b']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('linear', LinearCombinationComp(a=2.))  # 'b' not specified\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('linear.x', 3)\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('linear.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(prob.get_val('linear.y') == 7.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, both ‘a’ and ‘b’ are optional, so it is valid to pass in ‘a’, but not ‘b’.\n",
    "\n",
    "## Specifying Values or Types\n",
    "\n",
    "The parameters available when declaring an option allow a great deal of flexibility in specifying exactly what types and values are acceptable.\n",
    "\n",
    "As seen above, the allowed types can be specified using the `types` parameter. If an option is more limited, then the set of allowed values can be given with `values`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import openmdao.api as om\n",
    "\n",
    "class VectorDoublingComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('size', values=[2, 4, 6, 8])\n",
    "\n",
    "    def setup(self):\n",
    "        size = self.options['size']\n",
    "\n",
    "        self.add_input('x', shape=size)\n",
    "        self.add_output('y', shape=size)\n",
    "        self.declare_partials('y', 'x', val=2.,\n",
    "                              rows=np.arange(size),\n",
    "                              cols=np.arange(size))\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = 2 * inputs['x']\n",
    "\n",
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('double', VectorDoublingComp(size=4))\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('double.x', [1., 2., 3., 4.])\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('double.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('double.y'), [2., 4., 6., 8.])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{warning}\n",
    "It is an error to attempt to specify both a list of acceptable values and a list of acceptable types.\n",
    "```\n",
    "\n",
    "\n",
    "Alternatively, the allowable values can be set using bounds and/or a validation function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import openmdao.api as om\n",
    "\n",
    "def check_even(name, value):\n",
    "    if value % 2 != 0:\n",
    "        raise ValueError(f\"Option '{name}' with value {value} must be an even number.\")\n",
    "\n",
    "class VectorDoublingComp(om.ExplicitComponent):\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('size', types=int, lower=2, upper=8, check_valid=check_even)\n",
    "\n",
    "    def setup(self):\n",
    "        size = self.options['size']\n",
    "\n",
    "        self.add_input('x', shape=size)\n",
    "        self.add_output('y', shape=size)\n",
    "        self.declare_partials('y', 'x', val=2.,\n",
    "                              rows=np.arange(size),\n",
    "                              cols=np.arange(size))\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['y'] = 2 * inputs['x']\n",
    "\n",
    "try:\n",
    "    comp = VectorDoublingComp(size=5)\n",
    "except Exception as err:\n",
    "    msg = str(err)\n",
    "    print(msg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(msg == \"Option 'size' with value 5 must be an even number.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setting Multiple Option Values Simultaneously\n",
    "\n",
    "The *OptionsDictionary* class is versatile and can be used in a variety of situations.\n",
    "While all systems contain an `options` attribute, users may wish to introduce other options for their systems or other classes using OpenMDAO's *OptionsDictionary*.\n",
    "\n",
    "After initialization, users might want the ability to modify more than one option at a time.\n",
    "For this purpose, the `set` method on *OptionsDictionary* exists."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "linear_comp = prob.model.add_subsystem('linear', LinearCombinationComp())\n",
    "\n",
    "# Set options a and b at the same time.\n",
    "linear_comp.options.set(a=3., b=5.)\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('linear.x', 3)\n",
    "\n",
    "prob.run_model()\n",
    "print(prob.get_val('linear.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(prob.get_val('linear.y') == 14.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that the option name must be a valid python name in order to use this `set` function. If your option has a name that is not valid in python (e.g., \"aircraft:propulsion:NUM_ENGINES\"), then you will have to set them individually with the dictionary interface.\n",
    "\n",
    "## Setting options throughout a problem model (`Problem.model_options`)\n",
    "\n",
    "It is common for options to be passed down through a model tree from parent Group to child Group or Component.\n",
    "This can make the addition of a new option deep in the model a tedious procedure, since each parent system may need to have an option added to itself in order to know how to instantiate its children, and then actually take the given input option and pass it to the child.\n",
    "\n",
    "There are a few ways to address this. For example, you might implement a function to declare the common options and call it in the relevant subsystems' `initialize` method.\n",
    "Alternatively, those classes can subclass from a class which automatically adds the appropriate options.\n",
    "\n",
    "For passing options to components nested deeply within the model, the OpenMDAO `Problem` object provides an attribute named `model_options`. `Problem.model_options` is a standard dictionary, keyed by a string that serves as a [glob](https://en.wikipedia.org/wiki/Glob_(programming)) filter for system pathnames.\n",
    "\n",
    "For each corresponding value, a sub dictionary provides string keys of option names, with corresponding option values as the associated value.\n",
    "\n",
    "When OpenMDAO begins `setup`, these options are passed down to each system in the model\n",
    "If a system's pathname matches the key in model options, it will accept the value for each option which it possesses.\n",
    "If it does not possess a particular option, that option value is ignored by that particular system.\n",
    "\n",
    "Dictionaries in Python are ordered, so if a particular component matches multiple glob patterns specifying the same options, the last match will take precedence.\n",
    "\n",
    "The following code demonstrates a model with several levels of depth where the component leaves accept options `a` and `b`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "c1 = prob.model.add_subsystem('c1', LinearCombinationComp())\n",
    "g1 = prob.model.add_subsystem('g1', om.Group())\n",
    "\n",
    "c2 = g1.add_subsystem('c2', LinearCombinationComp())\n",
    "g2 = g1.add_subsystem('g2', om.Group())\n",
    "\n",
    "c3 = g2.add_subsystem('c3', LinearCombinationComp())\n",
    "g3 = g2.add_subsystem('g3', om.Group())\n",
    "\n",
    "prob.model.connect('c1.y', 'g1.c2.x')\n",
    "prob.model.connect('g1.c2.y', 'g1.g2.c3.x')\n",
    "\n",
    "# Any component with options 'a' or 'b' accepts these values.\n",
    "prob.model_options['*'] = {'a': 3., 'b': 5.}\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('c1.x', 3)\n",
    "\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(c1.options['a'] == c2.options['a'] == c3.options['a'])\n",
    "assert(c1.options['b'] == c2.options['b'] == c3.options['b'])\n",
    "assert(prob.get_val('g1.g2.c3.y') == ((3 * 3 + 5) * 3 + 5) * 3 + 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Showing detailed node information for the components below will indicate that options `a` and `b` have the correct values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "om.n2(prob)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Using glob patterns to set different option values in different systems."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "c1 = prob.model.add_subsystem('c1', LinearCombinationComp())\n",
    "g1 = prob.model.add_subsystem('g1', om.Group())\n",
    "\n",
    "c2 = g1.add_subsystem('c2', LinearCombinationComp())\n",
    "g2 = g1.add_subsystem('g2', om.Group())\n",
    "\n",
    "c3 = g2.add_subsystem('c3', LinearCombinationComp())\n",
    "g3 = g2.add_subsystem('g3', om.Group())\n",
    "\n",
    "prob.model.connect('c1.y', 'g1.c2.x')\n",
    "prob.model.connect('g1.c2.y', 'g1.g2.c3.x')\n",
    "\n",
    "# Only component 'c1' will accept these values.\n",
    "prob.model_options['c1'] = {'a': 3., 'b': 5.}\n",
    "\n",
    "# Any component that is not at the top of the model and whose name matches\n",
    "# pattern `c?` will accept these values.\n",
    "prob.model_options['*.c?'] = {'a': 4., 'b': 6.}\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('c1.x', 3)\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('g1.g2.c3.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert(c1.options['a'] == 3)\n",
    "assert(c2.options['a'] == c3.options['a'] == 4.)\n",
    "assert(c1.options['b'] == 5.)\n",
    "assert(c2.options['b'] == c3.options['b'] == 6.)\n",
    "assert(prob.get_val('g1.g2.c3.y') == ((3 * 3 + 5) * 4 + 6) * 4 + 6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Modifying `model_options` at the Group level\n",
    "\n",
    "Sometimes it may be desirable to have a Group modify the contents of `model_options` during setup so that it can specify options for its children.  For this purpose, group has a view of `model_options` that is accessible via the `Group.model_options` attribute.\n",
    "\n",
    "During setup, a Group can set options for all of its children by appending something to `model_options` with the appropriate path.\n",
    "\n",
    "Remember that `setup` is a top-down process, so `model_options` must be modified no later than `setup` in order for the appropriate information to be sent to the Group's descendents.\n",
    "The user should be cautious when using this functionality.\n",
    "They have the ability to modify model options for other systems in the model tree (such as \"sibling\" systems and their descendents), but doing so may result in undefined behavior that will work sometimes and othertimes not depending on the specific ordering of the systems in the model tree.\n",
    "In short, it's good practice to prefix the model options with `'self.pathname'` when using this feature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyGroup(om.Group):\n",
    "\n",
    "    def setup(self):\n",
    "        g1 = self.add_subsystem('g1', om.Group())\n",
    "        g1.add_subsystem('c1', LinearCombinationComp())\n",
    "        g1.add_subsystem('c2', LinearCombinationComp())\n",
    "        g1.add_subsystem('c3', LinearCombinationComp())\n",
    "\n",
    "        # Send options a and b to all descendents of this model.\n",
    "        self.model_options[f'{self.pathname}.*'] = {'a': 3., 'b': 5.}\n",
    "\n",
    "        g1.connect('c1.y', 'c2.x')\n",
    "        g1.connect('c2.y', 'c3.x')\n",
    "\n",
    "p = om.Problem()\n",
    "p.model.add_subsystem('my_group', MyGroup())\n",
    "p.setup()\n",
    "\n",
    "p.set_val('my_group.g1.c1.x', 4)\n",
    "\n",
    "p.run_model()\n",
    "\n",
    "print(p.get_val('my_group.g1.c3.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "c3y = p.get_val('my_group.g1.c3.y')\n",
    "expected = ((4 * 3 + 5) * 3 + 5) * 3 + 5.\n",
    "assert_near_equal(expected, c3y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Listing Options\n",
    "\n",
    "You can list all options in a `Group` or `Component` and their associated linear and nonlinear solvers by using the `list_options` method. This will return the options in a list of tuples of (system pathname, system dictionary of options, nonlinear_solver dictionary of options, linear_solver dictionary of options), and will also print them to stdout or any output stream that you request."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "myoptions = p.model.list_options(include_solvers=True, include_default=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Adding a function to pre-process setting an option\n",
    "\n",
    "The `OptionsDictionary` has support for custom pre-processing the value before it is set. One potential use for this is to provide a way to convert units while setting an option. The following example shows how to do this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "from openmdao.utils.units import convert_units\n",
    "\n",
    "\n",
    "# TODO: Turn this into a test.\n",
    "\n",
    "def units_setter(opt_meta, value):\n",
    "    \"\"\"\n",
    "    Check and convert new units tuple into\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    opt_meta : dict\n",
    "        Dictionary of entries for the option.\n",
    "    value : any\n",
    "        New value for the option.\n",
    "\n",
    "    Returns\n",
    "    -------\n",
    "    any\n",
    "        Post processed value to set into the option.\n",
    "    \"\"\"\n",
    "    new_val, new_units = value\n",
    "    old_val, units = opt_meta['val']\n",
    "\n",
    "    converted_val = convert_units(new_val, new_units, units)\n",
    "    return (converted_val, units)\n",
    "\n",
    "\n",
    "class MyComp(om.ExplicitComponent):\n",
    "\n",
    "    def setup(self):\n",
    "\n",
    "        self.add_input('x', 3.0)\n",
    "        self.add_output('y', 3.0)\n",
    "\n",
    "    def initialize(self):\n",
    "        self.options.declare('length', default=(12.0, 'inch'),\n",
    "                             set_function=units_setter)\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        length = self.options['length'][0]\n",
    "\n",
    "        x = inputs['x']\n",
    "        outputs['y'] = length * x\n",
    "\n",
    "\n",
    "class MySubgroup(om.Group):\n",
    "\n",
    "    def setup(self):\n",
    "        self.add_subsystem('mass', MyComp())\n",
    "\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('statics', MySubgroup())\n",
    "\n",
    "prob.model_options['*'] = {'length': (2.0, 'ft')}\n",
    "prob.setup()\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print('The following should be 72 if the units convert correctly.')\n",
    "print(prob.get_val('statics.mass.y'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output",
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "assert_near_equal(prob.get_val('statics.mass.y'), 72, 1e-6)\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.12.3"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
