{ "cells": [ { "cell_type": "markdown", "id": "309c6d2c", "metadata": {}, "source": [ "# Setup" ] }, { "cell_type": "markdown", "id": "c94f0187", "metadata": {}, "source": [ "This notebook can be found [here](https://github.com/nicolaipalm/paref/blob/main/docs/notebooks/main_example_small.ipynb)." ] }, { "cell_type": "markdown", "id": "ef516a2e", "metadata": {}, "source": [ "Let us imagine the following situation: We have a blackbox function (bbf) that we want to optimize multicriterially or, synonymously, multiobjectively (multi objective optimization, MOO). In other words, we are searching for the bbf Pareto front. The bbf can represent a machine for which we can set certain parameters (vector $x \\in D$, where D represents the domain of the problem, which we also call design space) and which then reacts measurably via certain target variables (vector $y \\in T$, where T represents the co-domain of the problem, which we also call target space). Or the bbf represents a simulation model, where we can also define specifications x for its design and obtain responses y through simulation.\n", " \n", "In our example here, we choose as bbf the mathematical test function Zitzler-Deb-Thiele N.1 (ZDT-1, reference: Deb, Kalyan; Thiele, L.; Laumanns, Marco; Zitzler, Eckart (2002). \"Scalable multi-objective optimization test problems\". Proceedings of the 2002 IEEE Congress on Evolutionary Computation. Vol. 1. pp. 825–830. doi:10.1109/CEC.2002.1007032). In the further course of our example, however, we assume that we do not know this function (which makes it a bbf). We can only \"test\" the bbf qua function calls in places we choose. \n", "\n", "This approach offers the following advantage: ZDT-1 is not only defined analytically, but we also know (analytically) its Pareto front. Thus, we can immediately compare our results with the expected results and see how well our new approach works.\n", " \n", "In many cases, the general MOO problem is formulated as \"find the Pareto front\" and not specified more clearly. Many MOO algorithms are accordingly designed to do just that: They identify Pareto-optimal points (i.e. points on the Paretofront), but they do not guarantee the user any further properties (e.g. to identify co-domain corner points of the Paretofront). The package paref now allows exactly that: besides the general property \"identified points are Pareto-optimal\", paref users can specify further properties of the Paretofront to be identified and construct MOO algorithms based on those defined properties. Let's make it concrete for our example:\n", " \n", "From our bbf ZDT-1 we want to identify Pareto-optimal solutions that a) represent the co-domain corners of the Pareto front and b) reflect its path in equidistant steps. The first property gives us an answer to the question \"in which target area are there Pareto-optimal trade-offs of our bbf at all?\". The second property answers the question \"what does the path of the bbf Pareto front look like in a grid that I can specify?\". Both are quite typical questions that arise in the context of a bbf MOO. Let's get down to work and see how all these considerations transfer into code....\n", " \n" ] }, { "cell_type": "markdown", "id": "bba8082e", "metadata": {}, "source": [ "## Step: Definition of blackbox function" ] }, { "cell_type": "markdown", "id": "1a15b91a", "metadata": {}, "source": [ "In our environment, we first need to define how many dimensions our domain (Design Space) and co-domain (Target Space) have. In our example (ZDT-1) we want to map 10 Design Space dimensions to 2 Target Space dimensions. \n", "Second, we must define how it can retrieve function values at selected points, i.e. we must declare calls to our bbf. Those four information, i.e.\n", "- assignment of vector of design variables to vector of target values (here given by ZDT-1) implemented in the ``__call__`` method\n", "- dimension of design space (here 10)\n", "- dimension of target space (here 2)\n", "- design space definition (here $[0,1]^{10}$)\n", "\n", "must be implemented in Parefs\\` ``BlackboxFunction``.\n", "\n", " CAUTION: by default, the evaluations of the blackbox function must be stored in the ``self._evaluations``variable and must be of the form [x,y] where x is a one dimensional numpy array representing the design vector and y a one dimensional numpy array representing the corresponding target vector!\n", "\n", "The corresponding code looks like this:" ] }, { "cell_type": "code", "execution_count": null, "id": "347151ee", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "from paref.black_box_functions.design_space.bounds import Bounds\n", "from paref.interfaces.moo_algorithms.blackbox_function import BlackboxFunction\n", "\n", "\n", "class ZDT1(BlackboxFunction):\n", " def __call__(self, x: np.ndarray) -> np.ndarray:\n", " n = len(x)\n", " f1 = x[0]\n", " g = 1 + 9 / (n - 1) * np.sum(x[1:])\n", " h = 1 - np.sqrt(f1 / g)\n", " f2 = g * h\n", " y = np.array([f1, f2])\n", " self._evaluations.append([x, y]) # store evaluation\n", " return y\n", "\n", " @property\n", " def dimension_design_space(self) -> int:\n", " return 5\n", "\n", " @property\n", " def dimension_target_space(self) -> int:\n", " return 2\n", "\n", " @property\n", " def design_space(self) -> Bounds:\n", " return Bounds(upper_bounds=np.ones(self.dimension_design_space),\n", " lower_bounds=np.zeros(self.dimension_design_space))" ] }, { "cell_type": "markdown", "id": "ee9fa97f", "metadata": {}, "source": [ "We then initialize an instance of this bbf." ] }, { "cell_type": "code", "execution_count": null, "id": "3cdb18f4", "metadata": {}, "outputs": [], "source": [ "bbf = ZDT1()" ] }, { "cell_type": "markdown", "id": "ce1ccf68", "metadata": {}, "source": [ "The Pareto front of ZDT-1 looks as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "07858e8f", "metadata": { "code_folding": [ 5, 13 ] }, "outputs": [], "source": [ "import plotly.graph_objects as go\n", "\n", "pareto_points_of_bbf = [i * np.eye(1, bbf.dimension_design_space, 0)[0] for i in np.arange(0, 1, 0.01)]\n", "pareto_front_of_bbf = np.array([bbf(point) for point in pareto_points_of_bbf])\n", "bbf.clear_evaluations()\n", "\n", "data = [\n", " go.Scatter(x=pareto_front_of_bbf.T[0],\n", " y=pareto_front_of_bbf.T[1],\n", " name='Real Pareto front',\n", " line=dict(width=4)\n", " ),\n", "]\n", "fig = go.Figure(data=data)\n", "fig.update_layout(\n", " title=\"Pareto front of ZDT-1\",\n", " width=600,\n", " height=600,\n", " plot_bgcolor='rgba(0,0,0,0)',\n", " legend=dict(\n", " x=0.2,\n", " y=0.9, )\n", ")\n", "fig.show()" ] }, { "cell_type": "markdown", "id": "ae25a084", "metadata": {}, "source": [ "Note that in general we are not able to calculate the Pareto front. In this particular case, we can and use this knowledge in order to validate our results." ] }, { "cell_type": "markdown", "id": "07c087a7", "metadata": {}, "source": [ "## Step: Declaration of generic MOO algorithm (genMOO)" ] }, { "cell_type": "markdown", "id": "a2c01823", "metadata": {}, "source": [ "In the third step we have to define a generic MOO algorithm genMOO, i.e. an operator that is able to return a set of Pareto points when we release it on a bbf. We do not need to require further properties of the genMOO algorithm. Such algorithms exist in great variety. In our example we choose the *minimization algorithm* ``DifferentialEvolutionMinimizer`` which is already implemented in Paref. \n", "This minimization algorithm exploits the fact that the underlying test function (ZDT-1) is cheap to sample.\n", "This looks as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "29830177", "metadata": {}, "outputs": [], "source": [ "from paref.moo_algorithms.minimizer.differential_evolution_minimizer import DifferentialEvolutionMinimizer\n", "\n", "moo = DifferentialEvolutionMinimizer()" ] }, { "cell_type": "markdown", "id": "9e9be416", "metadata": {}, "source": [ "In addition to determining which genMOO algorithm we want to use, we must bear in mind one of their fundamental properties: They only provide approximations for Pareto points or converge to Pareto points when used infinitely long or frequently. Accordingly, we have to define a so-called \"stop criterion\", i.e. a rule when we consider an approximation to be sufficient for our purposes. In the case of our example, we choose the criterion ``MaxIterationsReached`` which tells the MOO algorithm to stop after a specified number of iterations.\n", "We choose a maximum number of 100 iterations.\n", "The implementation lookes as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "f8d1736c", "metadata": {}, "outputs": [], "source": [ "from paref.moo_algorithms.stopping_criteria.max_iterations_reached import MaxIterationsReached\n", "\n", "stopping_criteria = MaxIterationsReached(max_iterations=12)" ] }, { "cell_type": "markdown", "id": "f852031d", "metadata": {}, "source": [ "## Step: User definition of properties for MOO search" ] }, { "cell_type": "markdown", "id": "54c515c9", "metadata": {}, "source": [ "In this step, the central paref added value-add follows: We define further properties that we demand from a - then user-defined - MOO (then parefMOO called) algorithm when we let it loose on the bbf. In our example case, we require the two properties mentioned above \n", "- (a) to identify the corners of the bbf Pareto front and \n", "- (b) to reproduce its path in equidistant steps. \n", "\n", "These two properties are each represented within the paref framework by individual Pareto reflections or sequences. Specifically property a) is represented by the sequence of Pareto reflections ``FindEdgePointsSequence``" ] }, { "cell_type": "code", "execution_count": null, "id": "4ebfcde2", "metadata": {}, "outputs": [], "source": [ "from paref.pareto_reflection_sequences.multi_dimensional.find_edge_points_sequence import FindEdgePointsSequence\n", "\n", "sequence_edge_points = FindEdgePointsSequence()" ] }, { "cell_type": "markdown", "id": "7e59c708", "metadata": {}, "source": [ "while property b) is represented by the sequence ``FillGapsOfParetoFrontSequence``" ] }, { "cell_type": "code", "execution_count": null, "id": "384f714a", "metadata": {}, "outputs": [], "source": [ "from paref.pareto_reflection_sequences.multi_dimensional.fill_gaps_of_pareto_front_sequence import \\\n", " FillGapsOfParetoFrontSequence\n", "\n", "sequence_equidistant_path = FillGapsOfParetoFrontSequence()" ] }, { "cell_type": "markdown", "id": "5199480c", "metadata": {}, "source": [ "## Step: parefMOO Sequence application and visualization of Pareto front with user defined properties" ] }, { "cell_type": "markdown", "id": "0361b4b0", "metadata": {}, "source": [ "In the last step, we first apply the parefMOO sequence defined above to the bbf. Note that the evaluations of the bbf are stored within the ``bbf.evaluations`` resp. ``bbf.y`` and ``bbf.x`` property.\n", "In order to apply the MOO to the sequence and bbf, we simply need to call the ``bbf.apply_to_sequence``method!\n", "We first apply the MOO to the ``FindEdgePointsSequence``:" ] }, { "cell_type": "code", "execution_count": null, "id": "6fb3945e", "metadata": {}, "outputs": [], "source": [ "moo.apply_to_sequence(sequence_pareto_reflections=sequence_edge_points,\n", " blackbox_function=bbf,\n", " stopping_criteria=stopping_criteria, )" ] }, { "cell_type": "markdown", "id": "734a5cb8", "metadata": {}, "source": [ "After determining the edge points, we apply the MOO (with the remaining number of evaluations) to the equidistance sequence:" ] }, { "cell_type": "code", "execution_count": null, "id": "3fec7f8d", "metadata": {}, "outputs": [], "source": [ "moo.apply_to_sequence(sequence_pareto_reflections=sequence_equidistant_path,\n", " blackbox_function=bbf,\n", " stopping_criteria=stopping_criteria, )" ] }, { "cell_type": "markdown", "id": "db7c39e7", "metadata": {}, "source": [ "In “real life” you may eventually have to be patient – this step may take a while. Once it is done, we can look at the corresponding result in the co-domain (target space):" ] }, { "cell_type": "code", "execution_count": null, "id": "cf931392", "metadata": { "code_folding": [ 0, 1, 13 ] }, "outputs": [], "source": [ "data = [\n", " go.Scatter(x=pareto_front_of_bbf.T[0],\n", " y=pareto_front_of_bbf.T[1],\n", " name='Real Pareto front',\n", " line=dict(width=4)\n", " ),\n", " go.Scatter(x=bbf.y.T[0], y=bbf.y.T[1],\n", " mode='markers',\n", " marker=dict(size=10),\n", " name='Determined Pareto points'\n", " ),\n", "]\n", "fig = go.Figure(data=data)\n", "fig.update_layout(\n", " width=600,\n", " height=600,\n", " plot_bgcolor='rgba(0,0,0,0)',\n", " legend=dict(\n", " x=0.2,\n", " y=0.9, )\n", ")\n", "fig.show()" ] } ], "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.8" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": true }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }