{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "hide_input": true,
    "tags": [
     "active-ipynb",
     "remove-input",
     "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": [
    "# Wind Turbine Actuator Disc\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Optimizing an Actuator Disc Model to Find Betz Limit for Wind Turbines\n",
    "\n",
    "The Betz limit is the theoretical maximum amount of kinetic energy that a wind turbine can extract from the flow.\n",
    "This limit was derived analytically by Albert Betz in 1919, but it can also be found numerically using an optimizer and a simple actuator disc model for a wind-turbine."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![The actuator disc model of a wind turbine.](images/actuator_disk.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Actuator Disc Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import scipy\n",
    "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",
    "\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",
    "        # Every output depends on `a`\n",
    "        self.declare_partials(of='*', wrt='a', method='cs')\n",
    "\n",
    "        # Other dependencies\n",
    "        self.declare_partials(of='Vr', wrt=['Vu'], method='cs')\n",
    "        self.declare_partials(of=['thrust', 'power'], wrt=['Area', 'rho', 'Vu'], method='cs')\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"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build the problem and add the actuator disc model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "prob.model.add_subsystem('a_disk', ActuatorDisc(),\n",
    "                         promotes_inputs=['a', 'Area', 'rho', 'Vu']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define the driver and optimization problem"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob.driver = om.ScipyOptimizeDriver()\n",
    "prob.driver.options['optimizer'] = 'SLSQP'\n",
    "\n",
    "prob.model.add_design_var('a', lower=0., upper=1.)\n",
    "\n",
    "# negative one so we maximize the objective\n",
    "prob.model.add_objective('a_disk.Cp', scaler=-1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Call setup and provide the initial guess"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob.setup()\n",
    "\n",
    "prob.set_val('a', .5)\n",
    "prob.set_val('Area', 10.0, units='m**2')\n",
    "prob.set_val('rho', 1.225, units='kg/m**3')\n",
    "prob.set_val('Vu', 10.0, units='m/s')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Run the optimization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fail = prob.run_driver()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## View the inputs and outputs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob.model.list_inputs(val=True, units=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "prob.model.list_outputs(val=True, units=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "hide_input": true
   },
   "outputs": [],
   "source": [
    "# Verify the correct outputs\n",
    "\n",
    "# minimum value\n",
    "print(prob.get_val('a_disk.Cp'))\n",
    "print(prob.get_val('a'))"
   ]
  },
  {
   "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['a_disk.Cp'], 16./27., 1e-4)\n",
    "assert_near_equal(prob['a'], 0.33333, 1e-4)"
   ]
  }
 ],
 "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"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
