{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b182f63f",
   "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",
   "id": "20a12d7f",
   "metadata": {},
   "source": [
    "# Declaring Partial Derivatives\n",
    "If you know additional information about the structure of partial derivatives in your component (for example, if an output does not depend on a particular input), you can use the `declare_partials`() method to inform the framework. This will allow the framework to be more efficient in terms of memory and computation (especially if using a sparse `AssembledJacobian`). This information should be declared in the setup_partials method of your component."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61a2acac",
   "metadata": {},
   "source": [
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.core.component.Component.declare_partials\n",
    "        :noindex:\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df8f395e",
   "metadata": {},
   "source": [
    "## Usage\n",
    "Specifying that a variable does not depend on another. Note that this is not typically required, because by default OpenMDAO assumes that all variables are independent. However, in some cases it might be needed if a previous glob pattern matched a large set of variables and some sub-set of that needs to be marked as independent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed8fee44",
   "metadata": {},
   "outputs": [],
   "source": [
    "def setup(self):\n",
    "    self.add_input('x', shape=1)\n",
    "    self.add_input('y1', shape=2)\n",
    "    self.add_input('y2', shape=2)\n",
    "    self.add_input('y3', shape=2)\n",
    "    self.add_input('z', shape=(2, 2))\n",
    "\n",
    "    self.add_output('f', shape=1)\n",
    "    self.add_output('g', shape=(2, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a626eee2",
   "metadata": {},
   "source": [
    "Declaring multiple derivatives using glob patterns (see https://docs.python.org/3.6/library/fnmatch.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97526cd6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def setup(self):\n",
    "    self.add_input('x', shape=1)\n",
    "    self.add_input('y1', shape=2)\n",
    "    self.add_input('y2', shape=2)\n",
    "    self.add_input('y3', shape=2)\n",
    "    self.add_input('z', shape=(2, 2))\n",
    "\n",
    "    self.add_output('f', shape=1)\n",
    "    self.add_output('g', shape=(2, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a35ce0d",
   "metadata": {},
   "source": [
    "Using the val argument to set a constant partial derivative. Note that this is intended for cases when the derivative value is constant, and hence the derivatives do not ever need to be recomputed in `compute_partials`. Here are several examples of how you can specify derivative values for differently-shaped partial derivative sub-Jacobians.\n",
    "\n",
    "* Scalar (see $\\frac{\\partial f}{\\partial x}$ )\n",
    "\n",
    "* Dense Array (see $\\frac{\\partial g}{\\partial y_1}$ )\n",
    "\n",
    "* Nested List (see $\\frac{\\partial g}{\\partial y_1}$ and $\\frac{\\partial g}{\\partial y_3}$ )\n",
    "\n",
    "* Sparse Matrix (see [Sparse Partial Derivatives doc](sparse_partials) for more details) (see $\\frac{\\partial g}{\\partial y_2}$ and $\\frac{\\partial g}{\\partial x}$)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be2d02df",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import scipy as sp\n",
    "\n",
    "import openmdao.api as om\n",
    "\n",
    "class SimpleCompConst(om.ExplicitComponent):\n",
    "    def setup(self):\n",
    "        self.add_input('x', shape=1)\n",
    "        self.add_input('y1', shape=2)\n",
    "        self.add_input('y2', shape=2)\n",
    "        self.add_input('y3', shape=2)\n",
    "        self.add_input('z', shape=(2, 2))\n",
    "\n",
    "        self.add_output('f', shape=1)\n",
    "        self.add_output('g', shape=(2, 2))\n",
    "\n",
    "    def setup_partials(self):\n",
    "        # Declare derivatives\n",
    "\n",
    "        self.declare_partials('f', ['y1', 'y2', 'y3'], dependent=False)\n",
    "        self.declare_partials('g', 'z', dependent=False)\n",
    "\n",
    "        self.declare_partials('f', 'x', val=1.)\n",
    "        self.declare_partials('f', 'z', val=np.ones((1, 4)))\n",
    "        # y[13] is a glob pattern for ['y1', 'y3']\n",
    "        self.declare_partials('g', 'y[13]', val=[[1, 0], [1, 0], [0, 1], [0, 1]])\n",
    "        self.declare_partials('g', 'y2', val=[1., 1., 1., 1.], cols=[0, 0, 1, 1], rows=[0, 2, 1, 3])\n",
    "        self.declare_partials('g', 'x', val=sp.sparse.coo_matrix(((1., 1.), ((0, 3), (0, 0)))))\n",
    "\n",
    "    def compute(self, inputs, outputs):\n",
    "        outputs['f'] = np.sum(inputs['z']) + inputs['x']\n",
    "        outputs['g'] = np.outer(inputs['y1'] + inputs['y3'], inputs['y2']) + inputs['x'] * np.eye(2)\n",
    "\n",
    "    def compute_partials(self, inputs, partials):\n",
    "        # note: all the partial derivatives are constant, so no calculations happen here.\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "296b0b56",
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "model = om.Group(assembled_jac_type='dense')\n",
    "problem = om.Problem(model=model)\n",
    "problem.set_solver_print(0)\n",
    "\n",
    "model.linear_solver = om.DirectSolver(assemble_jac=True)\n",
    "model.add_subsystem('simple', SimpleCompConst(),\n",
    "                    promotes=['x', 'y1', 'y2', 'y3', 'z', 'f', 'g'])\n",
    "problem.setup()\n",
    "problem.run_model()\n",
    "totals = problem.compute_totals(['f', 'g'],\n",
    "                                      ['x', 'y1', 'y2', 'y3', 'z'])\n",
    "\n",
    "assert_near_equal(totals['f', 'x'], [[1.]])\n",
    "assert_near_equal(totals['f', 'z'], np.ones((1, 4)))\n",
    "assert_near_equal(totals['f', 'y1'], np.zeros((1, 2)))\n",
    "assert_near_equal(totals['f', 'y2'], np.zeros((1, 2)))\n",
    "assert_near_equal(totals['f', 'y3'], np.zeros((1, 2)))\n",
    "assert_near_equal(totals['g', 'z'], np.zeros((4, 4)))\n",
    "assert_near_equal(totals['g', 'y1'], [[1, 0], [1, 0], [0, 1], [0, 1]])\n",
    "assert_near_equal(totals['g', 'y2'], [[1, 0], [0, 1], [1, 0], [0, 1]])\n",
    "assert_near_equal(totals['g', 'y3'], [[1, 0], [1, 0], [0, 1], [0, 1]])\n",
    "assert_near_equal(totals['g', 'x'], [[1], [0], [0], [1]])"
   ]
  }
 ],
 "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.5"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
