{
 "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": [
    "# Component Options (Arguments to Components)\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 component. Options associated with a particular component must be declared in the *initialize* method of the component 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 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.\")"
   ]
  }
 ],
 "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.1"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
