{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "remove-input",
     "remove-output",
     "active-ipynb"
    ]
   },
   "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": [
    "# Adding Design Variables\n",
    "\n",
    "To add a design variable to an optimization, use the `add_design_var` method on System."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.core.system.System.add_design_var\n",
    "        :noindex:\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Specifying units\n",
    "\n",
    "You can specify units when adding a design variable. When this is done, the quanitity is converted from the target output’s units to the desired unit before giving it to the optimizer. When the optimizer commands a new design variable, it is assumed to be in the given units, and converted to the units of the input before setting the value in your model. If you also specify scaling, that scaling is applied after the unit conversion when being passed from the model to the optimizer, and before the unit conversion when being passed back into the model. Moreover, the upper and lower bound should be specified using these units."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    },
    "tags": [
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "model.add_subsystem('comp1', om.ExecComp('y1 = 2.0*x',\n",
    "                                         x={'val': 2.0, 'units': 'degF'},\n",
    "                                         y1={'val': 2.0, 'units': 'degF'}),\n",
    "                    promotes=['x', 'y1'])\n",
    "\n",
    "model.add_subsystem('comp2', om.ExecComp('y2 = 3.0*x',\n",
    "                                         x={'val': 2.0, 'units': 'degF'},\n",
    "                                         y2={'val': 2.0, 'units': 'degF'}),\n",
    "                    promotes=['x', 'y2'])\n",
    "\n",
    "model.set_input_defaults('x', 35.0, units='degF')\n",
    "\n",
    "model.add_design_var('x', units='degC', lower=0.0, upper=100.0)\n",
    "model.add_constraint('y1', units='degC', lower=0.0, upper=100.0)\n",
    "model.add_objective('y2', units='degC')\n",
    "\n",
    "prob.setup()\n",
    "prob.run_driver()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "print('Model variables')\n",
    "print(prob.get_val('x', indices=[0]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "print(prob.get_val('comp2.y2', indices=[0]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "print(prob.get_val('comp1.y1', indices=[0]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "print('Driver variables')\n",
    "dv = prob.driver.get_design_var_values()\n",
    "print(dv['x'][0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "jupyter": {
     "outputs_hidden": false
    },
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "obj = prob.driver.get_objective_values(driver_scaling=True)\n",
    "print(obj['y2'][0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "con = prob.driver.get_constraint_values(driver_scaling=True)\n",
    "print(con['y1'][0])"
   ]
  },
  {
   "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('x', indices=[0]), 35.)\n",
    "assert_near_equal(prob.get_val('comp2.y2', indices=[0]), 105.)\n",
    "assert_near_equal(prob.get_val('comp1.y1', indices=[0]), 70.)\n",
    "assert_near_equal(dv['x'][0], 1.6666666666666983)\n",
    "assert_near_equal(obj['y2'][0], 40.555555555555586)\n",
    "assert_near_equal(con['y1'][0], 21.111111111111143)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# How the optimizer sees scaled variables\n",
    "\n",
    "As stated above, when dealing with both a unit conversion and user-defined scaling for design variables (or constraints or objectives), the quantity is first converted to the specified units and then scaled according to the users `ref|ref0|scaler|adder` specification. \n",
    "\n",
    "This means that any scaling specification should be done in the specified units.\n",
    "\n",
    "The following derivation of the \"total_scaler\" and the \"total_adder\" applies to both driver design variables and responses.\n",
    "\n",
    "Given some design variable quantity `y`, we first convert it to the driver's units. OpenMDAO's unit system determines the scale factor (`unit_scaler`) and offset (`unit_adder`) that convert `y` from it's units within the model to its units from the driver's perspective.  These are the units specified in the call to `add_design_var` (or `add_constraint` or `add_objective`).\n",
    "\n",
    "```\n",
    "y_in_desired_units = unit_scaler * (y + unit_adder)\n",
    "```\n",
    "\n",
    "Then we apply the user-declared scaling\n",
    "\n",
    "```\n",
    "y_opt = declared_scaler * (y_in_desired_units + declared_adder)\n",
    "```\n",
    "\n",
    "Thus the optimizer sees the quantity as:\n",
    "\n",
    "```\n",
    "y_opt = declared_scaler * (unit_scaler * (y + unit_adder) + declared_adder)\n",
    "```\n",
    "\n",
    "If we gather the scaler and adder terms we have\n",
    "\n",
    "```\n",
    "y_opt = [declared_scaler * unit_scaler] * (y + unit_adder + declared_adder / unit_scaler)\n",
    "```\n",
    "\n",
    "And therefore the \"total_scaler\" and \"total_adder\" are:\n",
    "\n",
    "```\n",
    "total_scaler = declared_scaler * unit_scaler\n",
    "\n",
    "total_adder = unit_adder + declared_adder / unit_scaler\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "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.11.4"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
