{ "cells": [ { "cell_type": "markdown", "metadata": { "vscode": { "languageId": "plaintext" } }, "source": [ "# New Optimizer Development\n", "\n", "New optimizers can be developed within modOpt.\n", "Developing optimizers in modOpt offers several advantages.\n", "For instance, stable and efficient modules already available in modOpt\n", "can be reused for the new algorithm, eliminating the need for\n", "developers to implement these components from scratch.\n", "Since all optimizers in modOpt must be derived from the `Optimizer` base class,\n", "tools for checking first derivatives, visualizing, recording, and hot-starting\n", "are automatically inherited by new optimizers.\n", "\n", "Subclasses derived from `Optimizer` must implement an `initialize` method that sets \n", "the `solver_name` and declares any optimizer-specific options. \n", "Developers are required to define the `available_outputs`\n", "attribute within the `initialize` method. \n", "This attribute specifies the data that the optimizer will provide \n", "after each iteration of the algorithm by calling the `update_outputs` method. \n", "Developers must also define a `setup` method to handle \n", "any pre-processing of the problem data and configuration of the optimizer’s modules.\n", "\n", "The core of an optimizer in modOpt lies in the `solve` method. \n", "This method implements the numerical algorithm and iteratively calls \n", "the `‘_compute’` methods from the problem object.\n", "Upon completion of the optimization, the `solve` method should \n", "assign a `results` attribute that holds the optimization results \n", "in the form of a dictionary. \n", "Developers may optionally implement a `print_results` method \n", "to override the default implementation provided by the base class \n", "and customize the presentation of the results.\n", "\n", "Developers may need to implement additional methods for setting up constraints, \n", "their bounds, and derivatives, depending on their optimizer.\n", "\n", "```{note}\n", "Since HDF5 files from optimizer recording are incompatible with text editors, \n", "developers can provide users with the `readable_outputs` \n", "option during optimizer instantiation to export optimizer-generated\n", "data as plain text files. \n", "For each variable listed in `readable_outputs`, a separate file is generated,\n", "with rows representing optimizer iterations.\n", "While creating a new optimizer, developers may declare this option so that\n", "users are able to take advantage of this feature already implemented in\n", "the `Optimizer` base class.\n", "The list of variables allowed for `readable_outputs` is any\n", "subset of the keys in the `available_outputs` attribute.\n", "```\n", "\n", "## Developing the BFGS optimizer\n", "\n", "The following example shows the implementation of the BFGS algorithm for unconstrained\n", "optimization, employing modules for approximating Hessians and performing line searches.\n", "The Hessians are approximated using a damped BFGS algorithm and \n", "the line searches enforce strong Wolfe conditions.\n", "The code also demonstrates how to document the API for a new optimizer." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import time\n", "from modopt import Optimizer\n", "from modopt.line_search_algorithms import Minpack2LS\n", "from modopt.approximate_hessians import BFGSScipy as BFGS\n", "\n", "class QuasiNewton(Optimizer):\n", " \"\"\"\n", " Quasi-Newton method for unconstrained optimization.\n", "\n", " Parameters\n", " ----------\n", " problem : Problem or ProblemLite\n", " Object containing the problem to be solved.\n", " recording : bool, default=False\n", " If ``True``, record all outputs from the optimization.\n", " This needs to be enabled for hot-starting the same problem later,\n", " if the optimization is interrupted.\n", " out_dir : str, optional\n", " The directory to store all the output files generated from the optimization.\n", " hot_start_from : str, optional\n", " The record file from which to hot-start the optimization.\n", " hot_start_atol : float, default=0.\n", " The absolute tolerance check for the inputs\n", " when reusing outputs from the hot-start record.\n", " hot_start_rtol : float, default=0.\n", " The relative tolerance check for the inputs\n", " when reusing outputs from the hot-start record.\n", " visualize : list, default=[]\n", " The list of scalar variables to visualize during the optimization.\n", " keep_viz_open : bool, default=False\n", " If ``True``, keep the visualization window open after the optimization is complete.\n", " turn_off_outputs : bool, default=False\n", " If ``True``, prevent modOpt from generating any output files.\n", "\n", " maxiter : int, default=500\n", " Maximum number of iterations.\n", " opt_tol : float, default=1e-6\n", " Optimality tolerance.\n", " Certifies convergence when the 2-norm of the gradient is less than this value.\n", " readable_outputs : list, default=[]\n", " List of outputs to be written to readable text output files.\n", " Available outputs are: 'itr', 'obj', 'x', 'opt', 'time', 'nfev', 'ngev', 'step'.\n", " \"\"\"\n", " def initialize(self):\n", " self.solver_name = 'bfgs'\n", "\n", " self.obj = self.problem._compute_objective\n", " self.grad = self.problem._compute_objective_gradient\n", "\n", " self.options.declare('maxiter', default=500, types=int)\n", " self.options.declare('opt_tol', types=float, default=1e-6)\n", " self.options.declare('readable_outputs', types=list, default=[])\n", "\n", " self.available_outputs = {\n", " 'itr': int,\n", " 'obj': float,\n", " # for arrays from each iteration, sizes need to be declared\n", " 'x': (float, (self.problem.nx, )),\n", " 'opt': float,\n", " 'time': float,\n", " 'nfev': int,\n", " 'ngev': int,\n", " 'step': float,\n", " }\n", "\n", " def setup(self):\n", " self.LS = Minpack2LS(f=self.obj, g=self.grad)\n", " self.QN = BFGS(nx=self.problem.nx,\n", " exception_strategy='damp_update')\n", "\n", " def solve(self):\n", " # Assign shorter names to variables and methods\n", " opt_tol = self.options['opt_tol']\n", " maxiter = self.options['maxiter']\n", "\n", " obj = self.obj\n", " grad = self.grad\n", "\n", " start_time = time.time()\n", "\n", " # Set initial values for current iterates\n", " x_k = self.problem.x0 * 1.\n", " f_k = obj(x_k)\n", " g_k = grad(x_k)\n", "\n", " # Iteration counter\n", " itr = 0\n", "\n", " opt = np.linalg.norm(g_k) # optimality measure\n", " nfev = 1 # number of objective function evaluations\n", " ngev = 1 # number of objective gradient evaluations\n", "\n", " # Initializing declared outputs\n", " self.update_outputs(itr=0,\n", " x=x_k,\n", " obj=f_k,\n", " opt=opt,\n", " time=time.time() - start_time,\n", " nfev=nfev,\n", " ngev=ngev,\n", " step=0.)\n", "\n", " while (opt > opt_tol and itr < maxiter):\n", " itr_start = time.time()\n", " itr += 1\n", "\n", " # Hessian approximation\n", " B_k = self.QN.B_k\n", "\n", " # ALGORITHM STARTS HERE\n", " # >>>>>>>>>>>>>>>>>>>>>\n", "\n", " # Compute the search direction toward the next iterate\n", " p_k = np.linalg.solve(B_k, -g_k)\n", "\n", " # Compute the step length along the search direction via a line search\n", " alpha, f_k, g_new, slope_new, new_f_evals, new_g_evals, converged = self.LS.search(\n", " x=x_k, p=p_k, f0=f_k, g0=g_k)\n", "\n", " nfev += new_f_evals\n", " ngev += new_g_evals\n", "\n", " # A step of length 1e-4 is taken along p_k if line search does not converge\n", " if not converged:\n", " alpha = 1e-4\n", " d_k = p_k * alpha\n", "\n", " x_k += d_k\n", " f_k = obj(x_k)\n", "\n", " g_new = grad(x_k)\n", " w_k = g_new - g_k\n", " g_k = g_new\n", "\n", " else:\n", " d_k = alpha * p_k\n", " x_k += d_k\n", "\n", " w_k = g_new - g_k\n", " g_k = g_new\n", "\n", " opt = np.linalg.norm(g_k)\n", "\n", " # Update the Hessian approximation\n", " self.QN.update(d_k, w_k)\n", "\n", " # <<<<<<<<<<<<<<<<<<<\n", " # ALGORITHM ENDS HERE\n", "\n", " # Update arrays inside outputs dict with new values from the current iteration\n", " self.update_outputs(itr=itr,\n", " x=x_k,\n", " obj=f_k,\n", " opt=opt,\n", " time=time.time() - start_time,\n", " nfev=nfev,\n", " ngev=ngev,\n", " step=alpha)\n", "\n", " self.total_time = time.time() - start_time\n", "\n", " self.results = {\n", " 'x': x_k, \n", " 'objective': f_k, \n", " 'optimality': opt, \n", " 'nfev': nfev, \n", " 'ngev': ngev,\n", " 'niter': itr, \n", " 'time': self.total_time,\n", " 'converged': opt <= opt_tol,\n", " }\n", " \n", " # Run post-processing for the Optimizer() base class\n", " self.run_post_processing()\n", " \n", " return self.results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solving an unconstrained problem using the BFGS optimizer\n", "\n", "In the following code, we solve the problem\n", "\n", "$$\n", "\\underset{x_1, x_2 \\in \\mathbb{R}}{\\text{minimize}} \\quad x_1^2 + x_2^2\n", "$$\n", "\n", "using the `QuasiNewton` optimizer we developed above." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\tSolution from modOpt:\n", "\t----------------------------------------------------------------------------------------------------\n", "\tProblem : quartic\n", "\tSolver : bfgs\n", "\tx : [0. 0.]\n", "\tobjective : 0.0\n", "\toptimality : 0.0\n", "\tnfev : 4\n", "\tngev : 4\n", "\tniter : 2\n", "\ttime : 0.010946035385131836\n", "\tconverged : True\n", "\ttotal_callbacks : 8\n", "\tobj_evals : 4\n", "\tgrad_evals : 4\n", "\thess_evals : 0\n", "\tcon_evals : 0\n", "\tjac_evals : 0\n", "\treused_callbacks : 0\n", "\tout_dir : quartic_outputs/2025-01-27_12.49.49.006498\n", "\t----------------------------------------------------------------------------------------------------\n", "\n", "================================================================================================================\n", " modOpt summary table: \n", "================================================================================================================\n", " # itr obj opt time nfev ngev step \n", " 0 0 2.500250E+05 1.000050E+03 1.288891E-03 1 1 0.000000E+00 \n", " 1 1 2.500250E-03 1.000050E-01 1.006889E-02 3 3 4.999500E-01 \n", " 2 2 0.000000E+00 0.000000E+00 1.075673E-02 4 4 1.000000E+00 \n", "================================================================================================================\n" ] } ], "source": [ "from modopt import ProblemLite\n", "name = 'quartic'\n", "x0 = np.array([500., 5.])\n", "obj = lambda x: np.sum(x**2)\n", "grad = lambda x: 2 * x\n", "prob = ProblemLite (name=name, x0=x0, obj=obj, grad =grad)\n", "\n", "optimizer = QuasiNewton(problem=prob, maxiter=100, opt_tol=1e-6)\n", "results = optimizer.solve()\n", "optimizer.print_results(summary_table=True, all=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Modules\n", "\n", "Reusable modules are available for line searches, merit functions, Hessian approximations,\n", "and quadratic programming.\n", "For more details on the `Optimizer` class or any of the modules, \n", "visit the [API Reference](./api.md) page.\n", "\n", "Advanced users should note that the repository includes additional modules \n", "beyond those documented in the [API Reference](./api.md). \n", "Exploring the directories corresponding to the respective module categories might be helpful." ] } ], "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.9.13" } }, "nbformat": 4, "nbformat_minor": 2 }