{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d8d60374",
   "metadata": {},
   "source": [
    "# Noisy Rosenbrock Optimization\n",
    "\n",
    "Consider optimizing a noisy variant of the Rosenbrock function. The standard Rosenbrock function is defined as:\n",
    "\n",
    "$$\n",
    "f_{\\text{rosenbrock}}(\\mathbf{x}) = \\sum_{i=1}^{D-1} \\left[ 100 (\\mathbf{x}_i^2 - \\mathbf{x}_{i+1})^2 + (\\mathbf{x}_i - 1)^2 \\right],\n",
    "$$\n",
    "\n",
    "which achieves its global minimum $f_{\\text{rosenbrock}}(\\mathbf{x}^*) = 0$ at $\\mathbf{x}^* = (1,1,\\ldots,1)^T$. This function decomposes naturally into $D-1$ element functions:\n",
    "\n",
    "$$\n",
    "f_{\\text{rosenbrock}}(\\mathbf{x}) = \\sum_{i=1}^{D-1} f_i(\\mathbf{x}),\n",
    "$$\n",
    "\n",
    "where each $f_i(\\mathbf{x}) = 100 (\\mathbf{x}_i^2 - \\mathbf{x}_{i+1})^2 + (\\mathbf{x}_i - 1)^2$ depends exclusively on the $i$-th and $(i+1)$-th components of $\\mathbf{x}$.\n",
    "\n",
    "We introduce multiplicative noise to this function, creating the modified objective:\n",
    "\n",
    "$$\n",
    "\\widetilde{f}(\\mathbf{x}) = \\sum_{i=1}^{D-1} \\left[ f_i(\\mathbf{x}) \\cdot (1 + \\varepsilon \\cdot \\xi_i) \\right],\n",
    "$$\n",
    "\n",
    "where $\\varepsilon > 0$ is a small noise coefficient, and each $\\xi_i$ is an independent random variable with $\\mathbb{E}[\\xi_i] = 0$ and $\\text{Var}(\\xi_i) = 1$.\n",
    "\n",
    "The problem definition begins with the following code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "6afaf789",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The Rosenbrock Problem with 19 Elements and 20 Dimensions.\n"
     ]
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from upoqa.problems import PSProblem\n",
    "import numpy as np\n",
    "\n",
    "# Define the element function\n",
    "def rosenbrock_ele(z):    # input is z = [x[i-1], x[i]] for each i-th element\n",
    "    return 100.0 * (z[0] ** 2 - z[1]) ** 2 + (z[0] - 1) ** 2\n",
    "\n",
    "# there are 19 elements\n",
    "D = 20\n",
    "\n",
    "# every element is the same as rosenbrock_ele\n",
    "elements = dict([(f\"ele {i}\", rosenbrock_ele) for i in range(1, D)])\n",
    "\n",
    "# the i-th element depends on x[i-1] and x[i] \n",
    "coords = dict([(f\"ele {i}\", [i - 1, i]) for i in range(1, D)])\n",
    "\n",
    "# Tne elemental noise\n",
    "eps = 5e-3\n",
    "def noise(f: float, *args):\n",
    "    return f * (1 + eps * np.random.randn())\n",
    "\n",
    "# create the problem\n",
    "prob = PSProblem(elements, coords, noise_wrapper = noise)\n",
    "prob.update_meta_info({'name': 'Rosenbrock'})\n",
    "print(prob)\n",
    "\n",
    "# let's start from [-1, -1, ..., -1]\n",
    "x0 = - np.ones(prob.dim)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c5231b6",
   "metadata": {},
   "source": [
    "First, let's optimize the problem using conventional methods, and none of these methods converge near the solution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ed65ddf5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Function value obtained by COBYQA: 18.77492619915688\n",
      "Function value obtained by COBYLA: 18.87100851395784\n",
      "Function value obtained by UPOQA: 17.825729297079132\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 47668 characters]",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import scipy\n",
    "import upoqa\n",
    "from itertools import cycle\n",
    "\n",
    "prob.set_figure_size((8, 4))\n",
    "\n",
    "PLOT_LINE_FMT_CYCLER = iter(cycle(['-', '--', ':', '-.']))\n",
    "PLOT_MARKER_FMT_CYCLER = iter(cycle(['o', 's', '^', 'D', '+', '*']))\n",
    "\n",
    "# COBYQA\n",
    "res_cobyqa = scipy.optimize.minimize(prob.fun_eval, x0, method='cobyqa')\n",
    "print(\"Function value obtained by COBYQA:\", res_cobyqa.fun)\n",
    "prob.disp_fun_history(label = \"COBYQA\", show_fig = False, fmt = next(PLOT_LINE_FMT_CYCLER))\n",
    "prob.clear()\n",
    "\n",
    "# COBYLA\n",
    "res_cobyla = scipy.optimize.minimize(prob.fun_eval, x0, method='cobyla')\n",
    "print(\"Function value obtained by COBYLA:\", res_cobyla.fun)\n",
    "prob.disp_fun_history(label = \"COBYLA\", show_fig = False, fmt = next(PLOT_LINE_FMT_CYCLER))\n",
    "prob.clear()\n",
    "\n",
    "# UPOQA\n",
    "def nfev_recorder(intermediate_result):\n",
    "    with prob.debug_mode(incre_nfev = False, noisy = False):\n",
    "        fval_nf = prob.fun_eval(intermediate_result.x)\n",
    "    prob._update_history_fun(fval_nf)\n",
    "    \n",
    "res_upoqa = upoqa.minimize(prob.fun, x0, coords, callback = nfev_recorder, disp = False)\n",
    "print(\"Function value obtained by UPOQA:\", res_upoqa.fun)\n",
    "prob.disp_fun_history(label = \"UPOQA\", show_fig = True, fmt = next(PLOT_LINE_FMT_CYCLER))\n",
    "prob.clear()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a870e1b",
   "metadata": {},
   "source": [
    "None of these methods appear to converge to the optimal solution (function value = 0.0). We now configure `upoqa` with `noise_level = 1`, which activates two key features:\n",
    "\n",
    "1. **Expanded interpolation points**: The algorithm slightly increases the number of interpolation points used for function modeling, enhancing modeling accuracy in noisy environments.\n",
    "2. **Restart mechanism**: Similar to the approach used in `pybobyqa`, this feature enables the algorithm to restart with an enlarged trust-region radius upon convergence detection, increasing the probability of discovering improved solutions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "83184063",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Function value obtained by UPOQA with noise_level = 1: 1.4043661133316282e-11\n",
      "Solution obtained by UPOQA with noise_level = 1: [1.         1.00000001 0.99999999 0.99999999 1.00000003 1.00000003\n",
      " 1.00000009 1.00000013 1.00000005 1.00000001 1.         1.\n",
      " 1.00000003 0.99999997 0.99999995 0.9999999  0.99999981 0.99999965\n",
      " 0.99999942 0.99999861]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 53104 characters]",
      "text/plain": [
       "<Figure size 800x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "res_upoqa_n1 = upoqa.minimize(prob.fun, x0, coords, disp = False, \n",
    "                              noise_level = 1, maxfev = 800, callback = nfev_recorder)\n",
    "print(\"Function value obtained by UPOQA with noise_level = 1:\", res_upoqa_n1.fun)\n",
    "print(\"Solution obtained by UPOQA with noise_level = 1:\", res_upoqa_n1.x)\n",
    "prob.disp_fun_history(label = \"UPOQA with noise_level = 1\", show_fig = True, fmt = next(PLOT_LINE_FMT_CYCLER))\n",
    "prob.clear()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
