{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simple function minimization" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's minimize the following simple function:\n", "\n", "$$\n", "f(x, y) = (x - 3.6)^2 + 2 (y+1.1)^2\n", "$$\n", "\n", "The global minimum is obvious: $f=0$ for $x=3.6, y=-1.1$. We now want to find this minimum using `iwopy`, in a setup as follows:\n", "\n", "- Parameter bounds: $x, y \\in [-5,5]$\n", "- Unconstrained minimization\n", "- Gradient based optimizer [IPOPT](https://esa.github.io/pagmo2/docs/cpp/algorithms/ipopt.html) from [pygmo](https://esa.github.io/pygmo2/)\n", "- Analytical gradients\n", "\n", "The usage of finite difference gradients will be demonstrated directly after. We start by importing the required classes from the `iwopy` package:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from iwopy import SimpleProblem, SimpleObjective\n", "from iwopy.interfaces.pygmo import Optimizer_pygmo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `SimpleProblem` will be instantiated with two float type variables `x` and `y` which will be passed on to all linked objectives and constraints. The `SimpleObjective` (and also the `SimpleConstraint` classes assume the same variables as the problem, in the same order. We can therefore implement the above function $f(x, y)$ and the derivative \n", "$$\n", "g(v, x, y) = \\left\\{ \n", "\\begin{array}{ll}\n", "\\mathrm df / \\mathrm dx \\ ,& \\text{if} \\ v = 0 \\\\\n", "\\mathrm df / \\mathrm dy \\ ,& \\text{if} \\ v = 1\n", "\\end{array}\n", "\\right.\n", "$$ \n", "in a straight forward manner:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MinFunc(SimpleObjective):\n", " def f(self, x, y):\n", " return (x - 3.6) ** 2 + 2 * (y + 1.1) ** 2\n", "\n", " def g(self, v, x, y, components):\n", " return 2 * (x - 3.6) if v == 0 else 4 * (y + 1.1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the `components` argument of the function `g` is not used here since `f` is a single-component function.\n", "\n", "In the case of multi-component functions the parameter `n_components` has to be passed to the `__init__` function of `SimpleObjective`, and `f` has to return a list of scalars. In that case also `g` has to return a list of results, with same length as the requested `components`. The same rules apply to classes derived from `SimpleConstraint`.\n", "\n", "We can now proceed and setup the problem:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "problem = SimpleProblem(\n", " name=\"minf\",\n", " float_vars=[\"x\", \"y\"],\n", " init_values_float=[0.0, 0.0],\n", " min_values_float=[-5.0, -5.0],\n", " max_values_float=[5.0, 5.0],\n", ")\n", "problem.add_objective(MinFunc(problem))\n", "problem.initialize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that in a similar way you can easily add constraints to the problem, by defining them in a class that is derived from `iwopy.Constraint` or `iwopy.SimpleConstraint` and then adding them via `problem.add_constraint(...)`. \n", "\n", "Adding additional objectives works in the same way, simply repeat `problem.add_objective(...)` as often as you want. However, be aware that not all optimizers can handle multiple objective cases.\n", "\n", "Next, we create and initialize the solver:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "solver = Optimizer_pygmo(\n", " problem,\n", " problem_pars=dict(),\n", " algo_pars=dict(type=\"ipopt\", tol=1e-4),\n", ")\n", "solver.initialize()\n", "solver.print_info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here `tol` is an `IPOPT` parameter that defines the convergence tolerance. Now we are finally ready - let's solve the problem!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "results = solver.solve()\n", "solver.finalize(results)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(results)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `results` object is an instance of the class `OptResults`, cf. API. it carries attributes\n", "\n", "- `vars_int`: The integer variables of the solution\n", "- `vars_float`: The float variables of the solution\n", "- `objs`: The objective function values of the solution\n", "- `cons`: The constraint function values of the solution\n", "- `problem_results`: The object returned by the problem when applying the solution\n", "- `success` : Boolean flag indicating a successful solution\n", "- `vnames_int`: The names of the integer variables\n", "- `vnames_float`: The names of the float variables\n", "- `onames`: The names of the objectives (all components)\n", "- `cnames`: The names of the constraints (all components)\n", "\n", "Next, we want to explore finite difference gradient calculations for the same example. We can either remove the function `g(v, x, y, components)` from the `MinFunc` class, or create it invoking the parameter `has_ana_derivs=False`. This will then ignore the analytical gradient definition in the class.\n", "\n", "Additionally, we need to wrap the problem into a wrapper that provides numerical derivatives. There are two choices:\n", "\n", "- `DiscretizeRegGrid`: A wrapper that evaluates the problem on a regular grid and calculates derivatives based on fixed grid point locations. Interpolation in between is optional (and increases the number of required function evaluations). The grid also offers a memory such that re-calculations of grid point results are minimized.\n", "- `LocalFD`: A wrapper that evaluates local finite difference rules for derivative calculations. Hence the problem evaluation points are defined based on the current variables and hence not memorized since those usually differ with every call.\n", "\n", "The `LocalFD` is usually the faster choice, unless you are expecting a lot of benefits from the memory capabilities of the regular grid. Here is an example how to use it for solving our problem:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from iwopy import LocalFD" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "problem = SimpleProblem(\n", " name=\"minf\",\n", " float_vars=[\"x\", \"y\"],\n", " init_values_float=[0.0, 0.0],\n", " min_values_float=[-5.0, -5.0],\n", " max_values_float=[5.0, 5.0],\n", ")\n", "problem.add_objective(MinFunc(problem, has_ana_derivs=False))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "gproblem = LocalFD(problem, deltas=1e-4, fd_order=2)\n", "gproblem.initialize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `gproblem` object now corresponds to original `problem` with finite difference gradients of order 2. We can use the same gradient based solver as above for solving this problem:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "solver = Optimizer_pygmo(\n", " gproblem,\n", " problem_pars=dict(),\n", " algo_pars=dict(type=\"ipopt\", tol=1e-4),\n", ")\n", "solver.initialize()\n", "\n", "results = solver.solve()\n", "solver.finalize(results)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(results)" ] } ], "metadata": { "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 (default, Nov 14 2022, 12:59:47) \n[GCC 9.4.0]" }, "vscode": { "interpreter": { "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" } } }, "nbformat": 4, "nbformat_minor": 2 }