{
"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": [
"OpenMDAO considers component derivatives to be **partial derivatives**. The framework uses these partial derivatives in order to compute the **total derivatives** across your whole model. This tutorial is focused on how to define the partial derivatives for components that inherit from [ExplicitComponent](../../features/core_features/working_with_components/explicit_component.ipynb).\n",
"\n",
"# Defining Partial Derivatives on Explicit Components\n",
"\n",
"For any [ExplicitComponent](../../features/core_features/working_with_components/explicit_component.ipynb) you are going to provide derivatives of the **outputs with respect to the inputs**. Whenever you are going to define derivatives, there are two things you're required to do:\n",
"\n",
"1. Declare the partial derivatives via `declare_partials`.\n",
"2. Specify their values via `compute_partials`.\n",
"\n",
"Here is an example, based on the [Betz Limit Example](../../examples/betz_limit.ipynb):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import openmdao.api as om\n",
"\n",
"\n",
"class ActuatorDisc(om.ExplicitComponent):\n",
" \"\"\"Simple wind turbine model based on actuator disc theory\"\"\"\n",
"\n",
" def setup(self):\n",
" # Inputs\n",
" self.add_input('a', 0.5, desc=\"Induced Velocity Factor\")\n",
" self.add_input('Area', 10.0, units=\"m**2\", desc=\"Rotor disc area\")\n",
" self.add_input('rho', 1.225, units=\"kg/m**3\", desc=\"Air density\")\n",
" self.add_input('Vu', 10.0, units=\"m/s\", desc=\"Freestream air velocity, upstream of rotor\")\n",
"\n",
" # Outputs\n",
" self.add_output('Vr', 0.0, units=\"m/s\",\n",
" desc=\"Air velocity at rotor exit plane\")\n",
" self.add_output('Vd', 0.0, units=\"m/s\",\n",
" desc=\"Slipstream air velocity, downstream of rotor\")\n",
" self.add_output('Ct', 0.0, desc=\"Thrust Coefficient\")\n",
" self.add_output('thrust', 0.0, units=\"N\",\n",
" desc=\"Thrust produced by the rotor\")\n",
" self.add_output('Cp', 0.0, desc=\"Power Coefficient\")\n",
" self.add_output('power', 0.0, units=\"W\", desc=\"Power produced by the rotor\")\n",
"\n",
" def setup_partials(self):\n",
" self.declare_partials('Vr', ['a', 'Vu'])\n",
" self.declare_partials('Vd', 'a')\n",
" self.declare_partials('Ct', 'a')\n",
" self.declare_partials('thrust', ['a', 'Area', 'rho', 'Vu'])\n",
" self.declare_partials('Cp', 'a')\n",
" self.declare_partials('power', ['a', 'Area', 'rho', 'Vu'])\n",
"\n",
" def compute(self, inputs, outputs):\n",
" \"\"\" Considering the entire rotor as a single disc that extracts\n",
" velocity uniformly from the incoming flow and converts it to\n",
" power.\"\"\"\n",
"\n",
" a = inputs['a']\n",
" Vu = inputs['Vu']\n",
"\n",
" qA = .5 * inputs['rho'] * inputs['Area'] * Vu**2\n",
"\n",
" outputs['Vd'] = Vd = Vu * (1 - 2 * a)\n",
" outputs['Vr'] = .5 * (Vu + Vd)\n",
"\n",
" outputs['Ct'] = Ct = 4 * a * (1 - a)\n",
" outputs['thrust'] = Ct * qA\n",
"\n",
" outputs['Cp'] = Cp = Ct * (1 - a)\n",
" outputs['power'] = Cp * qA * Vu\n",
"\n",
" def compute_partials(self, inputs, J):\n",
" \"\"\" Jacobian of partial derivatives.\"\"\"\n",
"\n",
" a = inputs['a']\n",
" Vu = inputs['Vu']\n",
" Area = inputs['Area']\n",
" rho = inputs['rho']\n",
"\n",
" # pre-compute commonly needed quantities\n",
" a_times_area = a * Area\n",
" one_minus_a = 1.0 - a\n",
" a_area_rho_vu = a_times_area * rho * Vu\n",
"\n",
" J['Vr', 'a'] = -Vu\n",
" J['Vr', 'Vu'] = one_minus_a\n",
"\n",
" J['Vd', 'a'] = -2.0 * Vu\n",
"\n",
" J['Ct', 'a'] = 4.0 - 8.0 * a\n",
"\n",
" J['thrust', 'a'] = .5 * rho * Vu**2 * Area * J['Ct', 'a']\n",
" J['thrust', 'Area'] = 2.0 * Vu**2 * a * rho * one_minus_a\n",
" J['thrust', 'rho'] = 2.0 * a_times_area * Vu ** 2 * (one_minus_a)\n",
" J['thrust', 'Vu'] = 4.0 * a_area_rho_vu * (one_minus_a)\n",
"\n",
" J['Cp', 'a'] = 4.0 * a * (2.0 * a - 2.0) + 4.0 * (one_minus_a)**2\n",
"\n",
" J['power', 'a'] = 2.0 * Area * Vu**3 * a * rho * (\n",
" 2.0 * a - 2.0) + 2.0 * Area * Vu**3 * rho * one_minus_a**2\n",
" J['power', 'Area'] = 2.0 * Vu**3 * a * rho * one_minus_a**2\n",
" J['power', 'rho'] = 2.0 * a_times_area * Vu ** 3 * (one_minus_a)**2\n",
" J['power', 'Vu'] = 6.0 * Area * Vu**2 * a * rho * one_minus_a**2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The calls to `declare_partials` tell OpenMDAO which partial derivatives to expect. This should be done inside the `setup_partials` method. It's not illegal to do it in `setup`, but there are some cases where it must be called in `setup_partials`, for example the case where a component has dynamically sized variables. `setup_partials` iscalled after all shapes, even dynamic ones, are known, so it works on all cases. For the sake of consistency then, it's best to always call `declare_partials` in `setup_partials`. In this example, not all the outputs depend on all the inputs, and you'll see that if you look at the derivative declarations. Any partial that is not declared is assumed to be zero. You may declare all the partials in just one line as follows (see the documentation on [specifying partials](../../features/core_features/working_with_derivatives/specifying_partials.ipynb) for more details):\n",
"\n",
"```\n",
"self.declare_partials('*', '*')\n",
"```\n",
"\n",
"Declaring the partials in this fashion, however, indicates to OpenMDAO that all the partials are nonzero.\n",
"While you may save yourself a few lines of code using this method, the line savings could come at the expense of performance. Generally, it is better to be more specific, and declare only the nonzero partials.\n",
"\n",
"```{important}\n",
"There are a few more options to `declare_partials` that are worth taking a look at. There is support for when your derivatives are constant, and there is support for specifying derivatives in a sparse AIJ format. The full details can be found in the documentation on [specifying partials](../../features/core_features/working_with_derivatives/specifying_partials.ipynb).\n",
"```\n",
"\n",
"After you declare the nonzero partial derivatives, you need to implement the `compute_partials` method to perform the actual derivative computations. OpenMDAO will call this method whenever it needs to work with the partial derivatives. The values are stored in the Jacobian object, `J`, and get used in the linear solutions that are necessary to compute model-level total derivatives. This API results in the assembly of a Jacobian matrix in memory. The `compute_partials` API is the most appropriate way to declare derivatives in the vast majority of circumstances, and you should use it unless you have a good reason not to.\n",
"\n",
"## Providing Derivatives Using the Matrix-Free API\n",
"\n",
"Sometimes you don't want to assemble the full partial-derivative Jacobian of your component in memory.\n",
"The reasons why you might not want this are beyond the scope of this tutorial. For now, let's assume that if matrix assembly won't work for your application, that you are likely already well aware of this issue. So if you can't imagine why you would want to use a matrix-free API, you may disregard the following link. If you do need to work matrix-free, there is a `compute_jacvec_product` API, examples of which can be found in the feature document for [ExplicitComponent](../../features/core_features/working_with_components/explicit_component.ipynb).\n",
"\n",
"\n",
"## How Do I Know If My Derivatives Are Correct?\n",
"\n",
"It is really important, if you are going to provide analytic derivatives, that you make sure they are correct.\n",
"It is hard to overstate the importance of accurate derivatives in the convergence of analysis and optimization problems. OpenMDAO provides a helper function to make it easier to verify your partial derivatives. Any time you implement analytic derivatives, or change the nonlinear equations of your analysis, you should check your partial derivatives this way."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from openmdao.test_suite.test_examples.test_betz_limit import ActuatorDisc\n",
"\n",
"prob = om.Problem()\n",
"\n",
"prob.model.add_subsystem('a_disk', ActuatorDisc())\n",
"\n",
"prob.setup()\n",
"prob.check_partials(compact_print=True);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```{important}\n",
"`check_partials` is **really** important when you're coding derivatives. It has some options to give you more detailed outputs for debugging and to let you limit which components get tested. You should look over the complete documentation on [check_partials](../../features/core_features/working_with_derivatives/basic_check_partials.ipynb) before you start doing heavy development with derivatives.\n",
"```\n",
"\n",
"There is a lot of information there, but for now, just take a look at the *r(fwd-chk)* column, which shows the norm of the relative difference between the analytic derivatives Jacobian and one that was approximated using finite differences. Here, all the numbers are really small, and that's what you want to see. It's rare, except for linear functions, that the finite difference and analytic derivatives will match exactly, but they should be pretty close."
]
}
],
"metadata": {
"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"
}
},
"nbformat": 4,
"nbformat_minor": 4
}