{
 "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": [
    "# MetaModelSemiStructuredComp\n",
    "\n",
    "Semi-structued training data occurs frequently in engineering applications like performance maps for turbine engine performance, propeller performance, aerodyanmic drag, etc. For example, a simple semi-structured grid could be a mostly structured grid, but with a few data points missing on one area of the grid. Another more general kind of semi-structured grid is a set of data where the first dimension is regular, but the other dimensions are irregular. An example of this is the figure below, where the 'x' dimension is regular, and the data points fall on distinct values, but the 'y' dimension is not regular.\n",
    "\n",
    "![semi_example](images/semi_example.jpg)\n",
    "\n",
    "The [MetaModelStructured](metamodelstructured_comp) cannot be used if any points are missing, or if any dimension is not regular. You could use [MetaModelUnStructured](metamodelunstructured_comp) for this data, but you would lose the performance and flexibility you can gain from exploiting the semi structured nature of the data.\n",
    "\n",
    "`MetaModelSemiStructuredComp` handles semi-structured data by treating every \"column\" of data beyond the first dimension independently.  In the figure above, it doesn't matter that two adjacent columns have different points, as the interpolation in 'y' happens independently in each column before those values are subsequently interpolated in 'x'. \n",
    "\n",
    "The following interpolation methods are supported in `MetaModelSemiStructuredComp`:\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",
    "```\n",
    "\n",
    "Note that `MetaModelSemiStructuredComp` only accepts scalar inputs and outputs. If you have a multivariable function, each input variable needs its own named OpenMDAO input.\n",
    "\n",
    "For multi-dimensional data, fits are computed on a separable per-axis basis. A single interpolation method is used for all dimensions, so the minimum table dimension must be high enough to use the chosen interpolate. Further, the minimum dimension will potentially be driven by the sparsest area of your data, so care should be taken to choose the smallest regular dimension as the first dimension.\n",
    "\n",
    "Extrapolation is supported, and is enabled by default. Given the more sparse nature of semi-structured data, we expect that extrapolation will occur more often. When evaluating a point, if any of the requested values falls beyond the first or last point in a row, you are extrapolating. There is always a risk when extrapolating, so it is important to be careful. Extrapolation can be disabled via the `extrapolate` option (see below).\n",
    "\n",
    "## MetaModelSemiStructuredComp Options\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "om.show_options_table(\"openmdao.components.meta_model_semi_structured_comp.MetaModelSemiStructuredComp\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## MetaModelSemiStructuredComp Constructor\n",
    "\n",
    "The call signature for the `MetaModelSemiStructuredComp` constructor is:\n",
    "\n",
    "```{eval-rst}\n",
    "    .. automethod:: openmdao.components.meta_model_semi_structured_comp.MetaModelSemiStructuredComp.__init__\n",
    "        :noindex:\n",
    "```\n",
    "\n",
    "## MetaModelSemiStructuredComp Examples\n",
    "\n",
    "A simple quick-start example is fitting a 2-dimensional data set that has been evaluated on a small selection of semi structured points:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "data_x = np.array([\n",
    "    1.0,\n",
    "    1.0,\n",
    "    1.0,\n",
    "    1.0,\n",
    "    1.0,\n",
    "\n",
    "    1.3,\n",
    "    1.3,\n",
    "    1.3,\n",
    "    1.3,\n",
    "    1.3,\n",
    "\n",
    "    1.6,\n",
    "    1.6,\n",
    "    1.6,\n",
    "    1.6,\n",
    "    1.6,\n",
    "\n",
    "    2.1,\n",
    "    2.1,\n",
    "    2.1,\n",
    "    2.1,\n",
    "    2.1,\n",
    "\n",
    "    2.5,\n",
    "    2.5,\n",
    "    2.5,\n",
    "    2.5,\n",
    "    2.5,\n",
    "    2.5,\n",
    "\n",
    "    2.9,\n",
    "    2.9,\n",
    "    2.9,\n",
    "    2.9,\n",
    "    2.9,\n",
    "\n",
    "    3.2,\n",
    "    3.2,\n",
    "    3.2,\n",
    "    3.2,\n",
    "\n",
    "    3.6,\n",
    "    3.6,\n",
    "    3.6,\n",
    "    3.6,\n",
    "    3.6,\n",
    "    3.6,\n",
    "\n",
    "    4.3,\n",
    "    4.3,\n",
    "    4.3,\n",
    "    4.3,\n",
    "\n",
    "    4.6,\n",
    "    4.6,\n",
    "    4.6,\n",
    "    4.6,\n",
    "    4.6,\n",
    "    4.6,\n",
    "\n",
    "    4.9,\n",
    "    4.9,\n",
    "    4.9,\n",
    "    4.9,\n",
    "    4.9,\n",
    "])\n",
    "\n",
    "data_y = np.array([\n",
    "    1.0,\n",
    "    1.5,\n",
    "    1.6,\n",
    "    1.7,\n",
    "    1.9,\n",
    "\n",
    "    1.0,\n",
    "    1.5,\n",
    "    1.6,\n",
    "    1.7,\n",
    "    1.9,\n",
    "\n",
    "    1.0,\n",
    "    1.5,\n",
    "    1.6,\n",
    "    1.7,\n",
    "    1.9,\n",
    "\n",
    "    1.0,\n",
    "    1.6,\n",
    "    1.7,\n",
    "    1.9,\n",
    "    2.4,\n",
    "\n",
    "    1.3,\n",
    "    1.7,\n",
    "    1.9,\n",
    "    2.4,\n",
    "    2.6,\n",
    "    2.9,\n",
    "\n",
    "    1.9,\n",
    "    2.1,\n",
    "    2.3,\n",
    "    2.5,\n",
    "    3.1,\n",
    "\n",
    "    2.3,\n",
    "    2.5,\n",
    "    3.1,\n",
    "    3.7,\n",
    "\n",
    "    2.3,\n",
    "    3.1,\n",
    "    3.3,\n",
    "    3.7,\n",
    "    4.1,\n",
    "    4.2,\n",
    "\n",
    "    3.3,\n",
    "    3.6,\n",
    "    4.0,\n",
    "    4.5,\n",
    "\n",
    "    3.9,\n",
    "    4.2,\n",
    "    4.4,\n",
    "    4.5,\n",
    "    4.6,\n",
    "    4.7,\n",
    "\n",
    "    4.4,\n",
    "    4.5,\n",
    "    4.6,\n",
    "    4.7,\n",
    "    4.9,\n",
    "])\n",
    "\n",
    "data_values = 3.0 + np.sin(data_x*0.2) * np.cos(data_y*0.3)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the table input data is expressed as flat ndarrays (or lists). They should be sorted in ascending coordinate order starting with the lowest n-tuple. The table output values are also in a flat array that corresponds to the input arrays."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import openmdao.api as om\n",
    "\n",
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "interp = om.MetaModelSemiStructuredComp(method='lagrange2')\n",
    "\n",
    "# The order that the inputs are added matters. The first table dimension is 'x'.\n",
    "interp.add_input('x', data_x)\n",
    "interp.add_input('y', data_y)\n",
    "\n",
    "interp.add_output('f', training_data=data_values)\n",
    "\n",
    "# Including a second output to show that multiple outputs are supported.\n",
    "# All outputs use the same input grid but use different values at each point.\n",
    "interp.add_output('g', training_data=2.0 * data_values)\n",
    "        \n",
    "model.add_subsystem('interp', interp)\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('interp.x', np.array([3.1]))\n",
    "prob.set_val('interp.y', np.array([2.75]))\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('interp.f'), prob.get_val('interp.g'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output",
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials\n",
    "assert_near_equal(prob.get_val('interp.f'), 3.39415716, 1e-7)\n",
    "assert_near_equal(prob.get_val('interp.g'), 2.0 * 3.39415716, 1e-7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can also predict multiple independent output points by setting the `vec_size` argument to be equal to the number of points you want to predict. Here, we set it to 3 and predict 3 points with `MetaModelSemiStructuredComp`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "interp = om.MetaModelSemiStructuredComp(method='lagrange2', vec_size=3)\n",
    "\n",
    "# The order that the inputs are added matters. The first table dimension is 'x'.\n",
    "interp.add_input('x', data_x)\n",
    "interp.add_input('y', data_y)\n",
    "\n",
    "interp.add_output('f', training_data=data_values)\n",
    "\n",
    "model.add_subsystem('interp', interp)\n",
    "\n",
    "prob.setup()\n",
    "\n",
    "prob.set_val('interp.x', np.array([3.1, 4.7, 5.1]))\n",
    "prob.set_val('interp.y', np.array([2.75, 3.9, 4.2]))\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('interp.f'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('interp.f'), np.array([3.39415716, 3.31511313, 3.2608656 ]), 1e-7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, it is possible to compute gradients with respect to the given\n",
    "output training data. These gradients are not computed by default, but\n",
    "can be enabled by setting the option `training_data_gradients` to `True`.\n",
    "When this is done, for each output that is added to the component, a\n",
    "corresponding input is added to the component with the same name but with an\n",
    "`_train` suffix. This allows you to connect in the training data as an input\n",
    "array, if desired.\n",
    "\n",
    "The following simple example shows the preceding model with training inputs enabled."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "prob = om.Problem()\n",
    "model = prob.model\n",
    "\n",
    "interp = om.MetaModelSemiStructuredComp(method='lagrange2', training_data_gradients=True)\n",
    "interp.add_input('x', data_x)\n",
    "interp.add_input('y', data_y)\n",
    "\n",
    "# Initialize all trable values to zero.\n",
    "interp.add_output('f', training_data=np.zeros(len(data_x)))\n",
    "\n",
    "model.add_subsystem('interp', interp)\n",
    "\n",
    "prob.setup(force_alloc_complex=True)\n",
    "\n",
    "prob.set_val('interp.x', np.array([3.1]))\n",
    "prob.set_val('interp.y', np.array([2.75]))\n",
    "\n",
    "# The table values come from somewhere in OpenMDAO, in this case, from the indepvarcomp output.\n",
    "# They could also come from an upstream component if you connect the input to another source.\n",
    "prob.set_val('interp.f_train', data_values)\n",
    "\n",
    "prob.run_model()\n",
    "\n",
    "print(prob.get_val('interp.f'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "assert_near_equal(prob.get_val('interp.f'), 3.39415716, 1e-7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Standalone Interface for Table Interpolation\n",
    "\n",
    "The underlying interpolation algorithms can be used standalone (i.e., outside of the\n",
    "MetaModelSemiStructuredComp) through the `InterpNDSemi` class. This can be useful for inclusion in another\n",
    "component.  The following component shows how to perform interpolation on the a much simpler table than before. We only have 2 points at x=1.0, so we can only choose 'slinear'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from openmdao.components.interp_util.interp_semi import InterpNDSemi\n",
    "\n",
    "# Simple grid with one point missing.\n",
    "x = [1.0, 1.0, 2.0, 2.0, 2.0]\n",
    "y = [1.0, 2.0, 1.0, 2.0, 3.0]\n",
    "values = [1.0, 2.5, 1.5, 4.0, 4.5]\n",
    "\n",
    "grid = np.array([x, y]).T\n",
    "print(grid)\n",
    "\n",
    "# We only have 2 points at x=1.0, so 'slinear' is the only one we can use.\n",
    "interp = InterpNDSemi(grid, values, method='slinear')\n",
    "\n",
    "x = np.array([1.5, 1.5])\n",
    "f, df_dx = interp.interpolate(x, compute_derivative=True)\n",
    "\n",
    "print('value', f)\n",
    "print('derivative', df_dx)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-output",
     "remove-input"
    ]
   },
   "outputs": [],
   "source": [
    "from openmdao.utils.assert_utils import assert_near_equal\n",
    "\n",
    "actual = np.array([2.25])\n",
    "deriv_actual = np.array([[1.0, 2.0]])\n",
    "\n",
    "assert_near_equal(f, actual, tolerance=1e-7)\n",
    "assert_near_equal(df_dx, deriv_actual, tolerance=1e-7)"
   ]
  }
 ],
 "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.10"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
