{
 "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": [
    "# SplineComp\n",
    "\n",
    "SplineComp allows you to represent a larger dimensional variable with a smaller dimensional variable by\n",
    "using an interpolation algorithm. This is useful for reducing the size of an optimization problem by\n",
    "decreasing the number of design variables it solves. The spatial distribution of the points, in both\n",
    "the original and interpolated spaces is typically uniform but other distributions can be used.\n",
    "\n",
    "```{Note}\n",
    "OpenMDAO contains two components that perform interpolation: `SplineComp` and `MetaModelStructuredComp`.\n",
    "While they provide access to mostly the same algorithms, their usage is subtly different.\n",
    "The fundamental differences between them are as follows:\n",
    "\n",
    "[MetaModelStructuredComp](metamodelstructured_comp) is used when you have a set of known data values y on a structured grid x and\n",
    "want to interpolate a new y value at a new x location that lies inside the grid. In this case, you\n",
    "generally start with a known set of fixed \"training\" values and their locations.\n",
    "\n",
    "`SplineComp` is used when you want to create a smooth curve with a large number of points, but you\n",
    "want to control the shape of the curve with a small number of control points. The x locations of\n",
    "the interpolated points (and where applicable, the control points) are fixed and known, but the\n",
    "y values at the control points vary as the curve shape is modified by an upstream connection.\n",
    "\n",
    "MetaModelStructuredComp can be used for multi-dimensional design spaces, whereas SplineComp is\n",
    "restricted to one dimension.\n",
    "```\n",
    "\n",
    "The following methods are available by setting the 'method' option:\n",
    "\n",
    "```{eval-rst}\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| Method        | Order  | Description                                                      |\n",
    "+===============+========+==================================================================+\n",
    "| slinear       | 1      | Basic linear interpolation                                       |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| lagrange2     | 2      | Second order Lagrange polynomial                                 |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| lagrange3     | 3      | Third order Lagrange polynomial                                  |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| akima         | 3      | Interpolation using Akima splines                                |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| cubic         | 3      | Cubic spline, with continuity of derivatives between segments    |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| bsplines      | var.   | BSplines, default order is 4.                                    |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| scipy_slinear | 1      | Scipy linear interpolation. Same as slinear, though slower       |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| scipy_cubic   | 3      | Scipy cubic interpolation. More accurate than cubic, but slower  |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "| scipy_quintic | 5      | Scipy quintic interpolation. Most accurate, but slowest          |\n",
    "+---------------+--------+------------------------------------------------------------------+\n",
    "```\n",
    "\n",
    "## SplineComp Options"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "om.show_options_table(\"openmdao.components.spline_comp.SplineComp\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## SplineComp Constructor\n",
    "\n",
    "The call signature for the `SplineComp` constructor is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.components.spline_comp.SplineComp.__init__\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "## SplineComp Basic Example\n",
    "\n",
    "In our example, we have a pre-generated curve that is described by a series of values `y_cp` at a\n",
    "sequence of locations `x_cp`, and we would like to interpolate new values at multiple locations\n",
    "between these points. We call these new fixed locations at which to interpolate: `x`. When we\n",
    "instantiate a `SplineComp`, we specify these `x_cp` and `x` locations as numpy arrays and pass\n",
    "them in as constructor keyword arguments. (Figure 1). Next we'll add our `y_cp` data in by\n",
    "calling the `add_spline` method and passing the `y_cp` values in as the keyword argument `y_cp_val` (Figure 2).\n",
    "`SplineComp` computes and outputs the `y_interp` values (Figure 3).\n",
    "\n",
    "![figure_1](images/figure_1.png)\n",
    "![figure_2](images/figure_2.png)\n",
    "![figure_3](images/figure_3.png)\n",
    "![figure_4](images/figure_4.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import openmdao.api as om\n",
    "\n",
    "xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "n = 50\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "akima_option = {'delta_x': 0.1}\n",
    "comp = om.SplineComp(method='akima', x_cp_val=xcp, x_interp_val=x,\n",
    "                     interp_options=akima_option)\n",
    "\n",
    "prob.model.add_subsystem('akima1', comp)\n",
    "\n",
    "comp.add_spline(y_cp_name='ycp', y_interp_name='y_val', y_cp_val=ycp)\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('akima1.y_val'))"
   ]
  },
  {
   "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('akima1.y_val'),\n",
    "                  np.array([[ 5.       ,  7.20902005,  9.21276849, 10.81097162, 11.80335574,\n",
    "                              12.1278001 , 12.35869145, 12.58588536, 12.81022332, 13.03254681,\n",
    "                              13.25369732, 13.47451633, 13.69584534, 13.91852582, 14.14281484,\n",
    "                              14.36710105, 14.59128625, 14.81544619, 15.03965664, 15.26399335,\n",
    "                              15.48853209, 15.7133486 , 15.93851866, 16.16573502, 16.39927111,\n",
    "                              16.63928669, 16.8857123 , 17.1384785 , 17.39751585, 17.66275489,\n",
    "                              17.93412619, 18.21156029, 18.49498776, 18.78433915, 19.07954501,\n",
    "                              19.38053589, 19.68724235, 19.99959495, 20.31752423, 20.64096076,\n",
    "                              20.96983509, 21.37579297, 21.94811407, 22.66809748, 23.51629844,\n",
    "                              24.47327219, 25.51957398, 26.63575905, 27.80238264, 29.        ]]),\n",
    "                  tolerance=1e-8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## SplineComp Multiple Splines\n",
    "\n",
    "`SplineComp` supports multiple splines on a fixed `x_interp` grid. Below is an example of how a user can\n",
    "setup two splines on a fixed grid. To do this the user needs to pass in names to give to the component\n",
    "input and output. The initial values for `y_cp` can also be specified here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_cp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "y_cp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "y_cp2 = np.array([1.0, 5.0, 7.0, 8.0, 13.0, 16.0])\n",
    "n = 50\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "comp = om.SplineComp(method='akima', x_cp_val=x_cp, x_interp_val=x)\n",
    "prob.model.add_subsystem('akima1', comp)\n",
    "\n",
    "comp.add_spline(y_cp_name='ycp1', y_interp_name='y_val1', y_cp_val=y_cp)\n",
    "comp.add_spline(y_cp_name='ycp2', y_interp_name='y_val2', y_cp_val=y_cp2)\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Specifying Options for 'akima'\n",
    "\n",
    "When you are using the 'akima' method, there are two akima-specific options that can be passed in to the\n",
    "`SplineComp` constructor.  The 'delta_x' option is used to define the radius of the smoothing interval\n",
    "that is used in the absolute values functions in the akima calculation in order to make their\n",
    "derivatives continuous.  This is set to zero by default, which effectively turns off the smoothing.\n",
    "The 'eps' option is used to define the value that triggers a division-by-zero\n",
    "safeguard; its default value is 1e-30."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_cp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "y_cp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "\n",
    "n = 50\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "# Set options specific to akima\n",
    "akima_option = {'delta_x': 0.1, 'eps': 1e-30}\n",
    "\n",
    "comp = om.SplineComp(method='akima', x_cp_val=x_cp, x_interp_val=x,\n",
    "                     interp_options=akima_option)\n",
    "\n",
    "prob.model.add_subsystem('atmosphere', comp)\n",
    "\n",
    "comp.add_spline(y_cp_name='alt_cp', y_interp_name='alt', y_cp_val=y_cp, y_units='kft')\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Specifying Options for 'bsplines'\n",
    "\n",
    "When you use the 'bsplines' method, you can specify the bspline order by defining 'order' in an\n",
    "otherwise empty dictionary and passing it in as 'interp_options'.\n",
    "\n",
    "In addition, when using 'bsplines', you cannot specify the 'x_cp' locations because the bspline\n",
    "formulation differs from other polynomial interpolants. When using bsplines, you should instead\n",
    "specify the number of control points using the 'num_cp' argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "n_cp = 80\n",
    "n_point = 160\n",
    "\n",
    "t = np.linspace(0, 3.0*np.pi, n_cp)\n",
    "tt = np.linspace(0, 3.0*np.pi, n_point)\n",
    "x = np.sin(t)\n",
    "\n",
    "# Set options specific to bsplines\n",
    "bspline_options = {'order': 3}\n",
    "\n",
    "comp = om.SplineComp(method='bsplines', x_interp_val=tt, num_cp=n_cp,\n",
    "                    interp_options=bspline_options)\n",
    "\n",
    "prob.model.add_subsystem('interp', comp, promotes_inputs=[('h_cp', 'x')])\n",
    "\n",
    "comp.add_spline(y_cp_name='h_cp', y_interp_name='h', y_cp_val=x, y_units=None)\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "prob.set_val('x', x)\n",
    "prob.run_model()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## SplineComp Interpolation Distribution\n",
    "\n",
    "The `cell_centered` function takes the number of cells, and the start and end values, and returns a\n",
    "vector of points that lie at the center of those cells. The 'node_centered' function reproduces the\n",
    "functionality of numpy's linspace.  Finally, the `sine_distribution` function creates a sinusoidal\n",
    "distribution, in which points are clustered towards the ends. A 'phase' argument is also included,\n",
    "and a phase of pi/2.0 clusters the points in the center with fewer points on the ends.\n",
    "\n",
    "```{Note}\n",
    "We have included three different distribution functions for users to replicate functionality that used to be built-in to the individual akima and bsplines components.\n",
    "```\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.spline_distributions.sine_distribution\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.spline_distributions.cell_centered\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "```{eval-rst}\n",
    "    .. autofunction:: openmdao.utils.spline_distributions.node_centered\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "Below is an example of `sine_distribution`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openmdao.utils.spline_distributions import sine_distribution\n",
    "\n",
    "x_cp = np.linspace(0., 1., 6)\n",
    "y_cp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "n = 20\n",
    "x = om.sine_distribution(20, start=0, end=1, phase=np.pi)\n",
    "\n",
    "prob = om.Problem()\n",
    "\n",
    "comp = om.SplineComp(method='akima', x_cp_val=x_cp, x_interp_val=x)\n",
    "prob.model.add_subsystem('akima1', comp)\n",
    "\n",
    "comp.add_spline(y_cp_name='ycp', y_interp_name='y_val', y_cp_val=y_cp)\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('akima1.y_val'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('akima1.y_val'),\n",
    "                  np.array([[5.         , 5.32381994,  6.28062691 , 7.79410646 , 9.64169506, 11.35166363,\n",
    "                             12.26525921, 12.99152288, 13.77257256, 14.58710327, 15.41289673, 16.28341046,\n",
    "                             17.96032258, 20.14140712, 22.31181718, 24.40891577, 26.27368825, 27.74068235,\n",
    "                             28.67782484, 29.        ]]),\n",
    "                  tolerance=1e-8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Standalone Interface for Spline Evaluation\n",
    "\n",
    "The underlying interpolation algorithms can be used standalone (i.e., outside of the SplineComp) through the\n",
    "`InterpND` class. This can be useful for inclusion in another component. The following example shows how to\n",
    "create and evaluate a standalone Akima spline:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openmdao.components.interp_util.interp import InterpND\n",
    "\n",
    "xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "n = 50\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "interp = InterpND(method='akima', points=xcp, x_interp=x, delta_x=0.1)\n",
    "y = interp.evaluate_spline(ycp)\n",
    "\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(y,\n",
    "                  np.array([ 5.       ,  7.20902005,  9.21276849, 10.81097162, 11.80335574,\n",
    "                             12.1278001 , 12.35869145, 12.58588536, 12.81022332, 13.03254681,\n",
    "                             13.25369732, 13.47451633, 13.69584534, 13.91852582, 14.14281484,\n",
    "                             14.36710105, 14.59128625, 14.81544619, 15.03965664, 15.26399335,\n",
    "                             15.48853209, 15.7133486 , 15.93851866, 16.16573502, 16.39927111,\n",
    "                             16.63928669, 16.8857123 , 17.1384785 , 17.39751585, 17.66275489,\n",
    "                             17.93412619, 18.21156029, 18.49498776, 18.78433915, 19.07954501,\n",
    "                             19.38053589, 19.68724235, 19.99959495, 20.31752423, 20.64096076,\n",
    "                             20.96983509, 21.37579297, 21.94811407, 22.66809748, 23.51629844,\n",
    "                             24.47327219, 25.51957398, 26.63575905, 27.80238264, 29.        ]),\n",
    "                  tolerance=1e-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Similiarly, the following example shows how to create a bspline:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openmdao.components.interp_util.interp import InterpND\n",
    "\n",
    "xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "n = 50\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "interp = InterpND(method='bsplines', num_cp=6, x_interp=x)\n",
    "\n",
    "y = interp.evaluate_spline(ycp)\n",
    "\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(y,\n",
    "                  np.array([ 9.21614583,  9.90911525, 10.52244151, 11.06231159, 11.53491244,\n",
    "                             11.94643105, 12.30305438, 12.61096939, 12.87636305, 13.10542234,\n",
    "                             13.30433422, 13.47928566, 13.63646363, 13.7820551 , 13.92203064,\n",
    "                             14.05954727, 14.19579437, 14.33192094, 14.46907599, 14.60840854,\n",
    "                             14.75106758, 14.89820214, 15.05096121, 15.2104938 , 15.37794893,\n",
    "                             15.5544756 , 15.74122282, 15.9393396 , 16.14997495, 16.37427787,\n",
    "                             16.61339737, 16.86848247, 17.14102103, 17.43486416, 17.75486932,\n",
    "                             18.10589772, 18.49281052, 18.92046894, 19.39373414, 19.91746734,\n",
    "                             20.4965297 , 21.13578243, 21.8400867 , 22.61430372, 23.46329467,\n",
    "                             24.39192074, 25.40504312, 26.507523  , 27.70422156, 29.        ]),\n",
    "                  tolerance=1e-6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can also compute the derivative of the interpolated output with respect to the control point values by setting\n",
    "the \"compute_derivate\" argument to True:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openmdao.components.interp_util.interp import InterpND\n",
    "\n",
    "xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])\n",
    "ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])\n",
    "n = 5\n",
    "x = np.linspace(1.0, 12.0, n)\n",
    "\n",
    "interp = InterpND(method='akima', points=xcp, x_interp=x, delta_x=0.1)\n",
    "y, dy_dycp = interp.evaluate_spline(ycp, compute_derivative=True)\n",
    "\n",
    "print(dy_dycp)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(dy_dycp,\n",
    "                  np.array([[ 1.00000000e+00,  0.00000000e+00,  0.00000000e+00,\n",
    "                              0.00000000e+00,  0.00000000e+00,  0.00000000e+00],\n",
    "                            [-1.86761492e-06,  3.31278014e-02,  1.05874907e+00,\n",
    "                             -9.18750000e-02,  0.00000000e+00,  0.00000000e+00],\n",
    "                            [ 0.00000000e+00,  0.00000000e+00, -2.10964627e-01,\n",
    "                              1.19119941e+00,  2.02602810e-02, -4.95062934e-04],\n",
    "                            [ 0.00000000e+00,  0.00000000e+00, -2.64126732e-01,\n",
    "                              5.82784977e-01,  6.83151998e-01, -1.81024253e-03],\n",
    "                            [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,\n",
    "                              0.00000000e+00,  0.00000000e+00,  1.00000000e+00]]),\n",
    "                  tolerance=1e-6)"
   ]
  }
 ],
 "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": 4
}
