From b3d887d38d472a600434c558f9395c8143cc4454 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Tue, 6 Feb 2024 16:16:05 -0600 Subject: [PATCH] Add v0.4 docs --- 0.4/.buildinfo | 4 + 0.4/_sources/api/collectors.rst.txt | 42 + 0.4/_sources/api/components.rst.txt | 44 + 0.4/_sources/api/helpers.rst.txt | 18 + 0.4/_sources/api/problems.rst.txt | 57 + 0.4/_sources/api/solvers.rst.txt | 26 + 0.4/_sources/guide/collectors.ipynb.txt | 288 + 0.4/_sources/guide/features.ipynb.txt | 334 ++ 0.4/_sources/guide/primal.ipynb.txt | 291 + 0.4/_sources/guide/problems.ipynb.txt | 1607 ++++++ 0.4/_sources/guide/solvers.ipynb.txt | 251 + 0.4/_sources/index.rst.txt | 68 + .../tutorials/cuts-gurobipy.ipynb.txt | 541 ++ .../getting-started-gurobipy.ipynb.txt | 837 +++ .../tutorials/getting-started-jump.ipynb.txt | 680 +++ .../tutorials/getting-started-pyomo.ipynb.txt | 858 +++ 0.4/_static/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 221 bytes 0.4/_static/basic.css | 925 +++ ...index.c5995385ac14fb8791e8eb36b4908be2.css | 6 + 0.4/_static/css/theme.css | 117 + 0.4/_static/custom.css | 120 + 0.4/_static/doctools.js | 156 + 0.4/_static/documentation_options.js | 13 + 0.4/_static/file.png | Bin 0 -> 286 bytes 0.4/_static/images/logo_binder.svg | 19 + 0.4/_static/images/logo_colab.png | Bin 0 -> 7601 bytes 0.4/_static/images/logo_jupyterhub.svg | 1 + 0.4/_static/js/index.1c5a1a01449ed65a7b51.js | 32 + 0.4/_static/language_data.js | 199 + 0.4/_static/minus.png | Bin 0 -> 90 bytes 0.4/_static/nbsphinx-broken-thumbnail.svg | 9 + 0.4/_static/nbsphinx-code-cells.css | 259 + 0.4/_static/nbsphinx-gallery.css | 31 + 0.4/_static/nbsphinx-no-thumbnail.svg | 9 + 0.4/_static/plus.png | Bin 0 -> 90 bytes 0.4/_static/pygments.css | 83 + 0.4/_static/searchtools.js | 574 ++ ...-theme.12a9622fbb08dcb3a2a40b2c02b83a57.js | 18 + ...theme.acff12b8f9c144ce68a297486a2fa670.css | 5 + 0.4/_static/sphinx-book-theme.css | 1 + 0.4/_static/sphinx_highlight.js | 154 + .../vendor/fontawesome/5.13.0/LICENSE.txt | 34 + .../vendor/fontawesome/5.13.0/css/all.min.css | 5 + .../5.13.0/webfonts/fa-brands-400.eot | Bin 0 -> 133034 bytes .../5.13.0/webfonts/fa-brands-400.svg | 3570 ++++++++++++ .../5.13.0/webfonts/fa-brands-400.ttf | Bin 0 -> 132728 bytes .../5.13.0/webfonts/fa-brands-400.woff | Bin 0 -> 89824 bytes .../5.13.0/webfonts/fa-brands-400.woff2 | Bin 0 -> 76612 bytes .../5.13.0/webfonts/fa-regular-400.eot | Bin 0 -> 34390 bytes .../5.13.0/webfonts/fa-regular-400.svg | 803 +++ .../5.13.0/webfonts/fa-regular-400.ttf | Bin 0 -> 34092 bytes .../5.13.0/webfonts/fa-regular-400.woff | Bin 0 -> 16800 bytes .../5.13.0/webfonts/fa-regular-400.woff2 | Bin 0 -> 13584 bytes .../5.13.0/webfonts/fa-solid-900.eot | Bin 0 -> 202902 bytes .../5.13.0/webfonts/fa-solid-900.svg | 4938 +++++++++++++++++ .../5.13.0/webfonts/fa-solid-900.ttf | Bin 0 -> 202616 bytes .../5.13.0/webfonts/fa-solid-900.woff | Bin 0 -> 103300 bytes .../5.13.0/webfonts/fa-solid-900.woff2 | Bin 0 -> 79444 bytes 0.4/_static/webpack-macros.html | 25 + 0.4/api/collectors/index.html | 771 +++ 0.4/api/components/index.html | 728 +++ 0.4/api/helpers/index.html | 473 ++ 0.4/api/problems/index.html | 783 +++ 0.4/api/solvers/index.html | 736 +++ 0.4/genindex/index.html | 872 +++ 0.4/guide/collectors.ipynb | 288 + 0.4/guide/collectors/index.html | 595 ++ 0.4/guide/features.ipynb | 334 ++ 0.4/guide/features/index.html | 538 ++ 0.4/guide/primal.ipynb | 291 + 0.4/guide/primal/index.html | 541 ++ 0.4/guide/problems.ipynb | 1607 ++++++ 0.4/guide/problems/index.html | 1647 ++++++ 0.4/guide/solvers.ipynb | 251 + 0.4/guide/solvers/index.html | 494 ++ 0.4/index.html | 386 ++ 0.4/objects.inv | Bin 0 -> 4746 bytes 0.4/py-modindex/index.html | 378 ++ 0.4/search/index.html | 272 + 0.4/searchindex.js | 1 + 0.4/tutorials/cuts-gurobipy.ipynb | 541 ++ 0.4/tutorials/cuts-gurobipy/index.html | 714 +++ 0.4/tutorials/getting-started-gurobipy.ipynb | 837 +++ .../getting-started-gurobipy/index.html | 888 +++ 0.4/tutorials/getting-started-jump.ipynb | 680 +++ 0.4/tutorials/getting-started-jump/index.html | 755 +++ 0.4/tutorials/getting-started-pyomo.ipynb | 858 +++ .../getting-started-pyomo/index.html | 909 +++ index.html | 2 +- 90 files changed, 36221 insertions(+), 1 deletion(-) create mode 100644 0.4/.buildinfo create mode 100644 0.4/_sources/api/collectors.rst.txt create mode 100644 0.4/_sources/api/components.rst.txt create mode 100644 0.4/_sources/api/helpers.rst.txt create mode 100644 0.4/_sources/api/problems.rst.txt create mode 100644 0.4/_sources/api/solvers.rst.txt create mode 100644 0.4/_sources/guide/collectors.ipynb.txt create mode 100644 0.4/_sources/guide/features.ipynb.txt create mode 100644 0.4/_sources/guide/primal.ipynb.txt create mode 100644 0.4/_sources/guide/problems.ipynb.txt create mode 100644 0.4/_sources/guide/solvers.ipynb.txt create mode 100644 0.4/_sources/index.rst.txt create mode 100644 0.4/_sources/tutorials/cuts-gurobipy.ipynb.txt create mode 100644 0.4/_sources/tutorials/getting-started-gurobipy.ipynb.txt create mode 100644 0.4/_sources/tutorials/getting-started-jump.ipynb.txt create mode 100644 0.4/_sources/tutorials/getting-started-pyomo.ipynb.txt create mode 100644 0.4/_static/__init__.py create mode 100644 0.4/_static/__pycache__/__init__.cpython-311.pyc create mode 100644 0.4/_static/basic.css create mode 100644 0.4/_static/css/index.c5995385ac14fb8791e8eb36b4908be2.css create mode 100644 0.4/_static/css/theme.css create mode 100644 0.4/_static/custom.css create mode 100644 0.4/_static/doctools.js create mode 100644 0.4/_static/documentation_options.js create mode 100644 0.4/_static/file.png create mode 100644 0.4/_static/images/logo_binder.svg create mode 100644 0.4/_static/images/logo_colab.png create mode 100644 0.4/_static/images/logo_jupyterhub.svg create mode 100644 0.4/_static/js/index.1c5a1a01449ed65a7b51.js create mode 100644 0.4/_static/language_data.js create mode 100644 0.4/_static/minus.png create mode 100644 0.4/_static/nbsphinx-broken-thumbnail.svg create mode 100644 0.4/_static/nbsphinx-code-cells.css create mode 100644 0.4/_static/nbsphinx-gallery.css create mode 100644 0.4/_static/nbsphinx-no-thumbnail.svg create mode 100644 0.4/_static/plus.png create mode 100644 0.4/_static/pygments.css create mode 100644 0.4/_static/searchtools.js create mode 100644 0.4/_static/sphinx-book-theme.12a9622fbb08dcb3a2a40b2c02b83a57.js create mode 100644 0.4/_static/sphinx-book-theme.acff12b8f9c144ce68a297486a2fa670.css create mode 100644 0.4/_static/sphinx-book-theme.css create mode 100644 0.4/_static/sphinx_highlight.js create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/LICENSE.txt create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/css/all.min.css create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.eot create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.svg create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.ttf create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff2 create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.eot create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.svg create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.ttf create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff2 create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.eot create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.svg create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.ttf create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff create mode 100644 0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff2 create mode 100644 0.4/_static/webpack-macros.html create mode 100644 0.4/api/collectors/index.html create mode 100644 0.4/api/components/index.html create mode 100644 0.4/api/helpers/index.html create mode 100644 0.4/api/problems/index.html create mode 100644 0.4/api/solvers/index.html create mode 100644 0.4/genindex/index.html create mode 100644 0.4/guide/collectors.ipynb create mode 100644 0.4/guide/collectors/index.html create mode 100644 0.4/guide/features.ipynb create mode 100644 0.4/guide/features/index.html create mode 100644 0.4/guide/primal.ipynb create mode 100644 0.4/guide/primal/index.html create mode 100644 0.4/guide/problems.ipynb create mode 100644 0.4/guide/problems/index.html create mode 100644 0.4/guide/solvers.ipynb create mode 100644 0.4/guide/solvers/index.html create mode 100644 0.4/index.html create mode 100644 0.4/objects.inv create mode 100644 0.4/py-modindex/index.html create mode 100644 0.4/search/index.html create mode 100644 0.4/searchindex.js create mode 100644 0.4/tutorials/cuts-gurobipy.ipynb create mode 100644 0.4/tutorials/cuts-gurobipy/index.html create mode 100644 0.4/tutorials/getting-started-gurobipy.ipynb create mode 100644 0.4/tutorials/getting-started-gurobipy/index.html create mode 100644 0.4/tutorials/getting-started-jump.ipynb create mode 100644 0.4/tutorials/getting-started-jump/index.html create mode 100644 0.4/tutorials/getting-started-pyomo.ipynb create mode 100644 0.4/tutorials/getting-started-pyomo/index.html diff --git a/0.4/.buildinfo b/0.4/.buildinfo new file mode 100644 index 0000000..d064bf8 --- /dev/null +++ b/0.4/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 5d2632d2fbe792265ca773e6a51b93f0 +tags: d77d1c0d9ca2f4c8421862c7c5a0d620 diff --git a/0.4/_sources/api/collectors.rst.txt b/0.4/_sources/api/collectors.rst.txt new file mode 100644 index 0000000..ca2072f --- /dev/null +++ b/0.4/_sources/api/collectors.rst.txt @@ -0,0 +1,42 @@ +Collectors & Extractors +======================= + +miplearn.classifiers.minprob +---------------------------- + +.. automodule:: miplearn.classifiers.minprob + :members: + :undoc-members: + :show-inheritance: + +miplearn.classifiers.singleclass +-------------------------------- + +.. automodule:: miplearn.classifiers.singleclass + :members: + :undoc-members: + :show-inheritance: + +miplearn.collectors.basic +------------------------- + +.. automodule:: miplearn.collectors.basic + :members: + :undoc-members: + :show-inheritance: + +miplearn.extractors.fields +-------------------------- + +.. automodule:: miplearn.extractors.fields + :members: + :undoc-members: + :show-inheritance: + +miplearn.extractors.AlvLouWeh2017 +--------------------------------- + +.. automodule:: miplearn.extractors.AlvLouWeh2017 + :members: + :undoc-members: + :show-inheritance: diff --git a/0.4/_sources/api/components.rst.txt b/0.4/_sources/api/components.rst.txt new file mode 100644 index 0000000..64b6c5b --- /dev/null +++ b/0.4/_sources/api/components.rst.txt @@ -0,0 +1,44 @@ +Components +========== + +miplearn.components.primal.actions +---------------------------------- + +.. automodule:: miplearn.components.primal.actions + :members: + :undoc-members: + :show-inheritance: + +miplearn.components.primal.expert +---------------------------------- + +.. automodule:: miplearn.components.primal.expert + :members: + :undoc-members: + :show-inheritance: + +miplearn.components.primal.indep +---------------------------------- + +.. automodule:: miplearn.components.primal.indep + :members: + :undoc-members: + :show-inheritance: + +miplearn.components.primal.joint +---------------------------------- + +.. automodule:: miplearn.components.primal.joint + :members: + :undoc-members: + :show-inheritance: + +miplearn.components.primal.mem +---------------------------------- + +.. automodule:: miplearn.components.primal.mem + :members: + :undoc-members: + :show-inheritance: + + \ No newline at end of file diff --git a/0.4/_sources/api/helpers.rst.txt b/0.4/_sources/api/helpers.rst.txt new file mode 100644 index 0000000..d83450f --- /dev/null +++ b/0.4/_sources/api/helpers.rst.txt @@ -0,0 +1,18 @@ +Helpers +======= + +miplearn.io +----------- + +.. automodule:: miplearn.io + :members: + :undoc-members: + :show-inheritance: + +miplearn.h5 +----------- + +.. automodule:: miplearn.h5 + :members: + :undoc-members: + :show-inheritance: diff --git a/0.4/_sources/api/problems.rst.txt b/0.4/_sources/api/problems.rst.txt new file mode 100644 index 0000000..a60a968 --- /dev/null +++ b/0.4/_sources/api/problems.rst.txt @@ -0,0 +1,57 @@ +Benchmark Problems +================== + +miplearn.problems.binpack +------------------------- + +.. automodule:: miplearn.problems.binpack + :members: + +miplearn.problems.multiknapsack +------------------------------- + +.. automodule:: miplearn.problems.multiknapsack + :members: + +miplearn.problems.pmedian +------------------------- + +.. automodule:: miplearn.problems.pmedian + :members: + +miplearn.problems.setcover +-------------------------- + +.. automodule:: miplearn.problems.setcover + :members: + +miplearn.problems.setpack +------------------------- + +.. automodule:: miplearn.problems.setpack + :members: + +miplearn.problems.stab +---------------------- + +.. automodule:: miplearn.problems.stab + :members: + +miplearn.problems.tsp +--------------------- + +.. automodule:: miplearn.problems.tsp + :members: + +miplearn.problems.uc +-------------------- + +.. automodule:: miplearn.problems.uc + :members: + +miplearn.problems.vertexcover +----------------------------- + +.. automodule:: miplearn.problems.vertexcover + :members: + diff --git a/0.4/_sources/api/solvers.rst.txt b/0.4/_sources/api/solvers.rst.txt new file mode 100644 index 0000000..2337d92 --- /dev/null +++ b/0.4/_sources/api/solvers.rst.txt @@ -0,0 +1,26 @@ +Solvers +======= + +miplearn.solvers.abstract +------------------------- + +.. automodule:: miplearn.solvers.abstract + :members: + :undoc-members: + :show-inheritance: + +miplearn.solvers.gurobi +------------------------- + +.. automodule:: miplearn.solvers.gurobi + :members: + :undoc-members: + :show-inheritance: + +miplearn.solvers.learning +------------------------- + +.. automodule:: miplearn.solvers.learning + :members: + :undoc-members: + :show-inheritance: diff --git a/0.4/_sources/guide/collectors.ipynb.txt b/0.4/_sources/guide/collectors.ipynb.txt new file mode 100644 index 0000000..443802e --- /dev/null +++ b/0.4/_sources/guide/collectors.ipynb.txt @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "505cea0b-5f5d-478a-9107-42bb5515937d", + "metadata": {}, + "source": [ + "# Training Data Collectors\n", + "The first step in solving mixed-integer optimization problems with the assistance of supervised machine learning methods is solving a large set of training instances and collecting the raw training data. In this section, we describe the various training data collectors included in MIPLearn. Additionally, the framework follows the convention of storing all training data in files with a specific data format (namely, HDF5). In this section, we briefly describe this format and the rationale for choosing it.\n", + "\n", + "## Overview\n", + "\n", + "In MIPLearn, a **collector** is a class that solves or analyzes the problem and collects raw data which may be later useful for machine learning methods. Collectors, by convention, take as input: (i) a list of problem data filenames, in gzipped pickle format, ending with `.pkl.gz`; (ii) a function that builds the optimization model, such as `build_tsp_model`. After processing is done, collectors store the training data in a HDF5 file located alongside with the problem data. For example, if the problem data is stored in file `problem.pkl.gz`, then the collector writes to `problem.h5`. Collectors are, in general, very time consuming, as they may need to solve the problem to optimality, potentially multiple times.\n", + "\n", + "## HDF5 Format\n", + "\n", + "MIPLearn stores all training data in [HDF5](HDF5) (Hierarchical Data Format, Version 5) files. The HDF format was originally developed by the [National Center for Supercomputing Applications][NCSA] (NCSA) for storing and organizing large amounts of data, and supports a variety of data types, including integers, floating-point numbers, strings, and arrays. Compared to other formats, such as CSV, JSON or SQLite, the HDF5 format provides several advantages for MIPLearn, including:\n", + "\n", + "- *Storage of multiple scalars, vectors and matrices in a single file* --- This allows MIPLearn to store all training data related to a given problem instance in a single file, which makes training data easier to store, organize and transfer.\n", + "- *High-performance partial I/O* --- Partial I/O allows MIPLearn to read a single element from the training data (e.g. value of the optimal solution) without loading the entire file to memory or reading it from beginning to end, which dramatically improves performance and reduces memory requirements. This is especially important when processing a large number of training data files.\n", + "- *On-the-fly compression* --- HDF5 files can be transparently compressed, using the gzip method, which reduces storage requirements and accelerates network transfers.\n", + "- *Stable, portable and well-supported data format* --- Training data files are typically expensive to generate. Having a stable and well supported data format ensures that these files remain usable in the future, potentially even by other non-Python MIP/ML frameworks.\n", + "\n", + "MIPLearn currently uses HDF5 as simple key-value storage for numerical data; more advanced features of the format, such as metadata, are not currently used. Although files generated by MIPLearn can be read with any HDF5 library, such as [h5py][h5py], some convenience functions are provided to make the access more simple and less error-prone. Specifically, the class [H5File][H5File], which is built on top of h5py, provides the methods [put_scalar][put_scalar], [put_array][put_array], [put_sparse][put_sparse], [put_bytes][put_bytes] to store, respectively, scalar values, dense multi-dimensional arrays, sparse multi-dimensional arrays and arbitrary binary data. The corresponding *get* methods are also provided. Compared to pure h5py methods, these methods automatically perform type-checking and gzip compression. The example below shows their usage.\n", + "\n", + "[HDF5]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format\n", + "[NCSA]: https://en.wikipedia.org/wiki/National_Center_for_Supercomputing_Applications\n", + "[h5py]: https://www.h5py.org/\n", + "[H5File]: ../../api/helpers/#miplearn.h5.H5File\n", + "[put_scalar]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_array]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_sparse]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_bytes]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f906fe9c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-30T22:19:30.826123021Z", + "start_time": "2024-01-30T22:19:30.766066926Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "x1 = 1\n", + "x2 = hello world\n", + "x3 = [1 2 3]\n", + "x4 = [[0.37454012 0.9507143 0.7319939 ]\n", + " [0.5986585 0.15601864 0.15599452]\n", + " [0.05808361 0.8661761 0.601115 ]]\n", + "x5 = (3, 2)\t0.6803075671195984\n", + " (2, 3)\t0.4504992663860321\n", + " (0, 4)\t0.013264961540699005\n", + " (2, 0)\t0.9422017335891724\n", + " (2, 4)\t0.5632882118225098\n", + " (1, 2)\t0.38541650772094727\n", + " (1, 1)\t0.015966251492500305\n", + " (0, 3)\t0.2308938205242157\n", + " (4, 4)\t0.24102546274662018\n", + " (3, 1)\t0.6832635402679443\n", + " (1, 3)\t0.6099966764450073\n", + " (3, 0)\t0.83319491147995\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import scipy.sparse\n", + "\n", + "from miplearn.h5 import H5File\n", + "\n", + "# Set random seed to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Create a new empty HDF5 file\n", + "with H5File(\"test.h5\", \"w\") as h5:\n", + " # Store a scalar\n", + " h5.put_scalar(\"x1\", 1)\n", + " h5.put_scalar(\"x2\", \"hello world\")\n", + "\n", + " # Store a dense array and a dense matrix\n", + " h5.put_array(\"x3\", np.array([1, 2, 3]))\n", + " h5.put_array(\"x4\", np.random.rand(3, 3))\n", + "\n", + " # Store a sparse matrix\n", + " h5.put_sparse(\"x5\", scipy.sparse.random(5, 5, 0.5))\n", + "\n", + "# Re-open the file we just created and print\n", + "# previously-stored data\n", + "with H5File(\"test.h5\", \"r\") as h5:\n", + " print(\"x1 =\", h5.get_scalar(\"x1\"))\n", + " print(\"x2 =\", h5.get_scalar(\"x2\"))\n", + " print(\"x3 =\", h5.get_array(\"x3\"))\n", + " print(\"x4 =\", h5.get_array(\"x4\"))\n", + " print(\"x5 =\", h5.get_sparse(\"x5\"))" + ] + }, + { + "cell_type": "markdown", + "id": "50441907", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "d0000c8d", + "metadata": {}, + "source": [ + "## Basic collector\n", + "\n", + "[BasicCollector][BasicCollector] is the most fundamental collector, and performs the following steps:\n", + "\n", + "1. Extracts all model data, such as objective function and constraint right-hand sides into numpy arrays, which can later be easily and efficiently accessed without rebuilding the model or invoking the solver;\n", + "2. Solves the linear relaxation of the problem and stores its optimal solution, basis status and sensitivity information, among other information;\n", + "3. Solves the original mixed-integer optimization problem to optimality and stores its optimal solution, along with solve statistics, such as number of explored nodes and wallclock time.\n", + "\n", + "Data extracted in Phases 1, 2 and 3 above are prefixed, respectively as `static_`, `lp_` and `mip_`. The entire set of fields is shown in the table below.\n", + "\n", + "[BasicCollector]: ../../api/collectors/#miplearn.collectors.basic.BasicCollector\n" + ] + }, + { + "cell_type": "markdown", + "id": "6529f667", + "metadata": {}, + "source": [ + "### Data fields\n", + "\n", + "| Field | Type | Description |\n", + "|-----------------------------------|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------|\n", + "| `static_constr_lhs` | `(nconstrs, nvars)` | Constraint left-hand sides, in sparse matrix format |\n", + "| `static_constr_names` | `(nconstrs,)` | Constraint names |\n", + "| `static_constr_rhs` | `(nconstrs,)` | Constraint right-hand sides |\n", + "| `static_constr_sense` | `(nconstrs,)` | Constraint senses (`\"<\"`, `\">\"` or `\"=\"`) |\n", + "| `static_obj_offset` | `float` | Constant value added to the objective function |\n", + "| `static_sense` | `str` | `\"min\"` if minimization problem or `\"max\"` otherwise |\n", + "| `static_var_lower_bounds` | `(nvars,)` | Variable lower bounds |\n", + "| `static_var_names` | `(nvars,)` | Variable names |\n", + "| `static_var_obj_coeffs` | `(nvars,)` | Objective coefficients |\n", + "| `static_var_types` | `(nvars,)` | Types of the decision variables (`\"C\"`, `\"B\"` and `\"I\"` for continuous, binary and integer, respectively) |\n", + "| `static_var_upper_bounds` | `(nvars,)` | Variable upper bounds |\n", + "| `lp_constr_basis_status` | `(nconstr,)` | Constraint basis status (`0` for basic, `-1` for non-basic) |\n", + "| `lp_constr_dual_values` | `(nconstr,)` | Constraint dual value (or shadow price) |\n", + "| `lp_constr_sa_rhs_{up,down}` | `(nconstr,)` | Sensitivity information for the constraint RHS |\n", + "| `lp_constr_slacks` | `(nconstr,)` | Constraint slack in the solution to the LP relaxation |\n", + "| `lp_obj_value` | `float` | Optimal value of the LP relaxation |\n", + "| `lp_var_basis_status` | `(nvars,)` | Variable basis status (`0`, `-1`, `-2` or `-3` for basic, non-basic at lower bound, non-basic at upper bound, and superbasic, respectively) |\n", + "| `lp_var_reduced_costs` | `(nvars,)` | Variable reduced costs |\n", + "| `lp_var_sa_{obj,ub,lb}_{up,down}` | `(nvars,)` | Sensitivity information for the variable objective coefficient, lower and upper bound. |\n", + "| `lp_var_values` | `(nvars,)` | Optimal solution to the LP relaxation |\n", + "| `lp_wallclock_time` | `float` | Time taken to solve the LP relaxation (in seconds) |\n", + "| `mip_constr_slacks` | `(nconstrs,)` | Constraint slacks in the best MIP solution |\n", + "| `mip_gap` | `float` | Relative MIP optimality gap |\n", + "| `mip_node_count` | `float` | Number of explored branch-and-bound nodes |\n", + "| `mip_obj_bound` | `float` | Dual bound |\n", + "| `mip_obj_value` | `float` | Value of the best MIP solution |\n", + "| `mip_var_values` | `(nvars,)` | Best MIP solution |\n", + "| `mip_wallclock_time` | `float` | Time taken to solve the MIP (in seconds) |" + ] + }, + { + "cell_type": "markdown", + "id": "f2894594", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "The example below shows how to generate a few random instances of the traveling salesman problem, store its problem data, run the collector and print some of the training data to screen." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ac6f8c6f", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-30T22:19:30.826707866Z", + "start_time": "2024-01-30T22:19:30.825940503Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "lp_obj_value = 2909.0\n", + "mip_obj_value = 2921.0\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from glob import glob\n", + "\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.h5 import H5File\n", + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "# Set random seed to make example reproducible.\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a few instances of the traveling salesman problem.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(10)\n", + "\n", + "# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...\n", + "write_pkl_gz(data, \"data/tsp\")\n", + "\n", + "# Solve all instances and collect basic solution information.\n", + "# Process at most four instances in parallel.\n", + "bc = BasicCollector()\n", + "bc.collect(glob(\"data/tsp/*.pkl.gz\"), build_tsp_model_gurobipy, n_jobs=4)\n", + "\n", + "# Read and print some training data for the first instance.\n", + "with H5File(\"data/tsp/00000.h5\", \"r\") as h5:\n", + " print(\"lp_obj_value = \", h5.get_scalar(\"lp_obj_value\"))\n", + " print(\"mip_obj_value = \", h5.get_scalar(\"mip_obj_value\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78f0b07a", + "metadata": { + "ExecuteTime": { + "start_time": "2024-01-30T22:19:30.826179789Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/guide/features.ipynb.txt b/0.4/_sources/guide/features.ipynb.txt new file mode 100644 index 0000000..495e8ea --- /dev/null +++ b/0.4/_sources/guide/features.ipynb.txt @@ -0,0 +1,334 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cdc6ebe9-d1d4-4de1-9b5a-4fc8ef57b11b", + "metadata": {}, + "source": [ + "# Feature Extractors\n", + "\n", + "In the previous page, we introduced *training data collectors*, which solve the optimization problem and collect raw training data, such as the optimal solution. In this page, we introduce **feature extractors**, which take the raw training data, stored in HDF5 files, and extract relevant information in order to train a machine learning model." + ] + }, + { + "cell_type": "markdown", + "id": "b4026de5", + "metadata": {}, + "source": [ + "\n", + "## Overview\n", + "\n", + "Feature extraction is an important step of the process of building a machine learning model because it helps to reduce the complexity of the data and convert it into a format that is more easily processed. Previous research has proposed converting absolute variable coefficients, for example, into relative values which are invariant to various transformations, such as problem scaling, making them more amenable to learning. Various other transformations have also been described.\n", + "\n", + "In the framework, we treat data collection and feature extraction as two separate steps to accelerate the model development cycle. Specifically, collectors are typically time-consuming, as they often need to solve the problem to optimality, and therefore focus on collecting and storing all data that may or may not be relevant, in its raw format. Feature extractors, on the other hand, focus entirely on filtering the data and improving its representation, and are therefore much faster to run. Experimenting with new data representations, therefore, can be done without resolving the instances.\n", + "\n", + "In MIPLearn, extractors implement the abstract class [FeatureExtractor][FeatureExtractor], which has methods that take as input an [H5File][H5File] and produce either: (i) instance features, which describe the entire instances; (ii) variable features, which describe a particular decision variables; or (iii) constraint features, which describe a particular constraint. The extractor is free to implement only a subset of these methods, if it is known that it will not be used with a machine learning component that requires the other types of features.\n", + "\n", + "[FeatureExtractor]: ../../api/collectors/#miplearn.features.fields.FeaturesExtractor\n", + "[H5File]: ../../api/helpers/#miplearn.h5.H5File" + ] + }, + { + "cell_type": "markdown", + "id": "b2d9736c", + "metadata": {}, + "source": [ + "\n", + "## H5FieldsExtractor\n", + "\n", + "[H5FieldsExtractor][H5FieldsExtractor], the most simple extractor in MIPLearn, simple extracts data that is already available in the HDF5 file, assembles it into a matrix and returns it as-is. The fields used to build instance, variable and constraint features are user-specified. The class also performs checks to ensure that the shapes of the returned matrices make sense." + ] + }, + { + "cell_type": "markdown", + "id": "e8184dff", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "The example below demonstrates the usage of H5FieldsExtractor in a randomly generated instance of the multi-dimensional knapsack problem." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ed9a18c8", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "instance features (11,) \n", + " [-1531.24308771 -350. -692. -454.\n", + " -709. -605. -543. -321.\n", + " -674. -571. -341. ]\n", + "variable features (10, 4) \n", + " [[-1.53124309e+03 -3.50000000e+02 0.00000000e+00 9.43468018e+01]\n", + " [-1.53124309e+03 -6.92000000e+02 2.51703322e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -4.54000000e+02 0.00000000e+00 8.25504150e+01]\n", + " [-1.53124309e+03 -7.09000000e+02 1.11373022e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -6.05000000e+02 1.00000000e+00 -1.26055283e+02]\n", + " [-1.53124309e+03 -5.43000000e+02 0.00000000e+00 1.68693771e+02]\n", + " [-1.53124309e+03 -3.21000000e+02 1.07488781e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -6.74000000e+02 8.82293701e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -5.71000000e+02 0.00000000e+00 1.41129074e+02]\n", + " [-1.53124309e+03 -3.41000000e+02 1.28830120e-01 0.00000000e+00]]\n", + "constraint features (5, 3) \n", + " [[ 1.3100000e+03 -1.5978307e-01 0.0000000e+00]\n", + " [ 9.8800000e+02 -3.2881632e-01 0.0000000e+00]\n", + " [ 1.0040000e+03 -4.0601316e-01 0.0000000e+00]\n", + " [ 1.2690000e+03 -1.3659772e-01 0.0000000e+00]\n", + " [ 1.0070000e+03 -2.8800571e-01 0.0000000e+00]]\n" + ] + } + ], + "source": [ + "from glob import glob\n", + "from shutil import rmtree\n", + "\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from miplearn.h5 import H5File\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.problems.multiknapsack import (\n", + " MultiKnapsackGenerator,\n", + " build_multiknapsack_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate some random multiknapsack instances\n", + "rmtree(\"data/multiknapsack/\", ignore_errors=True)\n", + "write_pkl_gz(\n", + " MultiKnapsackGenerator(\n", + " n=randint(low=10, high=11),\n", + " m=randint(low=5, high=6),\n", + " w=uniform(loc=0, scale=1000),\n", + " K=uniform(loc=100, scale=0),\n", + " u=uniform(loc=1, scale=0),\n", + " alpha=uniform(loc=0.25, scale=0),\n", + " w_jitter=uniform(loc=0.95, scale=0.1),\n", + " p_jitter=uniform(loc=0.75, scale=0.5),\n", + " fix_w=True,\n", + " ).generate(10),\n", + " \"data/multiknapsack\",\n", + ")\n", + "\n", + "# Run the basic collector\n", + "BasicCollector().collect(\n", + " glob(\"data/multiknapsack/*\"),\n", + " build_multiknapsack_model_gurobipy,\n", + " n_jobs=4,\n", + ")\n", + "\n", + "ext = H5FieldsExtractor(\n", + " # Use as instance features the value of the LP relaxation and the\n", + " # vector of objective coefficients.\n", + " instance_fields=[\n", + " \"lp_obj_value\",\n", + " \"static_var_obj_coeffs\",\n", + " ],\n", + " # For each variable, use as features the optimal value of the LP\n", + " # relaxation, the variable objective coefficient, the variable's\n", + " # value its reduced cost.\n", + " var_fields=[\n", + " \"lp_obj_value\",\n", + " \"static_var_obj_coeffs\",\n", + " \"lp_var_values\",\n", + " \"lp_var_reduced_costs\",\n", + " ],\n", + " # For each constraint, use as features the RHS, dual value and slack.\n", + " constr_fields=[\n", + " \"static_constr_rhs\",\n", + " \"lp_constr_dual_values\",\n", + " \"lp_constr_slacks\",\n", + " ],\n", + ")\n", + "\n", + "with H5File(\"data/multiknapsack/00000.h5\") as h5:\n", + " # Extract and print instance features\n", + " x1 = ext.get_instance_features(h5)\n", + " print(\"instance features\", x1.shape, \"\\n\", x1)\n", + "\n", + " # Extract and print variable features\n", + " x2 = ext.get_var_features(h5)\n", + " print(\"variable features\", x2.shape, \"\\n\", x2)\n", + "\n", + " # Extract and print constraint features\n", + " x3 = ext.get_constr_features(h5)\n", + " print(\"constraint features\", x3.shape, \"\\n\", x3)" + ] + }, + { + "cell_type": "markdown", + "id": "2da2e74e", + "metadata": {}, + "source": [ + "\n", + "[H5FieldsExtractor]: ../../api/collectors/#miplearn.features.fields.H5FieldsExtractor" + ] + }, + { + "cell_type": "markdown", + "id": "d879c0d3", + "metadata": {}, + "source": [ + "
\n", + "Warning\n", + "\n", + "You should ensure that the number of features remains the same for all relevant HDF5 files. In the previous example, to illustrate this issue, we used variable objective coefficients as instance features. While this is allowed, note that this requires all problem instances to have the same number of variables; otherwise the number of features would vary from instance to instance and MIPLearn would be unable to concatenate the matrices.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "cd0ba071", + "metadata": {}, + "source": [ + "## AlvLouWeh2017Extractor\n", + "\n", + "Alvarez, Louveaux and Wehenkel (2017) proposed a set features to describe a particular decision variable in a given node of the branch-and-bound tree, and applied it to the problem of mimicking strong branching decisions. The class [AlvLouWeh2017Extractor][] implements a subset of these features (40 out of 64), which are available outside of the branch-and-bound tree. Some features are derived from the static defintion of the problem (i.e. from objective function and constraint data), while some features are derived from the solution to the LP relaxation. The features have been designed to be: (i) independent of the size of the problem; (ii) invariant with respect to irrelevant problem transformations, such as row and column permutation; and (iii) independent of the scale of the problem. We refer to the paper for a more complete description.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a1bc38fe", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "x1 (10, 40) \n", + " [[-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 6.00e-01 1.00e+00 1.75e+01 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 1.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 7.00e-01 1.00e+00 5.10e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 3.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 9.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 5.00e-01 1.00e+00 1.30e+01 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 2.00e-01 1.00e+00 9.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 3.40e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 1.00e-01 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 7.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 6.00e-01 1.00e+00 3.80e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 8.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 7.00e-01 1.00e+00 3.30e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 3.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 1.00e+00 1.00e+00 5.70e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 6.80e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 4.00e-01 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 1.40e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 1.00e-01 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 5.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 5.00e-01 1.00e+00 7.60e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]]\n" + ] + } + ], + "source": [ + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.h5 import H5File\n", + "\n", + "# Build the extractor\n", + "ext = AlvLouWeh2017Extractor()\n", + "\n", + "# Open previously-created multiknapsack training data\n", + "with H5File(\"data/multiknapsack/00000.h5\") as h5:\n", + " # Extract and print variable features\n", + " x1 = ext.get_var_features(h5)\n", + " print(\"x1\", x1.shape, \"\\n\", x1.round(1))" + ] + }, + { + "cell_type": "markdown", + "id": "286c9927", + "metadata": {}, + "source": [ + "
\n", + "References\n", + "\n", + "* **Alvarez, Alejandro Marcos.** *Computational and theoretical synergies between linear optimization and supervised machine learning.* (2016). University of Liège.\n", + "* **Alvarez, Alejandro Marcos, Quentin Louveaux, and Louis Wehenkel.** *A machine learning-based approximation of strong branching.* INFORMS Journal on Computing 29.1 (2017): 185-195.\n", + "\n", + "
" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/guide/primal.ipynb.txt b/0.4/_sources/guide/primal.ipynb.txt new file mode 100644 index 0000000..26464ce --- /dev/null +++ b/0.4/_sources/guide/primal.ipynb.txt @@ -0,0 +1,291 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "880cf4c7-d3c4-4b92-85c7-04a32264cdae", + "metadata": {}, + "source": [ + "# Primal Components\n", + "\n", + "In MIPLearn, a **primal component** is class that uses machine learning to predict a (potentially partial) assignment of values to the decision variables of the problem. Predicting high-quality primal solutions may be beneficial, as they allow the MIP solver to prune potentially large portions of the search space. Alternatively, if proof of optimality is not required, the MIP solver can be used to complete the partial solution generated by the machine learning model and and double-check its feasibility. MIPLearn allows both of these usage patterns.\n", + "\n", + "In this page, we describe the four primal components currently included in MIPLearn, which employ machine learning in different ways. Each component is highly configurable, and accepts an user-provided machine learning model, which it uses for all predictions. Each component can also be configured to provide the solution to the solver in multiple ways, depending on whether proof of optimality is required.\n", + "\n", + "## Primal component actions\n", + "\n", + "Before presenting the primal components themselves, we briefly discuss the three ways a solution may be provided to the solver. Each approach has benefits and limitations, which we also discuss in this section. All primal components can be configured to use any of the following approaches.\n", + "\n", + "The first approach is to provide the solution to the solver as a **warm start**. This is implemented by the class [SetWarmStart](SetWarmStart). The main advantage is that this method maintains all optimality and feasibility guarantees of the MIP solver, while still providing significant performance benefits for various classes of problems. If the machine learning model is able to predict multiple solutions, it is also possible to set multiple warm starts. In this case, the solver evaluates each warm start, discards the infeasible ones, then proceeds with the one that has the best objective value. The main disadvantage of this approach, compared to the next two, is that it provides relatively modest speedups for most problem classes, and no speedup at all for many others, even when the machine learning predictions are 100% accurate.\n", + "\n", + "[SetWarmStart]: ../../api/components/#miplearn.components.primal.actions.SetWarmStart\n", + "\n", + "The second approach is to **fix the decision variables** to their predicted values, then solve a restricted optimization problem on the remaining variables. This approach is implemented by the class `FixVariables`. The main advantage is its potential speedup: if machine learning can accurately predict values for a significant portion of the decision variables, then the MIP solver can typically complete the solution in a small fraction of the time it would take to find the same solution from scratch. The main disadvantage of this approach is that it loses optimality guarantees; that is, the complete solution found by the MIP solver may no longer be globally optimal. Also, if the machine learning predictions are not sufficiently accurate, there might not even be a feasible assignment for the variables that were left free.\n", + "\n", + "Finally, the third approach, which tries to strike a balance between the two previous ones, is to **enforce proximity** to a given solution. This strategy is implemented by the class `EnforceProximity`. More precisely, given values $\\bar{x}_1,\\ldots,\\bar{x}_n$ for a subset of binary decision variables $x_1,\\ldots,x_n$, this approach adds the constraint\n", + "\n", + "$$\n", + "\\sum_{i : \\bar{x}_i=0} x_i + \\sum_{i : \\bar{x}_i=1} \\left(1 - x_i\\right) \\leq k,\n", + "$$\n", + "to the problem, where $k$ is a user-defined parameter, which indicates how many of the predicted variables are allowed to deviate from the machine learning suggestion. The main advantage of this approach, compared to fixing variables, is its tolerance to lower-quality machine learning predictions. Its main disadvantage is that it typically leads to smaller speedups, especially for larger values of $k$. This approach also loses optimality guarantees.\n", + "\n", + "## Memorizing primal component\n", + "\n", + "A simple machine learning strategy for the prediction of primal solutions is to memorize all distinct solutions seen during training, then try to predict, during inference time, which of those memorized solutions are most likely to be feasible and to provide a good objective value for the current instance. The most promising solutions may alternatively be combined into a single partial solution, which is then provided to the MIP solver. Both variations of this strategy are implemented by the `MemorizingPrimalComponent` class. Note that it is only applicable if the problem size, and in fact if the meaning of the decision variables, remains the same across problem instances.\n", + "\n", + "More precisely, let $I_1,\\ldots,I_n$ be the training instances, and let $\\bar{x}^1,\\ldots,\\bar{x}^n$ be their respective optimal solutions. Given a new instance $I_{n+1}$, `MemorizingPrimalComponent` expects a user-provided binary classifier that assigns (through the `predict_proba` method, following scikit-learn's conventions) a score $\\delta_i$ to each solution $\\bar{x}^i$, such that solutions with higher score are more likely to be good solutions for $I_{n+1}$. The features provided to the classifier are the instance features computed by an user-provided extractor. Given these scores, the component then performs one of the following to actions, as decided by the user:\n", + "\n", + "1. Selects the top $k$ solutions with the highest scores and provides them to the solver; this is implemented by `SelectTopSolutions`, and it is typically used with the `SetWarmStart` action.\n", + "\n", + "2. Merges the top $k$ solutions into a single partial solution, then provides it to the solver. This is implemented by `MergeTopSolutions`. More precisely, suppose that the machine learning regressor ordered the solutions in the sequence $\\bar{x}^{i_1},\\ldots,\\bar{x}^{i_n}$, with the most promising solutions appearing first, and with ties being broken arbitrarily. The component starts by keeping only the $k$ most promising solutions $\\bar{x}^{i_1},\\ldots,\\bar{x}^{i_k}$. Then it computes, for each binary decision variable $x_l$, its average assigned value $\\tilde{x}_l$:\n", + "$$\n", + " \\tilde{x}_l = \\frac{1}{k} \\sum_{j=1}^k \\bar{x}^{i_j}_l.\n", + "$$\n", + " Finally, the component constructs a merged solution $y$, defined as:\n", + "$$\n", + " y_j = \\begin{cases}\n", + " 0 & \\text{ if } \\tilde{x}_l \\le \\theta_0 \\\\\n", + " 1 & \\text{ if } \\tilde{x}_l \\ge \\theta_1 \\\\\n", + " \\square & \\text{otherwise,}\n", + " \\end{cases}\n", + "$$\n", + " where $\\theta_0$ and $\\theta_1$ are user-specified parameters, and where $\\square$ indicates that the variable is left undefined. The solution $y$ is then provided by the solver using any of the three approaches defined in the previous section.\n", + "\n", + "The above specification of `MemorizingPrimalComponent` is meant to be as general as possible. Simpler strategies can be implemented by configuring this component in specific ways. For example, a simpler approach employed in the literature is to collect all optimal solutions, then provide the entire list of solutions to the solver as warm starts, without any filtering or post-processing. This strategy can be implemented with `MemorizingPrimalComponent` by using a model that returns a constant value for all solutions (e.g. [scikit-learn's DummyClassifier][DummyClassifier]), then selecting the top $n$ (instead of $k$) solutions. See example below. Another simple approach is taking the solution to the most similar instance, and using it, by itself, as a warm start. This can be implemented by using a model that computes distances between the current instance and the training ones (e.g. [scikit-learn's KNeighborsClassifier][KNeighborsClassifier]), then select the solution to the nearest one. See also example below. More complex strategies, of course, can also be configured.\n", + "\n", + "[DummyClassifier]: https://scikit-learn.org/stable/modules/generated/sklearn.dummy.DummyClassifier.html\n", + "[KNeighborsClassifier]: https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "253adbf4", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.dummy import DummyClassifier\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "\n", + "from miplearn.components.primal.actions import (\n", + " SetWarmStart,\n", + " FixVariables,\n", + " EnforceProximity,\n", + ")\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " SelectTopSolutions,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.dummy import DummyExtractor\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "# Configures a memorizing primal component that collects\n", + "# all distinct solutions seen during training and provides\n", + "# them to the solver without any filtering or post-processing.\n", + "comp1 = MemorizingPrimalComponent(\n", + " clf=DummyClassifier(),\n", + " extractor=DummyExtractor(),\n", + " constructor=SelectTopSolutions(1_000_000),\n", + " action=SetWarmStart(),\n", + ")\n", + "\n", + "# Configures a memorizing primal component that finds the\n", + "# training instance with the closest objective function, then\n", + "# fixes the decision variables to the values they assumed\n", + "# at the optimal solution for that instance.\n", + "comp2 = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=1),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " constructor=SelectTopSolutions(1),\n", + " action=FixVariables(),\n", + ")\n", + "\n", + "# Configures a memorizing primal component that finds the distinct\n", + "# solutions to the 10 most similar training problem instances,\n", + "# selects the 3 solutions that were most often optimal to these\n", + "# training instances, combines them into a single partial solution,\n", + "# then enforces proximity, allowing at most 3 variables to deviate\n", + "# from the machine learning suggestion.\n", + "comp3 = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=10),\n", + " extractor=H5FieldsExtractor(instance_fields=[\"static_var_obj_coeffs\"]),\n", + " constructor=MergeTopSolutions(k=3, thresholds=[0.25, 0.75]),\n", + " action=EnforceProximity(3),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f194a793", + "metadata": {}, + "source": [ + "## Independent vars primal component\n", + "\n", + "Instead of memorizing previously-seen primal solutions, it is also natural to use machine learning models to directly predict the values of the decision variables, constructing a solution from scratch. This approach has the benefit of potentially constructing novel high-quality solutions, never observed in the training data. Two variations of this strategy are supported by MIPLearn: (i) predicting the values of the decision variables independently, using multiple ML models; or (ii) predicting the values jointly, with a single model. We describe the first variation in this section, and the second variation in the next section.\n", + "\n", + "Let $I_1,\\ldots,I_n$ be the training instances, and let $\\bar{x}^1,\\ldots,\\bar{x}^n$ be their respective optimal solutions. For each binary decision variable $x_j$, the component `IndependentVarsPrimalComponent` creates a copy of a user-provided binary classifier and trains it to predict the optimal value of $x_j$, given $\\bar{x}^1_j,\\ldots,\\bar{x}^n_j$ as training labels. The features provided to the model are the variable features computed by an user-provided extractor. During inference time, the component uses these $n$ binary classifiers to construct a solution and provides it to the solver using one of the available actions.\n", + "\n", + "Three issues often arise in practice when using this approach:\n", + "\n", + " 1. For certain binary variables $x_j$, it is frequently the case that its optimal value is either always zero or always one in the training dataset, which poses problems to some standard scikit-learn classifiers, since they do not expect a single class. The wrapper `SingleClassFix` can be used to fix this issue (see example below).\n", + "2. It is also frequently the case that machine learning classifier can only reliably predict the values of some variables with high accuracy, not all of them. In this situation, instead of computing a complete primal solution, it may be more beneficial to construct a partial solution containing values only for the variables for which the ML made a high-confidence prediction. The meta-classifier `MinProbabilityClassifier` can be used for this purpose. It asks the base classifier for the probability of the value being zero or one (using the `predict_proba` method) and erases from the primal solution all values whose probabilities are below a given threshold.\n", + "3. To make multiple copies of the provided ML classifier, MIPLearn uses the standard `sklearn.base.clone` method, which may not be suitable for classifiers from other frameworks. To handle this, it is possible to override the clone function using the `clone_fn` constructor argument.\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3fc0b5d1", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.linear_model import LogisticRegression\n", + "from miplearn.classifiers.minprob import MinProbabilityClassifier\n", + "from miplearn.classifiers.singleclass import SingleClassFix\n", + "from miplearn.components.primal.indep import IndependentVarsPrimalComponent\n", + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures a primal component that independently predicts the value of each\n", + "# binary variable using logistic regression and provides it to the solver as\n", + "# warm start. Erases predictions with probability less than 99%; applies\n", + "# single-class fix; and uses AlvLouWeh2017 features.\n", + "comp = IndependentVarsPrimalComponent(\n", + " base_clf=SingleClassFix(\n", + " MinProbabilityClassifier(\n", + " base_clf=LogisticRegression(),\n", + " thresholds=[0.99, 0.99],\n", + " ),\n", + " ),\n", + " extractor=AlvLouWeh2017Extractor(),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "45107a0c", + "metadata": {}, + "source": [ + "## Joint vars primal component\n", + "In the previous subsection, we used multiple machine learning models to independently predict the values of the binary decision variables. When these values are correlated, an alternative approach is to jointly predict the values of all binary variables using a single machine learning model. This strategy is implemented by `JointVarsPrimalComponent`. Compared to the previous ones, this component is much more straightforwad. It simply extracts instance features, using the user-provided feature extractor, then directly trains the user-provided binary classifier (using the `fit` method), without making any copies. The trained classifier is then used to predict entire solutions (using the `predict` method), which are given to the solver using one of the previously discussed methods. In the example below, we illustrate the usage of this component with a simple feed-forward neural network.\n", + "\n", + "`JointVarsPrimalComponent` can also be used to implement strategies that use multiple machine learning models, but not indepedently. For example, a common strategy in multioutput prediction is building a *classifier chain*. In this approach, the first decision variable is predicted using the instance features alone; but the $n$-th decision variable is predicted using the instance features plus the predicted values of the $n-1$ previous variables. This can be easily implemented using scikit-learn's `ClassifierChain` estimator, as shown in the example below.\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cf9b52dd", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.multioutput import ClassifierChain\n", + "from sklearn.neural_network import MLPClassifier\n", + "from miplearn.components.primal.joint import JointVarsPrimalComponent\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures a primal component that uses a feedforward neural network\n", + "# to jointly predict the values of the binary variables, based on the\n", + "# objective cost function, and provides the solution to the solver as\n", + "# a warm start.\n", + "comp = JointVarsPrimalComponent(\n", + " clf=MLPClassifier(),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " action=SetWarmStart(),\n", + ")\n", + "\n", + "# Configures a primal component that uses a chain of logistic regression\n", + "# models to jointly predict the values of the binary variables, based on\n", + "# the objective function.\n", + "comp = JointVarsPrimalComponent(\n", + " clf=ClassifierChain(SingleClassFix(LogisticRegression())),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "dddf7be4", + "metadata": {}, + "source": [ + "## Expert primal component\n", + "\n", + "Before spending time and effort choosing a machine learning strategy and tweaking its parameters, it is usually a good idea to evaluate what would be the performance impact of the model if its predictions were 100% accurate. This is especially important for the prediction of warm starts, since they are not always very beneficial. To simplify this task, MIPLearn provides `ExpertPrimalComponent`, a component which simply loads the optimal solution from the HDF5 file, assuming that it has already been computed, then directly provides it to the solver using one of the available methods. This component is useful in benchmarks, to evaluate how close to the best theoretical performance the machine learning components are.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9e2e81b9", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from miplearn.components.primal.expert import ExpertPrimalComponent\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures an expert primal component, which reads a pre-computed\n", + "# optimal solution from the HDF5 file and provides it to the solver\n", + "# as warm start.\n", + "comp = ExpertPrimalComponent(action=SetWarmStart())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/guide/problems.ipynb.txt b/0.4/_sources/guide/problems.ipynb.txt new file mode 100644 index 0000000..acc35fb --- /dev/null +++ b/0.4/_sources/guide/problems.ipynb.txt @@ -0,0 +1,1607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f89436b4-5bc5-4ae3-a20a-522a2cd65274", + "metadata": {}, + "source": [ + "# Benchmark Problems\n", + "\n", + "## Overview\n", + "\n", + "Benchmark sets such as [MIPLIB](https://miplib.zib.de/) or [TSPLIB](http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/) are usually employed to evaluate the performance of conventional MIP solvers. Two shortcomings, however, make existing benchmark sets less suitable for evaluating the performance of learning-enhanced MIP solvers: (i) while existing benchmark sets typically contain hundreds or thousands of instances, machine learning (ML) methods typically benefit from having orders of magnitude more instances available for training; (ii) current machine learning methods typically provide best performance on sets of homogeneous instances, buch general-purpose benchmark sets contain relatively few examples of each problem type.\n", + "\n", + "To tackle this challenge, MIPLearn provides random instance generators for a wide variety of classical optimization problems, covering applications from different fields, that can be used to evaluate new learning-enhanced MIP techniques in a measurable and reproducible way. As of MIPLearn 0.3, nine problem generators are available, each customizable with user-provided probability distribution and flexible parameters. The generators can be configured, for example, to produce large sets of very similar instances of same size, where only the objective function changes, or more diverse sets of instances, with various sizes and characteristics, belonging to a particular problem class.\n", + "\n", + "In the following, we describe the problems included in the library, their MIP formulation and the generation algorithm." + ] + }, + { + "cell_type": "markdown", + "id": "bd99c51f", + "metadata": {}, + "source": [ + "
\n", + "Warning\n", + "\n", + "The random instance generators and formulations shown below are subject to change. If you use them in your research, for reproducibility, you should specify the MIPLearn version and all parameters.\n", + "
\n", + "\n", + "
\n", + "Note\n", + "\n", + "- To make the instances easier to process, all formulations are written as a minimization problem.\n", + "- Some problem formulations, such as the one for the *traveling salesman problem*, contain an exponential number of constraints, which are enforced through constraint generation. The MPS files for these problems contain only the constraints that were generated during a trial run, not the entire set of constraints. Resolving the MPS file, therefore, may not generate a feasible primal solution for the problem.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "830f3784-a3fc-4e2f-a484-e7808841ffe8", + "metadata": { + "tags": [] + }, + "source": [ + "## Bin Packing\n", + "\n", + "**Bin packing** is a combinatorial optimization problem that asks for the optimal way to pack a given set of items into a finite number of containers (or bins) of fixed capacity. More specifically, the problem is to assign indivisible items of different sizes to identical bins, while minimizing the number of bins used. The problem is NP-hard and has many practical applications, including logistics and warehouse management, where it is used to determine how to best store and transport goods using a limited amount of space." + ] + }, + { + "cell_type": "markdown", + "id": "af933298-92a9-4c5d-8d07-0d4918dedbb8", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $n$ be the number of items, and $s_i$ the size of the $i$-th item. Also let $B$ be the size of the bins. For each bin $j$, let $y_j$ be a binary decision variable which equals one if the bin is used. For every item-bin pair $(i,j)$, let $x_{ij}$ be a binary decision variable which equals one if item $i$ is assigned to bin $j$. The bin packing problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "5e502345", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{j=1}^n y_j \\\\\n", + "\\text{subject to} \\;\\;\\;\n", + " & \\sum_{i=1}^n s_i x_{ij} \\leq B y_j & \\forall j=1,\\ldots,n \\\\\n", + " & \\sum_{j=1}^n x_{ij} = 1 & \\forall i=1,\\ldots,n \\\\\n", + " & y_i \\in \\{0,1\\} & \\forall i=1,\\ldots,n \\\\\n", + " & x_{ij} \\in \\{0,1\\} & \\forall i,j=1,\\ldots,n \\\\\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "9cba2077", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "Random instances of the bin packing problem can be generated using the class [BinPackGenerator][BinPackGenerator].\n", + "\n", + "If `fix_items=False`, the class samples the user-provided probability distributions `n`, `sizes` and `capacity` to decide, respectively, the number of items, the sizes of the items and capacity of the bin. All values are sampled independently.\n", + "\n", + "If `fix_items=True`, the class creates a reference instance, using the method previously described, then generates additional instances by perturbing its item sizes and bin capacity. More specifically, the sizes of the items are set to $s_i \\gamma_i$, where $s_i$ is the size of the $i$-th item in the reference instance and $\\gamma_i$ is sampled from `sizes_jitter`. Similarly, the bin size is set to $B \\beta$, where $B$ is the reference bin size and $\\beta$ is sampled from `capacity_jitter`. The number of items remains the same across all generated instances.\n", + "\n", + "[BinPackGenerator]: ../../api/problems/#miplearn.problems.binpack.BinPackGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "2bc62803", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f14e560c-ef9f-4c48-8467-72d6acce5f9f", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.409419720Z", + "start_time": "2023-11-07T16:29:47.824353556Z" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 [ 8.47 26. 19.52 14.11 3.65 3.65 1.4 21.76 14.82 16.96] 102.24\n", + "1 [ 8.69 22.78 17.81 14.83 4.12 3.67 1.46 22.05 13.66 18.08] 93.41\n", + "2 [ 8.55 25.9 20. 15.89 3.75 3.59 1.51 21.4 13.89 17.68] 90.69\n", + "3 [10.13 22.62 18.89 14.4 3.92 3.94 1.36 23.69 15.85 19.26] 107.9\n", + "4 [ 9.55 25.77 16.79 14.06 3.55 3.76 1.42 20.66 16.02 17.19] 95.62\n", + "5 [ 9.44 22.06 19.41 13.69 4.28 4.11 1.36 19.51 15.98 18.43] 104.58\n", + "6 [ 9.87 21.74 17.78 13.82 4.18 4. 1.4 19.76 14.46 17.08] 104.59\n", + "7 [ 9.62 25.61 18.2 13.83 4.07 4.1 1.47 22.83 15.01 17.78] 98.55\n", + "8 [ 8.47 21.9 16.58 15.37 3.76 3.91 1.57 20.57 14.76 18.61] 94.58\n", + "9 [ 8.57 22.77 17.06 16.25 4.14 4. 1.56 22.97 14.09 19.09] 100.79\n", + "\n", + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 20 rows, 110 columns and 210 nonzeros\n", + "Model fingerprint: 0x1ff9913f\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+02]\n", + " Objective range [1e+00, 1e+00]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 5.0000000\n", + "Presolve time: 0.00s\n", + "Presolved: 20 rows, 110 columns, 210 nonzeros\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "\n", + "Root relaxation: objective 1.274844e+00, 38 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.27484 0 4 5.00000 1.27484 74.5% - 0s\n", + "H 0 0 4.0000000 1.27484 68.1% - 0s\n", + "H 0 0 2.0000000 1.27484 36.3% - 0s\n", + " 0 0 1.27484 0 4 2.00000 1.27484 36.3% - 0s\n", + "\n", + "Explored 1 nodes (38 simplex iterations) in 0.03 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 3: 2 4 5 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.000000000000e+00, best bound 2.000000000000e+00, gap 0.0000%\n", + "\n", + "User-callback calls 143, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.binpack import BinPackGenerator, build_binpack_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances of the binpack problem with ten items\n", + "data = BinPackGenerator(\n", + " n=randint(low=10, high=11),\n", + " sizes=uniform(loc=0, scale=25),\n", + " capacity=uniform(loc=100, scale=0),\n", + " sizes_jitter=uniform(loc=0.9, scale=0.2),\n", + " capacity_jitter=uniform(loc=0.9, scale=0.2),\n", + " fix_items=True,\n", + ").generate(10)\n", + "\n", + "# Print sizes and capacities\n", + "for i in range(10):\n", + " print(i, data[i].sizes, data[i].capacity)\n", + "print()\n", + "\n", + "# Optimize first instance\n", + "model = build_binpack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "9a3df608-4faf-444b-b5c2-18d3e90cbb5a", + "metadata": { + "tags": [] + }, + "source": [ + "## Multi-Dimensional Knapsack\n", + "\n", + "The **multi-dimensional knapsack problem** is a generalization of the classic knapsack problem, which involves selecting a subset of items to be placed in a knapsack such that the total value of the items is maximized without exceeding a maximum weight. In this generalization, items have multiple weights (representing multiple resources), and multiple weight constraints must be satisfied." + ] + }, + { + "cell_type": "markdown", + "id": "8d989002-d837-4ccf-a224-0504a6d66473", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $n$ be the number of items and $m$ be the number of resources. For each item $j$ and resource $i$, let $p_j$ be the price of the item, let $w_{ij}$ be the amount of resource $j$ item $i$ consumes (i.e. the $j$-th weight of the item), and let $b_i$ be the total amount of resource $i$ available (or the size of the $j$-th knapsack). The formulation is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "d0d3ea42", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & - \\sum_{j=1}^n p_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j=1}^n w_{ij} x_j \\leq b_i\n", + " & \\forall i=1,\\ldots,m \\\\\n", + " & x_j \\in \\{0,1\\}\n", + " & \\forall j=1,\\ldots,n\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "81b5b085-cfa9-45ce-9682-3aeb9be96cba", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [MultiKnapsackGenerator][MultiKnapsackGenerator] can be used to generate random instances of this problem. The number of items $n$ and knapsacks $m$ are sampled from the user-provided probability distributions `n` and `m`. The weights $w_{ij}$ are sampled independently from the provided distribution `w`. The capacity of knapsack $i$ is set to\n", + "\n", + "[MultiKnapsackGenerator]: ../../api/problems/#miplearn.problems.multiknapsack.MultiKnapsackGenerator\n", + "\n", + "$$\n", + " b_i = \\alpha_i \\sum_{j=1}^n w_{ij}\n", + "$$\n", + "\n", + "where $\\alpha_i$, the tightness ratio, is sampled from the provided probability\n", + "distribution `alpha`. To make the instances more challenging, the costs of the items\n", + "are linearly correlated to their average weights. More specifically, the price of each\n", + "item $j$ is set to:\n", + "\n", + "$$\n", + " p_j = \\sum_{i=1}^m \\frac{w_{ij}}{m} + K u_j,\n", + "$$\n", + "\n", + "where $K$, the correlation coefficient, and $u_j$, the correlation multiplier, are sampled\n", + "from the provided probability distributions `K` and `u`.\n", + "\n", + "If `fix_w=True` is provided, then $w_{ij}$ are kept the same in all generated instances. This also implies that $n$ and $m$ are kept fixed. Although the prices and capacities are derived from $w_{ij}$, as long as `u` and `K` are not constants, the generated instances will still not be completely identical.\n", + "\n", + "\n", + "If a probability distribution `w_jitter` is provided, then item weights will be set to $w_{ij} \\gamma_{ij}$ where $\\gamma_{ij}$ is sampled from `w_jitter`. When combined with `fix_w=True`, this argument may be used to generate instances where the weight of each item is roughly the same, but not exactly identical, across all instances. The prices of the items and the capacities of the knapsacks will be calculated as above, but using these perturbed weights instead.\n", + "\n", + "By default, all generated prices, weights and capacities are rounded to the nearest integer number. If `round=False` is provided, this rounding will be disabled." + ] + }, + { + "cell_type": "markdown", + "id": "f92135b8-67e7-4ec5-aeff-2fc17ad5e46d", + "metadata": {}, + "source": [ + "
\n", + "References\n", + "\n", + "* **Freville, Arnaud, and Gérard Plateau.** *An efficient preprocessing procedure for the multidimensional 0–1 knapsack problem.* Discrete applied mathematics 49.1-3 (1994): 189-212.\n", + "* **Fréville, Arnaud.** *The multidimensional 0–1 knapsack problem: An overview.* European Journal of Operational Research 155.1 (2004): 1-21.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "f12a066f", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ce5f8fb-2769-4fbd-a40c-fd62b897690a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.485068449Z", + "start_time": "2023-11-07T16:29:48.406139946Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prices\n", + " [350. 692. 454. 709. 605. 543. 321. 674. 571. 341.]\n", + "weights\n", + " [[392. 977. 764. 622. 158. 163. 56. 840. 574. 696.]\n", + " [ 20. 948. 860. 209. 178. 184. 293. 541. 414. 305.]\n", + " [629. 135. 278. 378. 466. 803. 205. 492. 584. 45.]\n", + " [630. 173. 64. 907. 947. 794. 312. 99. 711. 439.]\n", + " [117. 506. 35. 915. 266. 662. 312. 516. 521. 178.]]\n", + "capacities\n", + " [1310. 988. 1004. 1269. 1007.]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 50 nonzeros\n", + "Model fingerprint: 0xaf3ac15e\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [2e+01, 1e+03]\n", + " Objective range [3e+02, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+03, 1e+03]\n", + "Found heuristic solution: objective -804.0000000\n", + "Presolve removed 0 rows and 3 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 7 columns, 34 nonzeros\n", + "Variable types: 0 continuous, 7 integer (7 binary)\n", + "\n", + "Root relaxation: objective -1.428726e+03, 4 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 -1428.7265 0 4 -804.00000 -1428.7265 77.7% - 0s\n", + "H 0 0 -1279.000000 -1428.7265 11.7% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + "\n", + "Explored 1 nodes (4 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: -1279 -804 \n", + "No other solutions better than -1279\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -1.279000000000e+03, best bound -1.279000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 490, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.multiknapsack import (\n", + " MultiKnapsackGenerator,\n", + " build_multiknapsack_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate ten similar random instances of the multiknapsack problem with\n", + "# ten items, five resources and weights around [0, 1000].\n", + "data = MultiKnapsackGenerator(\n", + " n=randint(low=10, high=11),\n", + " m=randint(low=5, high=6),\n", + " w=uniform(loc=0, scale=1000),\n", + " K=uniform(loc=100, scale=0),\n", + " u=uniform(loc=1, scale=0),\n", + " alpha=uniform(loc=0.25, scale=0),\n", + " w_jitter=uniform(loc=0.95, scale=0.1),\n", + " p_jitter=uniform(loc=0.75, scale=0.5),\n", + " fix_w=True,\n", + ").generate(10)\n", + "\n", + "# Print data for one of the instances\n", + "print(\"prices\\n\", data[0].prices)\n", + "print(\"weights\\n\", data[0].weights)\n", + "print(\"capacities\\n\", data[0].capacities)\n", + "print()\n", + "\n", + "# Build model and optimize\n", + "model = build_multiknapsack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "e20376b0-0781-4bfa-968f-ded5fa47e176", + "metadata": { + "tags": [] + }, + "source": [ + "## Capacitated P-Median\n", + "\n", + "The **capacitated p-median** problem is a variation of the classic $p$-median problem, in which a set of customers must be served by a set of facilities. In the capacitated $p$-Median problem, each facility has a fixed capacity, and the goal is to minimize the total cost of serving the customers while ensuring that the capacity of each facility is not exceeded. Variations of problem are often used in logistics and supply chain management to determine the most efficient locations for warehouses or distribution centers." + ] + }, + { + "cell_type": "markdown", + "id": "2af65137-109e-4ca0-8753-bd999825204f", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $I=\\{1,\\ldots,n\\}$ be the set of customers. For each customer $i \\in I$, let $d_i$ be its demand and let $y_i$ be a binary decision variable that equals one if we decide to open a facility at that customer's location. For each pair $(i,j) \\in I \\times I$, let $x_{ij}$ be a binary decision variable that equals one if customer $i$ is assigned to facility $j$. Furthermore, let $w_{ij}$ be the cost of serving customer $i$ from facility $j$, let $p$ be the number of facilities we must open, and let $c_j$ be the capacity of facility $j$. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "a2494ab1-d306-4db7-a100-8f1dfd4a55d7", + "metadata": { + "tags": [] + }, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & \\sum_{i \\in I} \\sum_{j \\in I} w_{ij} x_{ij}\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j \\in I} x_{ij} = 1 & \\forall i \\in I \\\\\n", + " & \\sum_{j \\in I} y_j = p \\\\\n", + " & \\sum_{i \\in I} d_i x_{ij} \\leq c_j y_j & \\forall j \\in I \\\\\n", + " & x_{ij} \\in \\{0, 1\\} & \\forall i, j \\in I \\\\\n", + " & y_j \\in \\{0, 1\\} & \\forall j \\in I\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "9dddf0d6-1f86-40d4-93a8-ccfe93d38e0d", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [PMedianGenerator][PMedianGenerator] can be used to generate random instances of this problem. First, it decides the number of customers and the parameter $p$ by sampling the provided `n` and `p` distributions, respectively. Then, for each customer $i$, the class builds its geographical location $(x_i, y_i)$ by sampling the provided `x` and `y` distributions. For each $i$, the demand for customer $i$ and the capacity of facility $i$ are decided by sampling the provided distributions `demands` and `capacities`, respectively. Finally, the costs $w_{ij}$ are set to the Euclidean distance between the locations of customers $i$ and $j$.\n", + "\n", + "If `fixed=True`, then the number of customers, their locations, the parameter $p$, the demands and the capacities are only sampled from their respective distributions exactly once, to build a reference instance which is then randomly perturbed. Specifically, in each perturbation, the distances, demands and capacities are multiplied by random scaling factors sampled from the distributions `distances_jitter`, `demands_jitter` and `capacities_jitter`, respectively. The result is a list of instances that have the same set of customers, but slightly different demands, capacities and distances.\n", + "\n", + "[PMedianGenerator]: ../../api/problems/#miplearn.problems.pmedian.PMedianGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "4e701397", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4e0e4223-b4e0-4962-a157-82a23a86e37d", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.575025403Z", + "start_time": "2023-11-07T16:29:48.453962705Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "p = 5\n", + "distances =\n", + " [[ 0. 50.17 82.42 32.76 33.2 35.45 86.88 79.11 43.17 66.2 ]\n", + " [ 50.17 0. 72.64 72.51 17.06 80.25 39.92 68.93 43.41 42.96]\n", + " [ 82.42 72.64 0. 71.69 70.92 82.51 67.88 3.76 39.74 30.73]\n", + " [ 32.76 72.51 71.69 0. 56.56 11.03 101.35 69.39 42.09 68.58]\n", + " [ 33.2 17.06 70.92 56.56 0. 63.68 54.71 67.16 34.89 44.99]\n", + " [ 35.45 80.25 82.51 11.03 63.68 0. 111.04 80.29 52.78 79.36]\n", + " [ 86.88 39.92 67.88 101.35 54.71 111.04 0. 65.13 61.37 40.82]\n", + " [ 79.11 68.93 3.76 69.39 67.16 80.29 65.13 0. 36.26 27.24]\n", + " [ 43.17 43.41 39.74 42.09 34.89 52.78 61.37 36.26 0. 26.62]\n", + " [ 66.2 42.96 30.73 68.58 44.99 79.36 40.82 27.24 26.62 0. ]]\n", + "demands = [6.12 1.39 2.92 3.66 4.56 7.85 2. 5.14 5.92 0.46]\n", + "capacities = [151.89 42.63 16.26 237.22 241.41 202.1 76.15 24.42 171.06 110.04]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 21 rows, 110 columns and 220 nonzeros\n", + "Model fingerprint: 0x8d8d9346\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Coefficient statistics:\n", + " Matrix range [5e-01, 2e+02]\n", + " Objective range [4e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 5e+00]\n", + "Found heuristic solution: objective 368.7900000\n", + "Presolve time: 0.00s\n", + "Presolved: 21 rows, 110 columns, 220 nonzeros\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Found heuristic solution: objective 245.6400000\n", + "\n", + "Root relaxation: objective 0.000000e+00, 18 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 0.00000 0 6 245.64000 0.00000 100% - 0s\n", + "H 0 0 185.1900000 0.00000 100% - 0s\n", + "H 0 0 148.6300000 17.14595 88.5% - 0s\n", + "H 0 0 113.1800000 17.14595 84.9% - 0s\n", + " 0 0 17.14595 0 10 113.18000 17.14595 84.9% - 0s\n", + "H 0 0 99.5000000 17.14595 82.8% - 0s\n", + "H 0 0 98.3900000 17.14595 82.6% - 0s\n", + "H 0 0 93.9800000 64.28872 31.6% - 0s\n", + " 0 0 64.28872 0 15 93.98000 64.28872 31.6% - 0s\n", + "H 0 0 93.9200000 64.28872 31.5% - 0s\n", + " 0 0 86.06884 0 15 93.92000 86.06884 8.36% - 0s\n", + "* 0 0 0 91.2300000 91.23000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (70 simplex iterations) in 0.08 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 10: 91.23 93.92 93.98 ... 368.79\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 9.123000000000e+01, best bound 9.123000000000e+01, gap 0.0000%\n", + "\n", + "User-callback calls 190, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.pmedian import PMedianGenerator, build_pmedian_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with ten customers located in a\n", + "# 100x100 square, with demands in [0,10], capacities in [0, 250].\n", + "data = PMedianGenerator(\n", + " x=uniform(loc=0.0, scale=100.0),\n", + " y=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=randint(low=5, high=6),\n", + " demands=uniform(loc=0, scale=10),\n", + " capacities=uniform(loc=0, scale=250),\n", + " distances_jitter=uniform(loc=0.9, scale=0.2),\n", + " demands_jitter=uniform(loc=0.9, scale=0.2),\n", + " capacities_jitter=uniform(loc=0.9, scale=0.2),\n", + " fixed=True,\n", + ").generate(10)\n", + "\n", + "# Print data for one of the instances\n", + "print(\"p =\", data[0].p)\n", + "print(\"distances =\\n\", data[0].distances)\n", + "print(\"demands =\", data[0].demands)\n", + "print(\"capacities =\", data[0].capacities)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_pmedian_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "36129dbf-ecba-4026-ad4d-f2356bad4a26", + "metadata": {}, + "source": [ + "## Set cover\n", + "\n", + "The **set cover problem** is a classical NP-hard optimization problem which aims to minimize the number of sets needed to cover all elements in a given universe. Each set may contain a different number of elements, and sets may overlap with each other. This problem can be useful in various real-world scenarios such as scheduling, resource allocation, and network design." + ] + }, + { + "cell_type": "markdown", + "id": "d5254e7a", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $U = \\{1,\\ldots,n\\}$ be a given universe set, and let $S=\\{S_1,\\ldots,S_m\\}$ be a collection of sets whose union equal $U$. For each $j \\in \\{1,\\ldots,m\\}$, let $w_j$ be the weight of set $S_j$, and let $x_j$ be a binary decision variable that equals one if set $S_j$ is chosen. The set cover problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "5062d606-678c-45ba-9a45-d3c8b7401ad1", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & \\sum_{j=1}^m w_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j : i \\in S_j} x_j \\geq 1 & \\forall i \\in \\{1,\\ldots,n\\} \\\\\n", + " & x_j \\in \\{0, 1\\} & \\forall j \\in \\{1,\\ldots,m\\}\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "2732c050-2e11-44fc-bdd1-1b804a60f166", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [SetCoverGenerator] can generate random instances of this problem. The class first decides the number of elements and sets by sampling the provided distributions `n_elements` and `n_sets`, respectively. Then it generates a random incidence matrix $M$, as follows:\n", + "\n", + "1. The density $d$ of $M$ is decided by sampling the provided probability distribution `density`.\n", + "2. Each entry of $M$ is then sampled from the Bernoulli distribution, with probability $d$.\n", + "3. To ensure that each element belongs to at least one set, the class identifies elements that are not contained in any set, then assigns them to a random set (chosen uniformly).\n", + "4. Similarly, to ensure that each set contains at least one element, the class identifies empty sets, then modifies them to include one random element (chosen uniformly).\n", + "\n", + "Finally, the weight of set $j$ is set to $w_j + K | S_j |$, where $w_j$ and $k$ are sampled from `costs` and `K`, respectively, and where $|S_j|$ denotes the size of set $S_j$. The parameter $K$ is used to introduce some correlation between the size of the set and its weight, making the instance more challenging. Note that `K` is only sampled once for the entire instance.\n", + "\n", + "If `fix_sets=True`, then all generated instances have exactly the same sets and elements. The costs of the sets, however, are multiplied by random scaling factors sampled from the provided probability distribution `costs_jitter`.\n", + "\n", + "[SetCoverGenerator]: ../../api/problems/#miplearn.problems.setcover.SetCoverGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "569aa5ec-d475-41fa-a5d9-0b1a675fdf95", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3224845b-9afd-463e-abf4-e0e93d304859", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.804292323Z", + "start_time": "2023-11-07T16:29:48.492933268Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matrix\n", + " [[1 0 0 0 1 1 1 0 0 0]\n", + " [1 0 0 1 1 1 1 0 1 1]\n", + " [0 1 1 1 1 0 1 0 0 1]\n", + " [0 1 1 0 0 0 1 1 0 1]\n", + " [1 1 1 0 1 0 1 0 0 1]]\n", + "costs [1044.58 850.13 1014.5 944.83 697.9 971.87 213.49 220.98 70.23\n", + " 425.33]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 28 nonzeros\n", + "Model fingerprint: 0xe5c2d4fa\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [7e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 213.4900000\n", + "Presolve removed 5 rows and 10 columns\n", + "Presolve time: 0.00s\n", + "Presolve: All rows and columns removed\n", + "\n", + "Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 1: 213.49 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.134900000000e+02, best bound 2.134900000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 178, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.setcover import SetCoverGenerator, build_setcover_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Build random instances with five elements, ten sets and costs\n", + "# in the [0, 1000] interval, with a correlation factor of 25 and\n", + "# an incidence matrix with 25% density.\n", + "data = SetCoverGenerator(\n", + " n_elements=randint(low=5, high=6),\n", + " n_sets=randint(low=10, high=11),\n", + " costs=uniform(loc=0.0, scale=1000.0),\n", + " costs_jitter=uniform(loc=0.90, scale=0.20),\n", + " density=uniform(loc=0.5, scale=0.00),\n", + " K=uniform(loc=25.0, scale=0.0),\n", + " fix_sets=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for one instance\n", + "print(\"matrix\\n\", data[0].incidence_matrix)\n", + "print(\"costs\", data[0].costs)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_setcover_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "255a4e88-2e38-4a1b-ba2e-806b6bd4c815", + "metadata": {}, + "source": [ + "## Set Packing\n", + "\n", + "**Set packing** is a classical optimization problem that asks for the maximum number of disjoint sets within a given list. This problem often arises in real-world situations where a finite number of resources need to be allocated to tasks, such as airline flight crew scheduling." + ] + }, + { + "cell_type": "markdown", + "id": "19342eb1", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $U=\\{1,\\ldots,n\\}$ be a given universe set, and let $S = \\{S_1, \\ldots, S_m\\}$ be a collection of subsets of $U$. For each subset $j \\in \\{1, \\ldots, m\\}$, let $w_j$ be the weight of $S_j$ and let $x_j$ be a binary decision variable which equals one if set $S_j$ is chosen. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "0391b35b", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & -\\sum_{j=1}^m w_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j : i \\in S_j} x_j \\leq 1 & \\forall i \\in \\{1,\\ldots,n\\} \\\\\n", + " & x_j \\in \\{0, 1\\} & \\forall j \\in \\{1,\\ldots,m\\}\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "c2d7df7b", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [SetPackGenerator][SetPackGenerator] can generate random instances of this problem. It accepts exactly the same arguments, and generates instance data in exactly the same way as [SetCoverGenerator][SetCoverGenerator]. For more details, please see the documentation for that class.\n", + "\n", + "[SetPackGenerator]: ../../api/problems/#miplearn.problems.setpack.SetPackGenerator\n", + "[SetCoverGenerator]: ../../api/problems/#miplearn.problems.setcover.SetCoverGenerator\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cc797da7", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.806917868Z", + "start_time": "2023-11-07T16:29:48.781619530Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matrix\n", + " [[1 0 0 0 1 1 1 0 0 0]\n", + " [1 0 0 1 1 1 1 0 1 1]\n", + " [0 1 1 1 1 0 1 0 0 1]\n", + " [0 1 1 0 0 0 1 1 0 1]\n", + " [1 1 1 0 1 0 1 0 0 1]]\n", + "costs [1044.58 850.13 1014.5 944.83 697.9 971.87 213.49 220.98 70.23\n", + " 425.33]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 28 nonzeros\n", + "Model fingerprint: 0x4ee91388\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [7e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective -1265.560000\n", + "Presolve removed 5 rows and 10 columns\n", + "Presolve time: 0.00s\n", + "Presolve: All rows and columns removed\n", + "\n", + "Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 2: -1986.37 -1265.56 \n", + "No other solutions better than -1986.37\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -1.986370000000e+03, best bound -1.986370000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 238, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.setpack import SetPackGenerator, build_setpack_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Build random instances with five elements, ten sets and costs\n", + "# in the [0, 1000] interval, with a correlation factor of 25 and\n", + "# an incidence matrix with 25% density.\n", + "data = SetPackGenerator(\n", + " n_elements=randint(low=5, high=6),\n", + " n_sets=randint(low=10, high=11),\n", + " costs=uniform(loc=0.0, scale=1000.0),\n", + " costs_jitter=uniform(loc=0.90, scale=0.20),\n", + " density=uniform(loc=0.5, scale=0.00),\n", + " K=uniform(loc=25.0, scale=0.0),\n", + " fix_sets=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for one instance\n", + "print(\"matrix\\n\", data[0].incidence_matrix)\n", + "print(\"costs\", data[0].costs)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_setpack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "373e450c-8f8b-4b59-bf73-251bdd6ff67e", + "metadata": {}, + "source": [ + "## Stable Set\n", + "\n", + "The **maximum-weight stable set problem** is a classical optimization problem in graph theory which asks for the maximum-weight subset of vertices in a graph such that no two vertices in the subset are adjacent. The problem often arises in real-world scheduling or resource allocation situations, where stable sets represent tasks or resources that can be chosen simultaneously without conflicts.\n", + "\n", + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple undirected graph, and for each vertex $v \\in V$, let $w_v$ be its weight. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "2f74dd10", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\; & -\\sum_{v \\in V} w_v x_v \\\\\n", + "\\text{such that} \\;\\;\\; & x_v + x_u \\leq 1 & \\forall (v,u) \\in E \\\\\n", + "& x_v \\in \\{0, 1\\} & \\forall v \\in V\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "ef030168", + "metadata": {}, + "source": [ + "\n", + "### Random instance generator\n", + "\n", + "The class [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator] can be used to generate random instances of this problem. The class first samples the user-provided probability distributions `n` and `p` to decide the number of vertices and the density of the graph. Then, it generates a random Erdős-Rényi graph $G_{n,p}$. We recall that, in such a graph, each potential edge is included with probabilty $p$, independently for each other. The class then samples the provided probability distribution `w` to decide the vertex weights.\n", + "\n", + "[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n", + "\n", + "If `fix_graph=True`, then all generated instances have the same random graph. For each instance, the weights are decided by sampling `w`, as described above.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f996e99-0ec9-472b-be8a-30c9b8556931", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.954896857Z", + "start_time": "2023-11-07T16:29:48.825579097Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]\n", + "weights[0] [37.45 95.07 73.2 59.87 15.6 15.6 5.81 86.62 60.11 70.81]\n", + "weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]\n", + "\n", + "Set parameter PreCrush to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 15 rows, 10 columns and 30 nonzeros\n", + "Model fingerprint: 0x3240ea4a\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [6e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective -219.1400000\n", + "Presolve removed 7 rows and 2 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 8 rows, 8 columns, 19 nonzeros\n", + "Variable types: 0 continuous, 8 integer (8 binary)\n", + "\n", + "Root relaxation: objective -2.205650e+02, 5 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 infeasible 0 -219.14000 -219.14000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: -219.14 \n", + "No other solutions better than -219.14\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -2.191400000000e+02, best bound -2.191400000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 299, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.stab import (\n", + " MaxWeightStableSetGenerator,\n", + " build_stab_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed 10-node graph,\n", + "# 25% density and random weights in the [0, 100] interval.\n", + "data = MaxWeightStableSetGenerator(\n", + " w=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=uniform(loc=0.25, scale=0.0),\n", + " fix_graph=True,\n", + ").generate(10)\n", + "\n", + "# Print the graph and weights for two instances\n", + "print(\"graph\", data[0].graph.edges)\n", + "print(\"weights[0]\", data[0].weights)\n", + "print(\"weights[1]\", data[1].weights)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_stab_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "444d1092-fd83-4957-b691-a198d56ba066", + "metadata": {}, + "source": [ + "## Traveling Salesman\n", + "\n", + "Given a list of cities and the distances between them, the **traveling salesman problem** asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp's 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes." + ] + }, + { + "cell_type": "markdown", + "id": "da3ca69c", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple undirected graph. For each edge $e \\in E$, let $d_e$ be its weight (or distance) and let $x_e$ be a binary decision variable which equals one if $e$ is included in the route. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "9cf296e9", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{e \\in E} d_e x_e \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & \\sum_{e : \\delta(v)} x_e = 2 & \\forall v \\in V, \\\\\n", + " & \\sum_{e \\in \\delta(S)} x_e \\geq 2 & \\forall S \\subsetneq V, |S| \\neq \\emptyset, \\\\\n", + " & x_e \\in \\{0, 1\\} & \\forall e \\in E,\n", + "\\end{align*}\n", + "$$\n", + "where $\\delta(v)$ denotes the set of edges adjacent to vertex $v$, and $\\delta(S)$ denotes the set of edges that have one extremity in $S$ and one in $V \\setminus S$. Because of its exponential size, we enforce the second set of inequalities as lazy constraints." + ] + }, + { + "cell_type": "markdown", + "id": "eba3dbe5", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [TravelingSalesmanGenerator][TravelingSalesmanGenerator] can be used to generate random instances of this problem. Initially, the class samples the user-provided probability distribution `n` to decide how many cities to generate. Then, for each city $i$, the class generates its geographical location $(x_i, y_i)$ by sampling the provided distributions `x` and `y`. The distance $d_{ij}$ between cities $i$ and $j$ is then set to\n", + "$$\n", + "\\gamma_{ij} \\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2},\n", + "$$\n", + "where $\\gamma$ is a random scaling factor sampled from the provided probability distribution `gamma`.\n", + "\n", + "If `fix_cities=True`, then the list of cities is kept the same for all generated instances. The $\\gamma$ values, however, and therefore also the distances, are still different. By default, all distances $d_{ij}$ are rounded to the nearest integer. If `round=False` is provided, this rounding will be disabled.\n", + "\n", + "[TravelingSalesmanGenerator]: ../../api/problems/#miplearn.problems.tsp.TravelingSalesmanGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "61f16c56", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9d0c56c6", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.958833448Z", + "start_time": "2023-11-07T16:29:48.898121017Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "distances[0]\n", + " [[ 0. 513. 762. 358. 325. 374. 932. 731. 391. 634.]\n", + " [ 513. 0. 726. 765. 163. 754. 409. 719. 446. 400.]\n", + " [ 762. 726. 0. 780. 756. 744. 656. 40. 383. 334.]\n", + " [ 358. 765. 780. 0. 549. 117. 925. 702. 422. 728.]\n", + " [ 325. 163. 756. 549. 0. 663. 526. 708. 377. 462.]\n", + " [ 374. 754. 744. 117. 663. 0. 1072. 802. 501. 853.]\n", + " [ 932. 409. 656. 925. 526. 1072. 0. 654. 603. 433.]\n", + " [ 731. 719. 40. 702. 708. 802. 654. 0. 381. 255.]\n", + " [ 391. 446. 383. 422. 377. 501. 603. 381. 0. 287.]\n", + " [ 634. 400. 334. 728. 462. 853. 433. 255. 287. 0.]]\n", + "distances[1]\n", + " [[ 0. 493. 900. 354. 323. 367. 841. 727. 444. 668.]\n", + " [ 493. 0. 690. 687. 175. 725. 368. 744. 398. 446.]\n", + " [ 900. 690. 0. 666. 728. 827. 736. 41. 371. 317.]\n", + " [ 354. 687. 666. 0. 570. 104. 1090. 712. 454. 648.]\n", + " [ 323. 175. 728. 570. 0. 655. 521. 650. 356. 469.]\n", + " [ 367. 725. 827. 104. 655. 0. 1146. 779. 476. 752.]\n", + " [ 841. 368. 736. 1090. 521. 1146. 0. 681. 565. 394.]\n", + " [ 727. 744. 41. 712. 650. 779. 681. 0. 374. 286.]\n", + " [ 444. 398. 371. 454. 356. 476. 565. 374. 0. 274.]\n", + " [ 668. 446. 317. 648. 469. 752. 394. 286. 274. 0.]]\n", + "\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x719675e5\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "\n", + "Root relaxation: objective 2.921000e+03, 17 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + "* 0 0 0 2921.0000000 2921.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (17 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 2921 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.921000000000e+03, best bound 2.921000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 106, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed ten cities in the 1000x1000 box\n", + "# and random distance scaling factors in the [0.90, 1.10] interval.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(10)\n", + "\n", + "# Print distance matrices for the first two instances\n", + "print(\"distances[0]\\n\", data[0].distances)\n", + "print(\"distances[1]\\n\", data[1].distances)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_tsp_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "26dfc157-11f4-4564-b368-95ee8200875e", + "metadata": {}, + "source": [ + "## Unit Commitment\n", + "\n", + "The **unit commitment problem** is a mixed-integer optimization problem which asks which power generation units should be turned on and off, at what time, and at what capacity, in order to meet the demand for electricity generation at the lowest cost. Numerous operational constraints are typically enforced, such as *ramping constraints*, which prevent generation units from changing power output levels too quickly from one time step to the next, and *minimum-up* and *minimum-down* constraints, which prevent units from switching on and off too frequently. The unit commitment problem is widely used in power systems planning and operations." + ] + }, + { + "cell_type": "markdown", + "id": "7048d771", + "metadata": {}, + "source": [ + "\n", + "
\n", + "Note\n", + "\n", + "MIPLearn includes a simple formulation for the unit commitment problem, which enforces only minimum and maximum power production, as well as minimum-up and minimum-down constraints. The formulation does not enforce, for example, ramping trajectories, piecewise-linear cost curves, start-up costs or transmission and n-1 security constraints. For a more complete set of formulations, solution methods and realistic benchmark instances for the problem, see [UnitCommitment.jl](https://github.com/ANL-CEEESA/UnitCommitment.jl).\n", + "
\n", + "\n", + "### Formulation\n", + "\n", + "Let $T$ be the number of time steps, $G$ be the number of generation units, and let $D_t$ be the power demand (in MW) at time $t$. For each generating unit $g$, let $P^\\max_g$ and $P^\\min_g$ be the maximum and minimum amount of power the unit is able to produce when switched on; let $L_g$ and $l_g$ be the minimum up- and down-time for unit $g$; let $C^\\text{fixed}$ be the cost to keep unit $g$ on for one time step, regardless of its power output level; let $C^\\text{start}$ be the cost to switch unit $g$ on; and let $C^\\text{var}$ be the cost for generator $g$ to produce 1 MW of power. In this formulation, we assume linear production costs. For each generator $g$ and time $t$, let $x_{gt}$ be a binary variable which equals one if unit $g$ is on at time $t$, let $w_{gt}$ be a binary variable which equals one if unit $g$ switches from being off at time $t-1$ to being on at time $t$, and let $p_{gt}$ be a continuous variable which indicates the amount of power generated. The formulation is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "bec5ee1c", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{t=1}^T \\sum_{g=1}^G \\left(\n", + " x_{gt} C^\\text{fixed}_g\n", + " + w_{gt} C^\\text{start}_g\n", + " + p_{gt} C^\\text{var}_g\n", + " \\right)\n", + " \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & \\sum_{k=t-L_g+1}^t w_{gk} \\leq x_{gt}\n", + " & \\forall g\\; \\forall t=L_g-1,\\ldots,T-1 \\\\\n", + " & \\sum_{k=g-l_g+1}^T w_{gt} \\leq 1 - x_{g,t-l_g+1}\n", + " & \\forall g \\forall t=l_g-1,\\ldots,T-1 \\\\\n", + " & w_{gt} \\geq x_{gt} - x_{g,t-1}\n", + " & \\forall g \\forall t=1,\\ldots,T-1 \\\\\n", + " & \\sum_{g=1}^G p_{gt} \\geq D_t\n", + " & \\forall t \\\\\n", + " & P^\\text{min}_g x_{gt} \\leq p_{gt}\n", + " & \\forall g, t \\\\\n", + " & p_{gt} \\leq P^\\text{max}_g x_{gt}\n", + " & \\forall g, t \\\\\n", + " & x_{gt} \\in \\{0, 1\\}\n", + " & \\forall g, t.\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "4a1ffb4c", + "metadata": {}, + "source": [ + "\n", + "The first set of inequalities enforces minimum up-time constraints: if unit $g$ is down at time $t$, then it cannot start up during the previous $L_g$ time steps. The second set of inequalities enforces minimum down-time constraints, and is symmetrical to the previous one. The third set ensures that if unit $g$ starts up at time $t$, then the start up variable must be one. The fourth set ensures that demand is satisfied at each time period. The fifth and sixth sets enforce bounds to the quantity of power generated by each unit.\n", + "\n", + "
\n", + "References\n", + "\n", + "- *Bendotti, P., Fouilhoux, P. & Rottner, C.* **The min-up/min-down unit commitment polytope.** J Comb Optim 36, 1024-1058 (2018). https://doi.org/10.1007/s10878-018-0273-y\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "01bed9fc", + "metadata": {}, + "source": [ + "\n", + "### Random instance generator\n", + "\n", + "The class `UnitCommitmentGenerator` can be used to generate random instances of this problem.\n", + "\n", + "First, the user-provided probability distributions `n_units` and `n_periods` are sampled to determine the number of generating units and the number of time steps, respectively. Then, for each unit, the probabilities `max_power` and `min_power` are sampled to determine the unit's maximum and minimum power output. To make it easier to generate valid ranges, `min_power` is not specified as the absolute power level in MW, but rather as a multiplier of `max_power`; for example, if `max_power` samples to 100 and `min_power` samples to 0.5, then the unit's power range is set to `[50,100]`. Then, the distributions `cost_startup`, `cost_prod` and `cost_fixed` are sampled to determine the unit's startup, variable and fixed costs, while the distributions `min_uptime` and `min_downtime` are sampled to determine its minimum up/down-time.\n", + "\n", + "After parameters for the units have been generated, the class then generates a periodic demand curve, with a peak every 12 time steps, in the range $(0.4C, 0.8C)$, where $C$ is the sum of all units' maximum power output. Finally, all costs and demand values are perturbed by random scaling factors independently sampled from the distributions `cost_jitter` and `demand_jitter`, respectively.\n", + "\n", + "If `fix_units=True`, then the list of generators (with their respective parameters) is kept the same for all generated instances. If `cost_jitter` and `demand_jitter` are provided, the instances will still have slightly different costs and demands." + ] + }, + { + "cell_type": "markdown", + "id": "855b87b4", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6217da7c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:49.061613905Z", + "start_time": "2023-11-07T16:29:48.941857719Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "min_power[0] [117.79 245.85 271.85 207.7 81.38]\n", + "max_power[0] [218.54 477.82 379.4 319.4 120.21]\n", + "min_uptime[0] [7 6 3 5 7]\n", + "min_downtime[0] [7 3 5 6 2]\n", + "min_power[0] [117.79 245.85 271.85 207.7 81.38]\n", + "cost_startup[0] [3042.42 5247.56 4319.45 2912.29 6118.53]\n", + "cost_prod[0] [ 6.97 14.61 18.32 22.8 39.26]\n", + "cost_fixed[0] [199.67 514.23 592.41 46.45 607.54]\n", + "demand[0]\n", + " [ 905.06 915.41 1166.52 1212.29 1127.81 953.52 905.06 796.21 783.78\n", + " 866.23 768.62 899.59 905.06 946.23 1087.61 1004.24 1048.36 992.03\n", + " 905.06 750.82 691.48 606.15 658.5 809.95]\n", + "\n", + "min_power[1] [117.79 245.85 271.85 207.7 81.38]\n", + "max_power[1] [218.54 477.82 379.4 319.4 120.21]\n", + "min_uptime[1] [7 6 3 5 7]\n", + "min_downtime[1] [7 3 5 6 2]\n", + "min_power[1] [117.79 245.85 271.85 207.7 81.38]\n", + "cost_startup[1] [2458.08 6200.26 4585.74 2666.05 4783.34]\n", + "cost_prod[1] [ 6.31 13.33 20.42 24.37 46.86]\n", + "cost_fixed[1] [196.9 416.42 655.57 52.51 626.15]\n", + "demand[1]\n", + " [ 981.42 840.07 1095.59 1102.03 1088.41 932.29 863.67 848.56 761.33\n", + " 828.28 775.18 834.99 959.76 865.72 1193.52 1058.92 985.19 893.92\n", + " 962.16 781.88 723.15 639.04 602.4 787.02]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 578 rows, 360 columns and 2128 nonzeros\n", + "Model fingerprint: 0x4dc1c661\n", + "Variable types: 120 continuous, 240 integer (240 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 5e+02]\n", + " Objective range [7e+00, 6e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+03]\n", + "Presolve removed 244 rows and 131 columns\n", + "Presolve time: 0.01s\n", + "Presolved: 334 rows, 229 columns, 842 nonzeros\n", + "Variable types: 116 continuous, 113 integer (113 binary)\n", + "Found heuristic solution: objective 440662.46430\n", + "Found heuristic solution: objective 429461.97680\n", + "Found heuristic solution: objective 374043.64040\n", + "\n", + "Root relaxation: objective 3.361348e+05, 142 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 336134.820 0 18 374043.640 336134.820 10.1% - 0s\n", + "H 0 0 368600.14450 336134.820 8.81% - 0s\n", + "H 0 0 364721.76610 336134.820 7.84% - 0s\n", + " 0 0 cutoff 0 364721.766 364721.766 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 3\n", + " Cover: 8\n", + " Implied bound: 29\n", + " Clique: 222\n", + " MIR: 7\n", + " Flow cover: 7\n", + " RLT: 1\n", + " Relax-and-lift: 7\n", + "\n", + "Explored 1 nodes (234 simplex iterations) in 0.02 seconds (0.02 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 5: 364722 368600 374044 ... 440662\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 3.647217661000e+05, best bound 3.647217661000e+05, gap 0.0000%\n", + "\n", + "User-callback calls 677, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.uc import UnitCommitmentGenerator, build_uc_model_gurobipy\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a random instance with 5 generators and 24 time steps\n", + "data = UnitCommitmentGenerator(\n", + " n_units=randint(low=5, high=6),\n", + " n_periods=randint(low=24, high=25),\n", + " max_power=uniform(loc=50, scale=450),\n", + " min_power=uniform(loc=0.5, scale=0.25),\n", + " cost_startup=uniform(loc=0, scale=10_000),\n", + " cost_prod=uniform(loc=0, scale=50),\n", + " cost_fixed=uniform(loc=0, scale=1_000),\n", + " min_uptime=randint(low=2, high=8),\n", + " min_downtime=randint(low=2, high=8),\n", + " cost_jitter=uniform(loc=0.75, scale=0.5),\n", + " demand_jitter=uniform(loc=0.9, scale=0.2),\n", + " fix_units=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for the two first instances\n", + "for i in range(2):\n", + " print(f\"min_power[{i}]\", data[i].min_power)\n", + " print(f\"max_power[{i}]\", data[i].max_power)\n", + " print(f\"min_uptime[{i}]\", data[i].min_uptime)\n", + " print(f\"min_downtime[{i}]\", data[i].min_downtime)\n", + " print(f\"min_power[{i}]\", data[i].min_power)\n", + " print(f\"cost_startup[{i}]\", data[i].cost_startup)\n", + " print(f\"cost_prod[{i}]\", data[i].cost_prod)\n", + " print(f\"cost_fixed[{i}]\", data[i].cost_fixed)\n", + " print(f\"demand[{i}]\\n\", data[i].demand)\n", + " print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_uc_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "169293c7-33e1-4d28-8d39-9982776251d7", + "metadata": {}, + "source": [ + "## Vertex Cover\n", + "\n", + "**Minimum weight vertex cover** is a classical optimization problem in graph theory where the goal is to find the minimum-weight set of vertices that are connected to all of the edges in the graph. The problem generalizes one of Karp's 21 NP-complete problems and has applications in various fields, including bioinformatics and machine learning." + ] + }, + { + "cell_type": "markdown", + "id": "91f5781a", + "metadata": {}, + "source": [ + "\n", + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple graph. For each vertex $v \\in V$, let $w_g$ be its weight, and let $x_v$ be a binary decision variable which equals one if $v$ is included in the cover. The mixed-integer linear formulation for the problem is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "544754cb", + "metadata": {}, + "source": [ + " $$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{v \\in V} w_v \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & x_i + x_j \\ge 1 & \\forall \\{i, j\\} \\in E, \\\\\n", + " & x_{i,j} \\in \\{0, 1\\}\n", + " & \\forall \\{i,j\\} \\in E.\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "35c99166", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [MinWeightVertexCoverGenerator][MinWeightVertexCoverGenerator] can be used to generate random instances of this problem. The class accepts exactly the same parameters and behaves exactly in the same way as [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator]. See the [stable set section](#Stable-Set) for more details.\n", + "\n", + "[MinWeightVertexCoverGenerator]: ../../api/problems/#module-miplearn.problems.vertexcover\n", + "[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5fff7afe-5b7a-4889-a502-66751ec979bf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:49.075657363Z", + "start_time": "2023-11-07T16:29:49.049561363Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]\n", + "weights[0] [37.45 95.07 73.2 59.87 15.6 15.6 5.81 86.62 60.11 70.81]\n", + "weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 15 rows, 10 columns and 30 nonzeros\n", + "Model fingerprint: 0x2d2d1390\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [6e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 301.0000000\n", + "Presolve removed 7 rows and 2 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 8 rows, 8 columns, 19 nonzeros\n", + "Variable types: 0 continuous, 8 integer (8 binary)\n", + "\n", + "Root relaxation: objective 2.995750e+02, 8 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 infeasible 0 301.00000 301.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (8 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 301 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 3.010000000000e+02, best bound 3.010000000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 326, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.vertexcover import (\n", + " MinWeightVertexCoverGenerator,\n", + " build_vertexcover_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed 10-node graph,\n", + "# 25% density and random weights in the [0, 100] interval.\n", + "data = MinWeightVertexCoverGenerator(\n", + " w=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=uniform(loc=0.25, scale=0.0),\n", + " fix_graph=True,\n", + ").generate(10)\n", + "\n", + "# Print the graph and weights for two instances\n", + "print(\"graph\", data[0].graph.edges)\n", + "print(\"weights[0]\", data[0].weights)\n", + "print(\"weights[1]\", data[1].weights)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_vertexcover_model_gurobipy(data[0])\n", + "model.optimize()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/guide/solvers.ipynb.txt b/0.4/_sources/guide/solvers.ipynb.txt new file mode 100644 index 0000000..c4ee9bc --- /dev/null +++ b/0.4/_sources/guide/solvers.ipynb.txt @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "9ec1907b-db93-4840-9439-c9005902b968", + "metadata": {}, + "source": [ + "# Learning Solver\n", + "\n", + "On previous pages, we discussed various components of the MIPLearn framework, including training data collectors, feature extractors, and individual machine learning components. In this page, we introduce **LearningSolver**, the main class of the framework which integrates all the aforementioned components into a cohesive whole. Using **LearningSolver** involves three steps: (i) configuring the solver; (ii) training the ML components; and (iii) solving new MIP instances. In the following, we describe each of these steps, then conclude with a complete runnable example.\n", + "\n", + "### Configuring the solver\n", + "\n", + "**LearningSolver** is composed by multiple individual machine learning components, each targeting a different part of the solution process, or implementing a different machine learning strategy. This architecture allows strategies to be easily enabled, disabled or customized, making the framework flexible. By default, no components are provided and **LearningSolver** is equivalent to a traditional MIP solver. To specify additional components, the `components` constructor argument may be used:\n", + "\n", + "```python\n", + "solver = LearningSolver(\n", + " components=[\n", + " comp1,\n", + " comp2,\n", + " comp3,\n", + " ]\n", + ")\n", + "```\n", + "\n", + "In this example, three components `comp1`, `comp2` and `comp3` are provided. The strategies implemented by these components are applied sequentially when solving the problem. For example, `comp1` and `comp2` could fix a subset of decision variables, while `comp3` constructs a warm start for the remaining problem.\n", + "\n", + "### Training and solving new instances\n", + "\n", + "Once a solver is configured, its ML components need to be trained. This can be achieved by the `solver.fit` method, as illustrated below. The method accepts a list of HDF5 files and trains each individual component sequentially. Once the solver is trained, new instances can be solved using `solver.optimize`. The method returns a dictionary of statistics collected by each component, such as the number of variables fixed.\n", + "\n", + "```python\n", + "# Build instances\n", + "train_data = ...\n", + "test_data = ...\n", + "\n", + "# Collect training data\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_model)\n", + "\n", + "# Build solver\n", + "solver = LearningSolver(...)\n", + "\n", + "# Train components\n", + "solver.fit(train_data)\n", + "\n", + "# Solve a new test instance\n", + "stats = solver.optimize(test_data[0], build_model)\n", + "\n", + "```\n", + "\n", + "### Complete example\n", + "\n", + "In the example below, we illustrate the usage of **LearningSolver** by building instances of the Traveling Salesman Problem, collecting training data, training the ML components, then solving a new instance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "92b09b98", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x6ddcd141\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.3600000e+02 1.700000e+01 0.000000e+00 0s\n", + " 15 2.7610000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 15 iterations and 0.00 seconds (0.00 work units)\n", + "Optimal objective 2.761000000e+03\n", + "\n", + "User-callback calls 56, time in user-callback 0.00 sec\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x74ca3d0a\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "\n", + "User MIP start produced solution with objective 2796 (0.00s)\n", + "Loaded user MIP start with objective 2796\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "\n", + "Root relaxation: objective 2.761000e+03, 14 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 2761.00000 0 - 2796.00000 2761.00000 1.25% - 0s\n", + " 0 0 cutoff 0 2796.00000 2796.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (16 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 2796 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.796000000000e+03, best bound 2.796000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 110, time in user-callback 0.00 sec\n" + ] + }, + { + "data": { + "text/plain": [ + "{'WS: Count': 1, 'WS: Number of variables set': 41.0}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import random\n", + "\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from sklearn.linear_model import LogisticRegression\n", + "\n", + "from miplearn.classifiers.minprob import MinProbabilityClassifier\n", + "from miplearn.classifiers.singleclass import SingleClassFix\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.indep import IndependentVarsPrimalComponent\n", + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "# Set random seed to make example reproducible.\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a few instances of the traveling salesman problem.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(50)\n", + "\n", + "# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...\n", + "all_data = write_pkl_gz(data, \"data/tsp\")\n", + "\n", + "# Split train/test data\n", + "train_data = all_data[:40]\n", + "test_data = all_data[40:]\n", + "\n", + "# Collect training data\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_tsp_model_gurobipy, n_jobs=4)\n", + "\n", + "# Build learning solver\n", + "solver = LearningSolver(\n", + " components=[\n", + " IndependentVarsPrimalComponent(\n", + " base_clf=SingleClassFix(\n", + " MinProbabilityClassifier(\n", + " base_clf=LogisticRegression(),\n", + " thresholds=[0.95, 0.95],\n", + " ),\n", + " ),\n", + " extractor=AlvLouWeh2017Extractor(),\n", + " action=SetWarmStart(),\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Train ML models\n", + "solver.fit(train_data)\n", + "\n", + "# Solve a test instance\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e27d2cbd-5341-461d-bbc1-8131aee8d949", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/index.rst.txt b/0.4/_sources/index.rst.txt new file mode 100644 index 0000000..ebecd36 --- /dev/null +++ b/0.4/_sources/index.rst.txt @@ -0,0 +1,68 @@ +MIPLearn +======== +**MIPLearn** is an extensible framework for solving discrete optimization problems using a combination of Mixed-Integer Linear Programming (MIP) and Machine Learning (ML). MIPLearn uses ML methods to automatically identify patterns in previously solved instances of the problem, then uses these patterns to accelerate the performance of conventional state-of-the-art MIP solvers such as CPLEX, Gurobi or XPRESS. + +Unlike pure ML methods, MIPLearn is not only able to find high-quality solutions to discrete optimization problems, but it can also prove the optimality and feasibility of these solutions. Unlike conventional MIP solvers, MIPLearn can take full advantage of very specific observations that happen to be true in a particular family of instances (such as the observation that a particular constraint is typically redundant, or that a particular variable typically assumes a certain value). For certain classes of problems, this approach may provide significant performance benefits. + + +Contents +-------- + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials + :numbered: 2 + + tutorials/getting-started-pyomo + tutorials/getting-started-gurobipy + tutorials/getting-started-jump + tutorials/cuts-gurobipy + +.. toctree:: + :maxdepth: 2 + :caption: User Guide + :numbered: 2 + + guide/problems + guide/collectors + guide/features + guide/primal + guide/solvers + +.. toctree:: + :maxdepth: 1 + :caption: Python API Reference + :numbered: 2 + + api/problems + api/collectors + api/components + api/solvers + api/helpers + + +Authors +------- + +- **Alinson S. Xavier** (Argonne National Laboratory) +- **Feng Qiu** (Argonne National Laboratory) +- **Xiaoyi Gu** (Georgia Institute of Technology) +- **Berkay Becu** (Georgia Institute of Technology) +- **Santanu S. Dey** (Georgia Institute of Technology) + + +Acknowledgments +--------------- +* Based upon work supported by **Laboratory Directed Research and Development** (LDRD) funding from Argonne National Laboratory, provided by the Director, Office of Science, of the U.S. Department of Energy. +* Based upon work supported by the **U.S. Department of Energy Advanced Grid Modeling Program**. + +Citing MIPLearn +--------------- + +If you use MIPLearn in your research (either the solver or the included problem generators), we kindly request that you cite the package as follows: + +* **Alinson S. Xavier, Feng Qiu, Xiaoyi Gu, Berkay Becu, Santanu S. Dey.** *MIPLearn: An Extensible Framework for Learning-Enhanced Optimization (Version 0.3)*. Zenodo (2023). DOI: https://doi.org/10.5281/zenodo.4287567 + +If you use MIPLearn in the field of power systems optimization, we kindly request that you cite the reference below, in which the main techniques implemented in MIPLearn were first developed: + +* **Alinson S. Xavier, Feng Qiu, Shabbir Ahmed.** *Learning to Solve Large-Scale Unit Commitment Problems.* INFORMS Journal on Computing (2020). DOI: https://doi.org/10.1287/ijoc.2020.0976 diff --git a/0.4/_sources/tutorials/cuts-gurobipy.ipynb.txt b/0.4/_sources/tutorials/cuts-gurobipy.ipynb.txt new file mode 100644 index 0000000..ffdc13d --- /dev/null +++ b/0.4/_sources/tutorials/cuts-gurobipy.ipynb.txt @@ -0,0 +1,541 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b4bd8bd6-3ce9-4932-852f-f98a44120a3e", + "metadata": {}, + "source": [ + "# User cuts and lazy constraints\n", + "\n", + "User cuts and lazy constraints are two advanced mixed-integer programming techniques that can accelerate solver performance. User cuts are additional constraints, derived from the constraints already in the model, that can tighten the feasible region and eliminate fractional solutions, thus reducing the size of the branch-and-bound tree. Lazy constraints, on the other hand, are constraints that are potentially part of the problem formulation but are omitted from the initial model to reduce its size; these constraints are added to the formulation only once the solver finds a solution that violates them. While both techniques have been successful, significant computational effort may still be required to generate strong user cuts and to identify violated lazy constraints, which can reduce their effectiveness.\n", + "\n", + "MIPLearn is able to predict which user cuts and which lazy constraints to enforce at the beginning of the optimization process, using machine learning. In this tutorial, we will use the framework to predict subtour elimination constraints for the **traveling salesman problem** using Gurobipy. We assume that MIPLearn has already been correctly installed.\n", + "\n", + "
\n", + "\n", + "Solver Compatibility\n", + "\n", + "User cuts and lazy constraints are also supported in the Python/Pyomo and Julia/JuMP versions of the package. See the source code of build_tsp_model_pyomo and build_tsp_model_jump for more details. Note, however, the following limitations:\n", + "\n", + "- Python/Pyomo: Only `gurobi_persistent` is currently supported. PRs implementing callbacks for other persistent solvers are welcome.\n", + "- Julia/JuMP: Only solvers supporting solver-independent callbacks are supported. As of JuMP 1.19, this includes Gurobi, CPLEX, XPRESS, SCIP and GLPK. Note that HiGHS and Cbc are not supported. As newer versions of JuMP implement further callback support, MIPLearn should become automatically compatible with these solvers.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "72229e1f-cbd8-43f0-82ee-17d6ec9c3b7d", + "metadata": {}, + "source": [ + "## Modeling the traveling salesman problem\n", + "\n", + "Given a list of cities and the distances between them, the **traveling salesman problem (TSP)** asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp's 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes.\n", + "\n", + "To describe an instance of TSP, we need to specify the number of cities $n$, and an $n \\times n$ matrix of distances. The class `TravelingSalesmanData`, in the `miplearn.problems.tsp` package, can hold this data:" + ] + }, + { + "cell_type": "markdown", + "id": "4598a1bc-55b6-48cc-a050-2262786c203a", + "metadata": {}, + "source": [ + "```python\n", + "@dataclass\r\n", + "class TravelingSalesmanData:\r\n", + " n_cities: int\r\n", + " distances: np.ndarray\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3a43cc12-1207-4247-bdb2-69a6a2910738", + "metadata": {}, + "source": [ + "MIPLearn also provides `TravelingSalesmandGenerator`, a random generator for TSP instances, and `build_tsp_model_gurobipy`, a function which converts `TravelingSalesmanData` into an actual gurobipy optimization model, and which uses lazy constraints to enforce subtour elimination.\n", + "\n", + "The example below is a simplified and annotated version of `build_tsp_model_gurobipy`, illustrating the usage of callbacks with MIPLearn. Compared the the previous tutorial examples, note that, in addition to defining the variables, objective function and constraints of our problem, we also define two callback functions `lazy_separate` and `lazy_enforce`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e4712a85-0327-439c-8889-933e1ff714e7", + "metadata": {}, + "outputs": [], + "source": [ + "import gurobipy as gp\n", + "from gurobipy import quicksum, GRB, tuplelist\n", + "from miplearn.solvers.gurobi import GurobiModel\n", + "import networkx as nx\n", + "import numpy as np\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanData,\n", + " TravelingSalesmanGenerator,\n", + ")\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.io import write_pkl_gz, read_pkl_gz\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.solvers.learning import LearningSolver\n", + "from miplearn.components.lazy.mem import MemorizingLazyComponent\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "\n", + "# Set up random seed to make example more reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Set up Python logging\n", + "import logging\n", + "\n", + "logging.basicConfig(level=logging.WARNING)\n", + "\n", + "\n", + "def build_tsp_model_gurobipy_simplified(data):\n", + " # Read data from file if a filename is provided\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " # Create empty gurobipy model\n", + " model = gp.Model()\n", + "\n", + " # Create set of edges between every pair of cities, for convenience\n", + " edges = tuplelist(\n", + " (i, j) for i in range(data.n_cities) for j in range(i + 1, data.n_cities)\n", + " )\n", + "\n", + " # Add binary variable x[e] for each edge e\n", + " x = model.addVars(edges, vtype=GRB.BINARY, name=\"x\")\n", + "\n", + " # Add objective function\n", + " model.setObjective(quicksum(x[(i, j)] * data.distances[i, j] for (i, j) in edges))\n", + "\n", + " # Add constraint: must choose two edges adjacent to each city\n", + " model.addConstrs(\n", + " (\n", + " quicksum(x[min(i, j), max(i, j)] for j in range(data.n_cities) if i != j)\n", + " == 2\n", + " for i in range(data.n_cities)\n", + " ),\n", + " name=\"eq_degree\",\n", + " )\n", + "\n", + " def lazy_separate(m: GurobiModel):\n", + " \"\"\"\n", + " Callback function that finds subtours in the current solution.\n", + " \"\"\"\n", + " # Query current value of the x variables\n", + " x_val = m.inner.cbGetSolution(x)\n", + "\n", + " # Initialize empty set of violations\n", + " violations = []\n", + "\n", + " # Build set of edges we have currently selected\n", + " selected_edges = [e for e in edges if x_val[e] > 0.5]\n", + "\n", + " # Build a graph containing the selected edges, using networkx\n", + " graph = nx.Graph()\n", + " graph.add_edges_from(selected_edges)\n", + "\n", + " # For each component of the graph\n", + " for component in list(nx.connected_components(graph)):\n", + "\n", + " # If the component is not the entire graph, we found a\n", + " # subtour. Add the edge cut to the list of violations.\n", + " if len(component) < data.n_cities:\n", + " cut_edges = [\n", + " [e[0], e[1]]\n", + " for e in edges\n", + " if (e[0] in component and e[1] not in component)\n", + " or (e[0] not in component and e[1] in component)\n", + " ]\n", + " violations.append(cut_edges)\n", + "\n", + " # Return the list of violations\n", + " return violations\n", + "\n", + " def lazy_enforce(m: GurobiModel, violations) -> None:\n", + " \"\"\"\n", + " Callback function that, given a list of subtours, adds lazy\n", + " constraints to remove them from the feasible region.\n", + " \"\"\"\n", + " print(f\"Enforcing {len(violations)} subtour elimination constraints\")\n", + " for violation in violations:\n", + " m.add_constr(quicksum(x[e[0], e[1]] for e in violation) >= 2)\n", + "\n", + " return GurobiModel(\n", + " model,\n", + " lazy_separate=lazy_separate,\n", + " lazy_enforce=lazy_enforce,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "58875042-d6ac-4f93-b3cc-9a5822b11dad", + "metadata": {}, + "source": [ + "The `lazy_separate` function starts by querying the current fractional solution value through `m.inner.cbGetSolution` (recall that `m.inner` is a regular gurobipy model), then finds the set of violated lazy constraints. Unlike a regular lazy constraint solver callback, note that `lazy_separate` does not add the violated constraints to the model; it simply returns a list of objects that uniquely identifies the set of lazy constraints that should be generated. Enforcing the constraints is the responsbility of the second callback function, `lazy_enforce`. This function takes as input the model and the list of violations found by `lazy_separate`, converts them into actual constraints, and adds them to the model through `m.add_constr`.\n", + "\n", + "During training data generation, MIPLearn calls `lazy_separate` and `lazy_enforce` in sequence, inside a regular solver callback. However, once the machine learning models are trained, MIPLearn calls `lazy_enforce` directly, before the optimization process starts, with a list of **predicted** violations, as we will see in the example below." + ] + }, + { + "cell_type": "markdown", + "id": "5839728e-406c-4be2-ba81-83f2b873d4b2", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Constraint Representation\n", + "\n", + "How should user cuts and lazy constraints be represented is a decision that the user can make; MIPLearn is representation agnostic. The objects returned by `lazy_separate`, however, are serialized as JSON and stored in the HDF5 training data files. Therefore, it is recommended to use only simple objects, such as lists, tuples and dictionaries.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "847ae32e-fad7-406a-8797-0d79065a07fd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "To test the callback defined above, we generate a small set of TSP instances, using the provided random instance generator. As in the previous tutorial, we generate some test instances and some training instances, then solve them using `BasicCollector`. Input problem data is stored in `tsp/train/00000.pkl.gz, ...`, whereas solver training data (including list of required lazy constraints) is stored in `tsp/train/00000.h5, ...`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eb63154a-1fa6-4eac-aa46-6838b9c201f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Configure generator to produce instances with 50 cities located\n", + "# in the 1000 x 1000 square, and with slightly perturbed distances.\n", + "gen = TravelingSalesmanGenerator(\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " n=randint(low=50, high=51),\n", + " gamma=uniform(loc=1.0, scale=0.25),\n", + " fix_cities=True,\n", + " round=True,\n", + ")\n", + "\n", + "# Generate 500 instances and store input data file to .pkl.gz files\n", + "data = gen.generate(500)\n", + "train_data = write_pkl_gz(data[0:450], \"tsp/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"tsp/test\")\n", + "\n", + "# Solve the training instances in parallel, collecting the required lazy\n", + "# constraints, in addition to other information, such as optimal solution.\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_tsp_model_gurobipy_simplified, n_jobs=10)" + ] + }, + { + "cell_type": "markdown", + "id": "6903c26c-dbe0-4a2e-bced-fdbf93513dde", + "metadata": {}, + "source": [ + "## Training and solving new instances" + ] + }, + { + "cell_type": "markdown", + "id": "57cd724a-2d27-4698-a1e6-9ab8345ef31f", + "metadata": {}, + "source": [ + "After producing the training dataset, we can train the machine learning models to predict which lazy constraints are necessary. In this tutorial, we use the following ML strategy: given a new instance, find the 50 most similar ones in the training dataset and verify how often each lazy constraint was required. If a lazy constraint was required for the majority of the 50 most-similar instances, enforce it ahead-of-time for the current instance. To measure instance similarity, use the objective function only. This ML strategy can be implemented using `MemorizingLazyComponent` with `H5FieldsExtractor` and `KNeighborsClassifier`, as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "43779e3d-4174-4189-bc75-9f564910e212", + "metadata": {}, + "outputs": [], + "source": [ + "solver = LearningSolver(\n", + " components=[\n", + " MemorizingLazyComponent(\n", + " extractor=H5FieldsExtractor(instance_fields=[\"static_var_obj_coeffs\"]),\n", + " clf=KNeighborsClassifier(n_neighbors=100),\n", + " ),\n", + " ],\n", + ")\n", + "solver.fit(train_data)" + ] + }, + { + "cell_type": "markdown", + "id": "12480712-9d3d-4cbc-a6d7-d6c1e2f950f4", + "metadata": {}, + "source": [ + "Next, we solve one of the test instances using the trained solver. In the run below, we can see that MIPLearn adds many lazy constraints ahead-of-time, before the optimization starts. During the optimization process itself, some additional lazy constraints are required, but very few." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "23f904ad-f1a8-4b5a-81ae-c0b9e813a4b2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter Threads to value 1\n", + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x04d7bec1\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 4.0600000e+02 9.700000e+01 0.000000e+00 0s\n", + " 66 5.5880000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 66 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 5.588000000e+03\n", + "\n", + "User-callback calls 107, time in user-callback 0.00 sec\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:miplearn.components.cuts.mem:Predicting violated lazy constraints...\n", + "INFO:miplearn.components.lazy.mem:Enforcing 19 constraints ahead-of-time...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Enforcing 19 subtour elimination constraints\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 69 rows, 1225 columns and 6091 nonzeros\n", + "Model fingerprint: 0x09bd34d6\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Found heuristic solution: objective 29853.000000\n", + "Presolve time: 0.00s\n", + "Presolved: 69 rows, 1225 columns, 6091 nonzeros\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "\n", + "Root relaxation: objective 6.139000e+03, 93 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 6139.00000 0 6 29853.0000 6139.00000 79.4% - 0s\n", + "H 0 0 6390.0000000 6139.00000 3.93% - 0s\n", + " 0 0 6165.50000 0 10 6390.00000 6165.50000 3.51% - 0s\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6165.50000 0 6 6390.00000 6165.50000 3.51% - 0s\n", + " 0 0 6198.50000 0 16 6390.00000 6198.50000 3.00% - 0s\n", + "* 0 0 0 6219.0000000 6219.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 11\n", + " MIR: 1\n", + " Zero half: 4\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (222 simplex iterations) in 0.03 seconds (0.02 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 3: 6219 6390 29853 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 141, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "# Increase log verbosity, so that we can see what is MIPLearn doing\n", + "logging.getLogger(\"miplearn\").setLevel(logging.INFO)\n", + "\n", + "# Solve a new test instance\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);" + ] + }, + { + "cell_type": "markdown", + "id": "79cc3e61-ee2b-4f18-82cb-373d55d67de6", + "metadata": {}, + "source": [ + "Finally, we solve the same instance, but using a regular solver, without ML prediction. We can see that a much larger number of lazy constraints are added during the optimization process itself. Additionally, the solver requires a larger number of iterations to find the optimal solution. There is not a significant difference in running time because of the small size of these instances." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a015c51c-091a-43b6-b761-9f3577fc083e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x04d7bec1\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 4.0600000e+02 9.700000e+01 0.000000e+00 0s\n", + " 66 5.5880000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 66 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 5.588000000e+03\n", + "\n", + "User-callback calls 107, time in user-callback 0.00 sec\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x77a94572\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Found heuristic solution: objective 29695.000000\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "\n", + "Root relaxation: objective 5.588000e+03, 68 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 5588.00000 0 12 29695.0000 5588.00000 81.2% - 0s\n", + "Enforcing 9 subtour elimination constraints\n", + "Enforcing 11 subtour elimination constraints\n", + "H 0 0 27241.000000 5588.00000 79.5% - 0s\n", + " 0 0 5898.00000 0 8 27241.0000 5898.00000 78.3% - 0s\n", + "Enforcing 4 subtour elimination constraints\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6066.00000 0 - 27241.0000 6066.00000 77.7% - 0s\n", + "Enforcing 2 subtour elimination constraints\n", + " 0 0 6128.00000 0 - 27241.0000 6128.00000 77.5% - 0s\n", + " 0 0 6139.00000 0 6 27241.0000 6139.00000 77.5% - 0s\n", + "H 0 0 6368.0000000 6139.00000 3.60% - 0s\n", + " 0 0 6154.75000 0 15 6368.00000 6154.75000 3.35% - 0s\n", + "Enforcing 2 subtour elimination constraints\n", + " 0 0 6154.75000 0 6 6368.00000 6154.75000 3.35% - 0s\n", + " 0 0 6165.75000 0 11 6368.00000 6165.75000 3.18% - 0s\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6204.00000 0 6 6368.00000 6204.00000 2.58% - 0s\n", + "* 0 0 0 6219.0000000 6219.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 5\n", + " MIR: 1\n", + " Zero half: 4\n", + " Lazy constraints: 4\n", + "\n", + "Explored 1 nodes (224 simplex iterations) in 0.10 seconds (0.03 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 4: 6219 6368 27241 29695 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 170, time in user-callback 0.01 sec\n" + ] + } + ], + "source": [ + "solver = LearningSolver(components=[]) # empty set of ML components\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);" + ] + }, + { + "cell_type": "markdown", + "id": "432c99b2-67fe-409b-8224-ccef91de96d1", + "metadata": {}, + "source": [ + "## Learning user cuts\n", + "\n", + "The example above focused on lazy constraints. To enforce user cuts instead, the procedure is very similar, with the following changes:\n", + "\n", + "- Instead of `lazy_separate` and `lazy_enforce`, use `cuts_separate` and `cuts_enforce`\n", + "- Instead of `m.inner.cbGetSolution`, use `m.inner.cbGetNodeRel`\n", + "\n", + "For a complete example, see `build_stab_model_gurobipy`, `build_stab_model_pyomo` and `build_stab_model_jump`, which solves the maximum-weight stable set problem using user cut callbacks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6cb694d-8c43-410f-9a13-01bf9e0763b7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/tutorials/getting-started-gurobipy.ipynb.txt b/0.4/_sources/tutorials/getting-started-gurobipy.ipynb.txt new file mode 100644 index 0000000..110e3f4 --- /dev/null +++ b/0.4/_sources/tutorials/getting-started-gurobipy.ipynb.txt @@ -0,0 +1,837 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (Gurobipy)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Python/Gurobipy version of MIPLearn\n", + "2. Model a simple optimization problem using Gurobipy\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
\n", + "Note\n", + " \n", + "The Python/Gurobipy version of MIPLearn is only compatible with the Gurobi Optimizer. For broader solver compatibility, see the Python/Pyomo and Julia/JuMP versions of the package.\n", + "
\n", + "\n", + "
\n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Gurobipy version of the package. The first step is to install Python 3.8+ in your computer. See the [official Python website for more instructions](https://www.python.org/downloads/). After Python is installed, we proceed to install MIPLearn using `pip`:\n", + "\n", + "```\n", + "$ pip install MIPLearn==0.3\n", + "```\n", + "\n", + "In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.\n", + "\n", + "```\n", + "$ pip install 'gurobipy>=10,<10.1'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Note\n", + " \n", + "In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "22a67170-10b4-43d3-8708-014d91141e73", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:18:25.442346786Z", + "start_time": "2023-06-06T20:18:25.329017476Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "import numpy as np\n", + "\n", + "\n", + "@dataclass\n", + "class UnitCommitmentData:\n", + " demand: float\n", + " pmin: List[float]\n", + " pmax: List[float]\n", + " cfix: List[float]\n", + " cvar: List[float]" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete Pyomo model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a compressed pickle file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2f67032f-0d74-4317-b45c-19da0ec859e9", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:48:05.953902842Z", + "start_time": "2023-06-06T20:48:05.909747925Z" + } + }, + "outputs": [], + "source": [ + "import gurobipy as gp\n", + "from gurobipy import GRB, quicksum\n", + "from typing import Union\n", + "from miplearn.io import read_pkl_gz\n", + "from miplearn.solvers.gurobi import GurobiModel\n", + "\n", + "\n", + "def build_uc_model(data: Union[str, UnitCommitmentData]) -> GurobiModel:\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " model = gp.Model()\n", + " n = len(data.pmin)\n", + " x = model._x = model.addVars(n, vtype=GRB.BINARY, name=\"x\")\n", + " y = model._y = model.addVars(n, name=\"y\")\n", + " model.setObjective(\n", + " quicksum(data.cfix[i] * x[i] + data.cvar[i] * y[i] for i in range(n))\n", + " )\n", + " model.addConstrs(y[i] <= data.pmax[i] * x[i] for i in range(n))\n", + " model.addConstrs(y[i] >= data.pmin[i] * x[i] for i in range(n))\n", + " model.addConstr(quicksum(y[i] for i in range(n)) == data.demand)\n", + " return GurobiModel(model)" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2a896f47", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:14.266758244Z", + "start_time": "2023-06-06T20:49:14.223514806Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x58dfdd53\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "obj = 1320.0\n", + "x = [-0.0, 1.0, 1.0]\n", + "y = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " demand=100.0,\n", + " pmin=[10, 20, 30],\n", + " pmax=[50, 60, 70],\n", + " cfix=[700, 600, 500],\n", + " cvar=[1.5, 2.0, 2.5],\n", + " )\n", + ")\n", + "\n", + "model.optimize()\n", + "print(\"obj =\", model.inner.objVal)\n", + "print(\"x =\", [model.inner._x[i].x for i in range(3)])\n", + "print(\"y =\", [model.inner._y[i].x for i in range(3)])" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Note\n", + "\n", + "- In the example above, `GurobiModel` is just a thin wrapper around a standard Gurobi model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original Gurobi model can be accessed through `model.inner`, as illustrated above.\n", + "- To ensure training data consistency, MIPLearn requires all decision variables to have names.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5eb09fab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:22.758192368Z", + "start_time": "2023-06-06T20:49:22.724784572Z" + } + }, + "outputs": [], + "source": [ + "from scipy.stats import uniform\n", + "from typing import List\n", + "import random\n", + "\n", + "\n", + "def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)\n", + " pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)\n", + " cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)\n", + " cvar = uniform(loc=1.25, scale=0.25).rvs(n)\n", + " return [\n", + " UnitCommitmentData(\n", + " demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),\n", + " pmin=pmin,\n", + " pmax=pmax,\n", + " cfix=cfix,\n", + " cvar=cvar,\n", + " )\n", + " for _ in range(samples)\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00000.pkl.gz`, `uc/train/00001.pkl.gz`, etc., which contain the input data in compressed (gzipped) pickle format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:24.811192929Z", + "start_time": "2023-06-06T20:49:24.575639142Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.io import write_pkl_gz\n", + "\n", + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_pkl_gz(data[0:450], \"uc/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"uc/test\")" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00000.h5`, `uc/train/00001.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00000.mps.gz`, `uc/train/00001.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:34.936729253Z", + "start_time": "2023-06-06T20:49:25.936126612Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_uc_model, n_jobs=4)" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:38.997939600Z", + "start_time": "2023-06-06T20:49:38.968261432Z" + } + }, + "outputs": [], + "source": [ + "from sklearn.neighbors import KNeighborsClassifier\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:42.072345411Z", + "start_time": "2023-06-06T20:49:41.294040974Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xa8b70287\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.01s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xcf27855a\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.29153e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 8.2915e+09 8.2906e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 3 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2915e+09 8.2907e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 1\n", + " Flow cover: 2\n", + "\n", + "Explored 1 nodes (565 simplex iterations) in 0.03 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 8.29153e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%\n" + ] + }, + { + "data": { + "text/plain": [ + "{'WS: Count': 1, 'WS: Number of variables set': 482.0}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:44.012782276Z", + "start_time": "2023-06-06T20:49:43.813974362Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xa8b70287\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x4cbbf7c7\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 9.757128e+09\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 9.7571e+09 8.2906e+09 15.0% - 0s\n", + "H 0 0 8.298273e+09 8.2906e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + "H 0 0 8.293980e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 5 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 1 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + "H 0 0 8.291465e+09 8.2908e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 2\n", + " MIR: 1\n", + "\n", + "Explored 1 nodes (1031 simplex iterations) in 0.15 seconds (0.03 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:50:12.869892930Z", + "start_time": "2023-06-06T20:50:12.509410473Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x19042f12\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.5917580e+09 5.627453e+04 0.000000e+00 0s\n", + " 1 8.2535968e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.253596777e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xf97cde91\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.25814e+09 (0.00s)\n", + "User MIP start produced solution with objective 8.25512e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.25459e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2536e+09 0 1 8.2546e+09 8.2536e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 3 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 1 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 4 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 5 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 6 8.2546e+09 8.2538e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + " MIR: 2\n", + " StrongCG: 1\n", + " Flow cover: 1\n", + "\n", + "Explored 1 nodes (575 simplex iterations) in 0.05 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%\n", + "obj = 8254590409.969726\n", + "x = [1.0, 1.0, 0.0]\n", + "y = [935662.0949262811, 1604270.0218116897, 0.0]\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[0]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "print(\"obj =\", model.inner.objVal)\n", + "print(\"x =\", [model.inner._x[i].x for i in range(3)])\n", + "print(\"y =\", [model.inner._y[i].x for i in range(3)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5593d23a-83bd-4e16-8253-6300f5e3f63b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/tutorials/getting-started-jump.ipynb.txt b/0.4/_sources/tutorials/getting-started-jump.ipynb.txt new file mode 100644 index 0000000..8dbf587 --- /dev/null +++ b/0.4/_sources/tutorials/getting-started-jump.ipynb.txt @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (JuMP)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Julia/JuMP version of MIPLearn\n", + "2. Model a simple optimization problem using JuMP\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
\n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Julia in your machine. See the [official Julia website for more instructions](https://julialang.org/downloads/). After Julia is installed, launch the Julia REPL, type `]` to enter package mode, then install MIPLearn:\n", + "\n", + "```\n", + "pkg> add MIPLearn@0.3\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "e8274543", + "metadata": {}, + "source": [ + "In addition to MIPLearn itself, we will also install:\n", + "\n", + "- the JuMP modeling language\n", + "- Gurobi, a state-of-the-art commercial MILP solver\n", + "- Distributions, to generate random data\n", + "- PyCall, to access ML model from Scikit-Learn\n", + "- Suppressor, to make the output cleaner\n", + "\n", + "```\n", + "pkg> add JuMP@1, Gurobi@1, Distributions@0.25, PyCall@1, Suppressor@0.2\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Note\n", + "\n", + "- If you do not have a Gurobi license available, you can also follow the tutorial by installing an open-source solver, such as `HiGHS`, and replacing `Gurobi.Optimizer` by `HiGHS.Optimizer` in all the code examples.\n", + "- In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Julia projects.\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Julia and JuMP. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c62ebff1-db40-45a1-9997-d121837f067b", + "metadata": {}, + "outputs": [], + "source": [ + "struct UnitCommitmentData\n", + " demand::Float64\n", + " pmin::Vector{Float64}\n", + " pmax::Vector{Float64}\n", + " cfix::Vector{Float64}\n", + " cvar::Vector{Float64}\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete JuMP model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a JLD2 file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "79ef7775-18ca-4dfa-b438-49860f762ad0", + "metadata": {}, + "outputs": [], + "source": [ + "using MIPLearn\n", + "using JuMP\n", + "using Gurobi\n", + "\n", + "function build_uc_model(data)\n", + " if data isa String\n", + " data = read_jld2(data)\n", + " end\n", + " model = Model(Gurobi.Optimizer)\n", + " G = 1:length(data.pmin)\n", + " @variable(model, x[G], Bin)\n", + " @variable(model, y[G] >= 0)\n", + " @objective(model, Min, sum(data.cfix[g] * x[g] + data.cvar[g] * y[g] for g in G))\n", + " @constraint(model, eq_max_power[g in G], y[g] <= data.pmax[g] * x[g])\n", + " @constraint(model, eq_min_power[g in G], y[g] >= data.pmin[g] * x[g])\n", + " @constraint(model, eq_demand, sum(y[g] for g in G) == data.demand)\n", + " return JumpModel(model)\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Gurobi to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "dd828d68-fd43-4d2a-a058-3e2628d99d9e", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:01:10.993801745Z", + "start_time": "2023-06-06T20:01:10.887580927Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x55e33a07\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 371, time in user-callback 0.00 sec\n", + "objective_value(model.inner) = 1320.0\n", + "Vector(value.(model.inner[:x])) = [-0.0, 1.0, 1.0]\n", + "Vector(value.(model.inner[:y])) = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " 100.0, # demand\n", + " [10, 20, 30], # pmin\n", + " [50, 60, 70], # pmax\n", + " [700, 600, 500], # cfix\n", + " [1.5, 2.0, 2.5], # cvar\n", + " )\n", + ")\n", + "model.optimize()\n", + "@show objective_value(model.inner)\n", + "@show Vector(value.(model.inner[:x]))\n", + "@show Vector(value.(model.inner[:y]));" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Notes\n", + " \n", + "- In the example above, `JumpModel` is just a thin wrapper around a standard JuMP model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original JuMP model can be accessed through `model.inner`, as illustrated above.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1326efd7-3869-4137-ab6b-df9cb609a7e0", + "metadata": {}, + "outputs": [], + "source": [ + "using Distributions\n", + "using Random\n", + "\n", + "function random_uc_data(; samples::Int, n::Int, seed::Int=42)::Vector\n", + " Random.seed!(seed)\n", + " pmin = rand(Uniform(100_000, 500_000), n)\n", + " pmax = pmin .* rand(Uniform(2, 2.5), n)\n", + " cfix = pmin .* rand(Uniform(100, 125), n)\n", + " cvar = rand(Uniform(1.25, 1.50), n)\n", + " return [\n", + " UnitCommitmentData(\n", + " sum(pmax) * rand(Uniform(0.5, 0.75)),\n", + " pmin,\n", + " pmax,\n", + " cfix,\n", + " cvar,\n", + " )\n", + " for _ in 1:samples\n", + " ]\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00001.jld2`, `uc/train/00002.jld2`, etc., which contain the input data in JLD2 format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:04.782830561Z", + "start_time": "2023-06-06T20:03:04.530421396Z" + } + }, + "outputs": [], + "source": [ + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_jld2(data[1:450], \"uc/train\")\n", + "test_data = write_jld2(data[451:500], \"uc/test\");" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00001.h5`, `uc/train/00002.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00001.mps.gz`, `uc/train/00002.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:35.571497019Z", + "start_time": "2023-06-06T20:03:25.804104036Z" + } + }, + "outputs": [], + "source": [ + "using Suppressor\n", + "@suppress_out begin\n", + " bc = BasicCollector()\n", + " bc.collect(train_data, build_uc_model)\n", + "end" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:20.497772794Z", + "start_time": "2023-06-06T20:05:20.484821405Z" + } + }, + "outputs": [], + "source": [ + "# Load kNN classifier from Scikit-Learn\n", + "using PyCall\n", + "KNeighborsClassifier = pyimport(\"sklearn.neighbors\").KNeighborsClassifier\n", + "\n", + "# Build the MIPLearn component\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ");" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:22.672002339Z", + "start_time": "2023-06-06T20:05:21.447466634Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xd2378195\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "\n", + "User MIP start produced solution with objective 1.02165e+10 (0.00s)\n", + "Loaded user MIP start with objective 1.02165e+10\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.0216e+10 0 1 1.0217e+10 1.0216e+10 0.01% - 0s\n", + "\n", + "Explored 1 nodes (510 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 1: 1.02165e+10 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.021651058978e+10, best bound 1.021567971257e+10, gap 0.0081%\n", + "\n", + "User-callback calls 169, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[1], build_uc_model);" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:46.969575966Z", + "start_time": "2023-06-06T20:05:46.420803286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xb45c0594\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 1.071463e+10\n", + "\n", + "Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.0216e+10 0 1 1.0715e+10 1.0216e+10 4.66% - 0s\n", + "H 0 0 1.025162e+10 1.0216e+10 0.35% - 0s\n", + " 0 0 1.0216e+10 0 1 1.0252e+10 1.0216e+10 0.35% - 0s\n", + "H 0 0 1.023090e+10 1.0216e+10 0.15% - 0s\n", + "H 0 0 1.022335e+10 1.0216e+10 0.07% - 0s\n", + "H 0 0 1.022281e+10 1.0216e+10 0.07% - 0s\n", + "H 0 0 1.021753e+10 1.0216e+10 0.02% - 0s\n", + "H 0 0 1.021752e+10 1.0216e+10 0.02% - 0s\n", + " 0 0 1.0216e+10 0 3 1.0218e+10 1.0216e+10 0.02% - 0s\n", + " 0 0 1.0216e+10 0 1 1.0218e+10 1.0216e+10 0.02% - 0s\n", + "H 0 0 1.021651e+10 1.0216e+10 0.01% - 0s\n", + "\n", + "Explored 1 nodes (764 simplex iterations) in 0.03 seconds (0.02 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 7: 1.02165e+10 1.02175e+10 1.02228e+10 ... 1.07146e+10\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.021651058978e+10, best bound 1.021573363741e+10, gap 0.0076%\n", + "\n", + "User-callback calls 204, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[1], build_uc_model);" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a JuMP model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:06:26.913448568Z", + "start_time": "2023-06-06T20:06:26.169047914Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x974a7fba\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "\n", + "User MIP start produced solution with objective 9.86729e+09 (0.00s)\n", + "User MIP start produced solution with objective 9.86675e+09 (0.00s)\n", + "User MIP start produced solution with objective 9.86654e+09 (0.01s)\n", + "User MIP start produced solution with objective 9.8661e+09 (0.01s)\n", + "Loaded user MIP start with objective 9.8661e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 9.865344e+09, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 9.8653e+09 0 1 9.8661e+09 9.8653e+09 0.01% - 0s\n", + "\n", + "Explored 1 nodes (510 simplex iterations) in 0.02 seconds (0.01 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 4: 9.8661e+09 9.86654e+09 9.86675e+09 9.86729e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 9.866096485614e+09, best bound 9.865343669936e+09, gap 0.0076%\n", + "\n", + "User-callback calls 182, time in user-callback 0.00 sec\n", + "objective_value(model.inner) = 9.866096485613789e9\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[1]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "@show objective_value(model.inner);" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.9.0", + "language": "julia", + "name": "julia-1.9" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_sources/tutorials/getting-started-pyomo.ipynb.txt b/0.4/_sources/tutorials/getting-started-pyomo.ipynb.txt new file mode 100644 index 0000000..e109ddb --- /dev/null +++ b/0.4/_sources/tutorials/getting-started-pyomo.ipynb.txt @@ -0,0 +1,858 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (Pyomo)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Python/Pyomo version of MIPLearn\n", + "2. Model a simple optimization problem using Pyomo\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
\n", + "Note\n", + " \n", + "The Python/Pyomo version of MIPLearn is currently only compatible with Pyomo persistent solvers (Gurobi, CPLEX and XPRESS). For broader solver compatibility, see the Julia/JuMP version of the package.\n", + "
\n", + "\n", + "
\n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Python 3.8+ in your computer. See the [official Python website for more instructions](https://www.python.org/downloads/). After Python is installed, we proceed to install MIPLearn using `pip`:\n", + "\n", + "```\n", + "$ pip install MIPLearn==0.3\n", + "```\n", + "\n", + "In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.\n", + "\n", + "```\n", + "$ pip install 'gurobipy>=10,<10.1'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Note\n", + " \n", + "In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.\n", + " \n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "22a67170-10b4-43d3-8708-014d91141e73", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:00:03.278853343Z", + "start_time": "2023-06-06T20:00:03.123324067Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "import numpy as np\n", + "\n", + "\n", + "@dataclass\n", + "class UnitCommitmentData:\n", + " demand: float\n", + " pmin: List[float]\n", + " pmax: List[float]\n", + " cfix: List[float]\n", + " cvar: List[float]" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete Pyomo model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a compressed pickle file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2f67032f-0d74-4317-b45c-19da0ec859e9", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:00:45.890126754Z", + "start_time": "2023-06-06T20:00:45.637044282Z" + } + }, + "outputs": [], + "source": [ + "import pyomo.environ as pe\n", + "from typing import Union\n", + "from miplearn.io import read_pkl_gz\n", + "from miplearn.solvers.pyomo import PyomoModel\n", + "\n", + "\n", + "def build_uc_model(data: Union[str, UnitCommitmentData]) -> PyomoModel:\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " model = pe.ConcreteModel()\n", + " n = len(data.pmin)\n", + " model.x = pe.Var(range(n), domain=pe.Binary)\n", + " model.y = pe.Var(range(n), domain=pe.NonNegativeReals)\n", + " model.obj = pe.Objective(\n", + " expr=sum(\n", + " data.cfix[i] * model.x[i] + data.cvar[i] * model.y[i] for i in range(n)\n", + " )\n", + " )\n", + " model.eq_max_power = pe.ConstraintList()\n", + " model.eq_min_power = pe.ConstraintList()\n", + " for i in range(n):\n", + " model.eq_max_power.add(model.y[i] <= data.pmax[i] * model.x[i])\n", + " model.eq_min_power.add(model.y[i] >= data.pmin[i] * model.x[i])\n", + " model.eq_demand = pe.Constraint(\n", + " expr=sum(model.y[i] for i in range(n)) == data.demand,\n", + " )\n", + " return PyomoModel(model, \"gurobi_persistent\")" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2a896f47", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:01:10.993801745Z", + "start_time": "2023-06-06T20:01:10.887580927Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x15c7a953\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n", + "obj = 1320.0\n", + "x = [-0.0, 1.0, 1.0]\n", + "y = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " demand=100.0,\n", + " pmin=[10, 20, 30],\n", + " pmax=[50, 60, 70],\n", + " cfix=[700, 600, 500],\n", + " cvar=[1.5, 2.0, 2.5],\n", + " )\n", + ")\n", + "\n", + "model.optimize()\n", + "print(\"obj =\", model.inner.obj())\n", + "print(\"x =\", [model.inner.x[i].value for i in range(3)])\n", + "print(\"y =\", [model.inner.y[i].value for i in range(3)])" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
\n", + " \n", + "Notes\n", + " \n", + "- In the example above, `PyomoModel` is just a thin wrapper around a standard Pyomo model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original Pyomo model can be accessed through `model.inner`, as illustrated above. \n", + "- To use CPLEX or XPRESS, instead of Gurobi, replace `gurobi_persistent` by `cplex_persistent` or `xpress_persistent` in the `build_uc_model`. Note that only persistent Pyomo solvers are currently supported. Pull requests adding support for other types of solver are very welcome.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5eb09fab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:02:27.324208900Z", + "start_time": "2023-06-06T20:02:26.990044230Z" + } + }, + "outputs": [], + "source": [ + "from scipy.stats import uniform\n", + "from typing import List\n", + "import random\n", + "\n", + "\n", + "def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)\n", + " pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)\n", + " cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)\n", + " cvar = uniform(loc=1.25, scale=0.25).rvs(n)\n", + " return [\n", + " UnitCommitmentData(\n", + " demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),\n", + " pmin=pmin,\n", + " pmax=pmax,\n", + " cfix=cfix,\n", + " cvar=cvar,\n", + " )\n", + " for _ in range(samples)\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00000.pkl.gz`, `uc/train/00001.pkl.gz`, etc., which contain the input data in compressed (gzipped) pickle format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:04.782830561Z", + "start_time": "2023-06-06T20:03:04.530421396Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.io import write_pkl_gz\n", + "\n", + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_pkl_gz(data[0:450], \"uc/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"uc/test\")" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00000.h5`, `uc/train/00001.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00000.mps.gz`, `uc/train/00001.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:35.571497019Z", + "start_time": "2023-06-06T20:03:25.804104036Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_uc_model, n_jobs=4)" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:20.497772794Z", + "start_time": "2023-06-06T20:05:20.484821405Z" + } + }, + "outputs": [], + "source": [ + "from sklearn.neighbors import KNeighborsClassifier\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:22.672002339Z", + "start_time": "2023-06-06T20:05:21.447466634Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x5e67c6ee\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x4a7cfe2b\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.29153e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 8.2915e+09 8.2906e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 3 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2915e+09 8.2907e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 1\n", + " Flow cover: 2\n", + "\n", + "Explored 1 nodes (565 simplex iterations) in 0.04 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 8.29153e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:46.969575966Z", + "start_time": "2023-06-06T20:05:46.420803286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x5e67c6ee\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x8a0f9587\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 9.757128e+09\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 9.7571e+09 8.2906e+09 15.0% - 0s\n", + "H 0 0 8.298273e+09 8.2906e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + "H 0 0 8.293980e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 5 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 1 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + "H 0 0 8.291465e+09 8.2908e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 2\n", + " MIR: 1\n", + "\n", + "Explored 1 nodes (1025 simplex iterations) in 0.12 seconds (0.03 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:06:26.913448568Z", + "start_time": "2023-06-06T20:06:26.169047914Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x2dfe4e1c\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.5917580e+09 5.627453e+04 0.000000e+00 0s\n", + " 1 8.2535968e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.253596777e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x0f0924a1\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.25814e+09 (0.00s)\n", + "User MIP start produced solution with objective 8.25512e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.25459e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2536e+09 0 1 8.2546e+09 8.2536e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 3 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 1 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 4 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 5 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 6 8.2546e+09 8.2538e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + " MIR: 2\n", + " StrongCG: 1\n", + " Flow cover: 1\n", + "\n", + "Explored 1 nodes (575 simplex iterations) in 0.09 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n", + "obj = 8254590409.96973\n", + " x = [1.0, 1.0, 0.0, 1.0, 1.0]\n", + " y = [935662.0949262811, 1604270.0218116897, 0.0, 1369560.835229226, 602828.5321028307]\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[0]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "print(\"obj =\", model.inner.obj())\n", + "print(\" x =\", [model.inner.x[i].value for i in range(5)])\n", + "print(\" y =\", [model.inner.y[i].value for i in range(5)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5593d23a-83bd-4e16-8253-6300f5e3f63b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/_static/__init__.py b/0.4/_static/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/0.4/_static/__pycache__/__init__.cpython-311.pyc b/0.4/_static/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a82a9a07b6dc7cfca61f26f20d966750001ab5d GIT binary patch literal 221 zcmXwzO$x#=5QP(qB0}*TE_9G?#l4^+^Z;Snjy6=2ki?&!#3P7D@H}2X=t@_npl|Wb zn>R4LTO1Eq(7Sq2JD)B7;y=^$g1NGSrEJ94cV;_bZ`$L>K&cM|c)^n-3#s1vGujX+ z%(W^o0a7OitHowXXcg>b2B!9@>K1t&d?4dxZ@$bmDZR2PwHT?t=O`0G4+nS&5g iNiW32)Sv$Bpo`<4=C&IOv=!Yj;1zMsU+fPXh3pH tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/0.4/_static/css/index.c5995385ac14fb8791e8eb36b4908be2.css b/0.4/_static/css/index.c5995385ac14fb8791e8eb36b4908be2.css new file mode 100644 index 0000000..655656d --- /dev/null +++ b/0.4/_static/css/index.c5995385ac14fb8791e8eb36b4908be2.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:540px;--breakpoint-md:720px;--breakpoint-lg:960px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1rem;line-height:1.5;color:#212529;text-align:left}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;background-color:transparent}a:hover{color:#0056b3}a:not([href]),a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{border-style:none}img,svg{vertical-align:middle}svg{overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem}.display-1,.display-2{font-weight:300;line-height:1.2}.display-2{font-size:5.5rem}.display-3{font-size:4.5rem}.display-3,.display-4{font-weight:300;line-height:1.2}.display-4{font-size:3.5rem}hr{margin-top:1rem;margin-bottom:1rem;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer:before{content:"\2014\00A0"}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:540px){.container{max-width:540px}}@media (min-width:720px){.container{max-width:720px}}@media (min-width:960px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1400px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:540px){.container,.container-sm{max-width:540px}}@media (min-width:720px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:960px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1400px}}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col-auto,.col-lg,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-auto,.col-md,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md-auto,.col-sm,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:540px){.col-sm{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:720px){.col-md{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:960px){.col-lg{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover,.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover,.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover,.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th,.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:hsla(0,0%,100%,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:hsla(0,0%,100%,.075)}@media (max-width:539.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:719.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:959.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size],textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label:before,.was-validated .custom-control-input:valid~.custom-control-label:before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .custom-control-input:valid:checked~.custom-control-label:before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label:before,.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3E%3Ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545'%3E%3Ccircle cx='6' cy='6' r='4.5'/%3E%3Cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3E%3Ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3E%3C/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label:before,.was-validated .custom-control-input:invalid~.custom-control-label:before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label:before,.was-validated .custom-control-input:invalid:checked~.custom-control-label:before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label:before,.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label:before,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:540px){.form-inline label{justify-content:center}.form-inline .form-group,.form-inline label{display:flex;align-items:center;margin-bottom:0}.form-inline .form-group{flex:0 0 auto;flex-flow:row wrap}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary.focus,.btn-secondary:focus,.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success.focus,.btn-success:focus,.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info.focus,.btn-info:focus,.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning.focus,.btn-warning:focus,.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger.focus,.btn-danger:focus,.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light.focus,.btn-light:focus,.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark.focus,.btn-dark:focus,.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3}.btn-link.focus,.btn-link:focus,.btn-link:hover{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:540px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:720px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:960px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-toggle:after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle:after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";display:none}.dropleft .dropdown-toggle:before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty:after{margin-left:0}.dropleft .dropdown-toggle:before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split:after,.dropright .dropdown-toggle-split:after,.dropup .dropdown-toggle-split:after{margin-left:0}.dropleft .dropdown-toggle-split:before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio],.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label:after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label:before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label:before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label:before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label:before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label:before,.custom-control-input[disabled]~.custom-control-label:before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label:before{pointer-events:none;background-color:#fff;border:1px solid #adb5bd}.custom-control-label:after,.custom-control-label:before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:""}.custom-control-label:after{background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label:before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label:before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label:before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label:before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label:after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label:after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label:after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{display:inline-block;margin-bottom:0}.custom-file,.custom-file-input{position:relative;width:100%;height:calc(1.5em + .75rem + 2px)}.custom-file-input{z-index:2;margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label:after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]:after{content:attr(data-browse)}.custom-file-label{left:0;z-index:1;height:calc(1.5em + .75rem + 2px);font-weight:400;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label,.custom-file-label:after{position:absolute;top:0;right:0;padding:.375rem .75rem;line-height:1.5;color:#495057}.custom-file-label:after{bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower,.custom-range::-ms-fill-upper{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label:before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label:before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;padding:.5rem 1rem}.navbar,.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat 50%;background-size:100% 100%}@media (max-width:539.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:540px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:719.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:720px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:959.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:960px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(0,0,0,0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:hsla(0,0%,100%,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:hsla(0,0%,100%,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:hsla(0,0%,100%,.5);border-color:hsla(0,0%,100%,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Cpath stroke='rgba(255,255,255,0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-bottom:-.75rem;border-bottom:0}.card-header-pills,.card-header-tabs{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:540px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:540px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:540px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb,.breadcrumb-item{display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item:before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover:before{text-decoration:underline;text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:540px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@keyframes progress-bar-stripes{0%{background-position:1rem 0}to{background-position:0 0}}.progress{height:1rem;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress,.progress-bar{display:flex;overflow:hidden}.progress-bar{flex-direction:column;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:540px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:720px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:960px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:hsla(0,0%,100%,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:hsla(0,0%,100%,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translateY(-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered:before{display:block;height:calc(100vh - 1rem);height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable:before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:540px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered:before{height:calc(100vh - 3.5rem);height:min-content}.modal-sm{max-width:300px}}@media (min-width:960px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow:before,.bs-tooltip-top .arrow:before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow:before,.bs-tooltip-right .arrow:before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow:before,.bs-tooltip-bottom .arrow:before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow:before,.bs-tooltip-left .arrow:before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{top:0;left:0;z-index:1060;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover,.popover .arrow{position:absolute;display:block}.popover .arrow{width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow:after,.popover .arrow:before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow:before,.bs-popover-top>.arrow:before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow:after,.bs-popover-top>.arrow:after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow:before,.bs-popover-right>.arrow:before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow:after,.bs-popover-right>.arrow:after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow:before,.bs-popover-bottom>.arrow:before{top:0;border-width:0 .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow:after,.bs-popover-bottom>.arrow:after{top:1px;border-width:0 .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header:before,.bs-popover-bottom .popover-header:before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow:before,.bs-popover-left>.arrow:before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow:after,.bs-popover-left>.arrow:after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner:after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8'%3E%3Cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(1turn)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid;border-right:.25em solid transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important}.rounded-right,.rounded-top{border-top-right-radius:.25rem!important}.rounded-bottom,.rounded-right{border-bottom-right-radius:.25rem!important}.rounded-bottom,.rounded-left{border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix:after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:540px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:720px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:960px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive:before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9:before{padding-top:42.85714%}.embed-responsive-16by9:before{padding-top:56.25%}.embed-responsive-4by3:before{padding-top:75%}.embed-responsive-1by1:before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:540px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:720px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:960px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:540px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:720px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:960px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{top:0}.fixed-bottom,.fixed-top{position:fixed;right:0;left:0;z-index:1030}.fixed-bottom{bottom:0}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:540px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:720px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:960px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link:after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:transparent}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:540px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:720px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:960px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:hsla(0,0%,100%,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,:after,:before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}.container,body{min-width:960px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}html{font-size:var(--pst-font-size-base);scroll-padding-top:calc(var(--pst-header-height) + 12px)}body{padding-top:calc(var(--pst-header-height) + 20px);background-color:#fff;font-family:var(--pst-font-family-base);font-weight:400;line-height:1.65;color:rgba(var(--pst-color-text-base),1)}p{margin-bottom:1.15rem;font-size:1em;color:rgba(var(--pst-color-paragraph),1)}p.rubric{border-bottom:1px solid #c9c9c9}a{color:rgba(var(--pst-color-link),1);text-decoration:none}a:hover{color:rgba(var(--pst-color-link-hover),1);text-decoration:underline}a.headerlink{color:rgba(var(--pst-color-headerlink),1);font-size:.8em;padding:0 4px;text-decoration:none}a.headerlink:hover{background-color:rgba(var(--pst-color-headerlink),1);color:rgba(var(--pst-color-headerlink-hover),1)}.heading-style,h1,h2,h3,h4,h5,h6{margin:2.75rem 0 1.05rem;font-family:var(--pst-font-family-heading);font-weight:400;line-height:1.15}h1{margin-top:0;font-size:var(--pst-font-size-h1);color:rgba(var(--pst-color-h1),1)}h2{font-size:var(--pst-font-size-h2);color:rgba(var(--pst-color-h2),1)}h3{font-size:var(--pst-font-size-h3);color:rgba(var(--pst-color-h3),1)}h4{font-size:var(--pst-font-size-h4);color:rgba(var(--pst-color-h4),1)}h5{font-size:var(--pst-font-size-h5);color:rgba(var(--pst-color-h5),1)}h6{font-size:var(--pst-font-size-h6);color:rgba(var(--pst-color-h6),1)}.text_small,small{font-size:var(--pst-font-size-milli)}hr{border:0;border-top:1px solid #e5e5e5}code,kbd,pre,samp{font-family:var(--pst-font-family-monospace)}code{color:rgba(var(--pst-color-inline-code),1)}pre{margin:1.5em 0;padding:10px;background-color:rgba(var(--pst-color-preformatted-background),1);color:rgba(var(--pst-color-preformatted-text),1);line-height:1.2em;border:1px solid #c9c9c9;box-shadow:1px 1px 1px #d8d8d8}.navbar{position:fixed;min-height:var(--pst-header-height);width:100%;padding:0}.navbar .container-xl{height:100%}@media (min-width:960px){.navbar #navbar-end>.navbar-end-item{display:inline-block}}.navbar-brand{position:relative;height:var(--pst-header-height);width:auto;padding:.5rem 0}.navbar-brand img{max-width:100%;height:100%;width:auto}.navbar-light{background:#fff!important;box-shadow:0 .125rem .25rem 0 rgba(0,0,0,.11)}.navbar-light .navbar-nav li a.nav-link{padding:0 .5rem;color:rgba(var(--pst-color-navbar-link),1)}.navbar-light .navbar-nav li a.nav-link:hover{color:rgba(var(--pst-color-navbar-link-hover),1)}.navbar-light .navbar-nav>.active>.nav-link{font-weight:600;color:rgba(var(--pst-color-navbar-link-active),1)}.navbar-header a{padding:0 15px}.admonition{margin:1.5625em auto;padding:0 .6rem .8rem!important;overflow:hidden;page-break-inside:avoid;border-left:.2rem solid;border-left-color:rgba(var(--pst-color-admonition-default),1);border-bottom-color:rgba(var(--pst-color-admonition-default),1);border-right-color:rgba(var(--pst-color-admonition-default),1);border-top-color:rgba(var(--pst-color-admonition-default),1);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1);transition:color .25s,background-color .25s,border-color .25s}.admonition :last-child{margin-bottom:0}.admonition p.admonition-title~*{padding:0 1.4rem}.admonition>ol,.admonition>ul{margin-left:1em}.admonition .admonition-title{position:relative;margin:0 -.6rem!important;padding:.4rem .6rem .4rem 2rem;font-weight:700;background-color:rgba(var(--pst-color-admonition-default),.1)}.admonition .admonition-title:before{position:absolute;left:.6rem;width:1rem;height:1rem;color:rgba(var(--pst-color-admonition-default),1);font-family:Font Awesome\ 5 Free;font-weight:900;content:var(--pst-icon-admonition-default)}.admonition .admonition-title+*{margin-top:.4em}.admonition.attention{border-color:rgba(var(--pst-color-admonition-attention),1)}.admonition.attention .admonition-title{background-color:rgba(var(--pst-color-admonition-attention),.1)}.admonition.attention .admonition-title:before{color:rgba(var(--pst-color-admonition-attention),1);content:var(--pst-icon-admonition-attention)}.admonition.caution{border-color:rgba(var(--pst-color-admonition-caution),1)}.admonition.caution .admonition-title{background-color:rgba(var(--pst-color-admonition-caution),.1)}.admonition.caution .admonition-title:before{color:rgba(var(--pst-color-admonition-caution),1);content:var(--pst-icon-admonition-caution)}.admonition.warning{border-color:rgba(var(--pst-color-admonition-warning),1)}.admonition.warning .admonition-title{background-color:rgba(var(--pst-color-admonition-warning),.1)}.admonition.warning .admonition-title:before{color:rgba(var(--pst-color-admonition-warning),1);content:var(--pst-icon-admonition-warning)}.admonition.danger{border-color:rgba(var(--pst-color-admonition-danger),1)}.admonition.danger .admonition-title{background-color:rgba(var(--pst-color-admonition-danger),.1)}.admonition.danger .admonition-title:before{color:rgba(var(--pst-color-admonition-danger),1);content:var(--pst-icon-admonition-danger)}.admonition.error{border-color:rgba(var(--pst-color-admonition-error),1)}.admonition.error .admonition-title{background-color:rgba(var(--pst-color-admonition-error),.1)}.admonition.error .admonition-title:before{color:rgba(var(--pst-color-admonition-error),1);content:var(--pst-icon-admonition-error)}.admonition.hint{border-color:rgba(var(--pst-color-admonition-hint),1)}.admonition.hint .admonition-title{background-color:rgba(var(--pst-color-admonition-hint),.1)}.admonition.hint .admonition-title:before{color:rgba(var(--pst-color-admonition-hint),1);content:var(--pst-icon-admonition-hint)}.admonition.tip{border-color:rgba(var(--pst-color-admonition-tip),1)}.admonition.tip .admonition-title{background-color:rgba(var(--pst-color-admonition-tip),.1)}.admonition.tip .admonition-title:before{color:rgba(var(--pst-color-admonition-tip),1);content:var(--pst-icon-admonition-tip)}.admonition.important{border-color:rgba(var(--pst-color-admonition-important),1)}.admonition.important .admonition-title{background-color:rgba(var(--pst-color-admonition-important),.1)}.admonition.important .admonition-title:before{color:rgba(var(--pst-color-admonition-important),1);content:var(--pst-icon-admonition-important)}.admonition.note{border-color:rgba(var(--pst-color-admonition-note),1)}.admonition.note .admonition-title{background-color:rgba(var(--pst-color-admonition-note),.1)}.admonition.note .admonition-title:before{color:rgba(var(--pst-color-admonition-note),1);content:var(--pst-icon-admonition-note)}div.deprecated{margin-bottom:10px;margin-top:10px;padding:7px;background-color:#f3e5e5;border:1px solid #eed3d7;border-radius:.5rem}div.deprecated p{color:#b94a48;display:inline}.topic{background-color:#eee}.seealso dd{margin-top:0;margin-bottom:0}.viewcode-back{font-family:var(--pst-font-family-base)}.viewcode-block:target{background-color:#f4debf;border-top:1px solid #ac9;border-bottom:1px solid #ac9}span.guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}table.field-list{border-collapse:separate;border-spacing:10px;margin-left:1px}table.field-list th.field-name{padding:1px 8px 1px 5px;white-space:nowrap;background-color:#eee}table.field-list td.field-body p{font-style:italic}table.field-list td.field-body p>strong{font-style:normal}table.field-list td.field-body blockquote{border-left:none;margin:0 0 .3em;padding-left:30px}.table.autosummary td:first-child{white-space:nowrap}footer{width:100%;border-top:1px solid #ccc;padding:10px}footer .footer-item p{margin-bottom:0}.bd-search{position:relative;padding:1rem 15px;margin-right:-15px;margin-left:-15px}.bd-search .icon{position:absolute;color:#a4a6a7;left:25px;top:25px}.bd-search input{border-radius:0;border:0;border-bottom:1px solid #e5e5e5;padding-left:35px}.bd-toc{-ms-flex-order:2;order:2;height:calc(100vh - 2rem);overflow-y:auto}@supports (position:-webkit-sticky) or (position:sticky){.bd-toc{position:-webkit-sticky;position:sticky;top:calc(var(--pst-header-height) + 20px);height:calc(100vh - 5rem);overflow-y:auto}}.bd-toc .onthispage{color:#a4a6a7}.section-nav{padding-left:0;border-left:1px solid #eee;border-bottom:none}.section-nav ul{padding-left:1rem}.toc-entry,.toc-entry a{display:block}.toc-entry a{padding:.125rem 1.5rem;color:rgba(var(--pst-color-toc-link),1)}@media (min-width:1200px){.toc-entry a{padding-right:0}}.toc-entry a:hover{color:rgba(var(--pst-color-toc-link-hover),1);text-decoration:none}.bd-sidebar{padding-top:1em}@media (min-width:720px){.bd-sidebar{border-right:1px solid rgba(0,0,0,.1)}@supports (position:-webkit-sticky) or (position:sticky){.bd-sidebar{position:-webkit-sticky;position:sticky;top:calc(var(--pst-header-height) + 20px);z-index:1000;height:calc(100vh - var(--pst-header-height) - 20px)}}}.bd-sidebar.no-sidebar{border-right:0}.bd-links{padding-top:1rem;padding-bottom:1rem;margin-right:-15px;margin-left:-15px}@media (min-width:720px){.bd-links{display:block!important}@supports (position:-webkit-sticky) or (position:sticky){.bd-links{max-height:calc(100vh - 11rem);overflow-y:auto}}}.bd-sidenav{display:none}.bd-content{padding-top:20px}.bd-content .section{max-width:100%}.bd-content .section table{display:block;overflow:auto}.bd-toc-link{display:block;padding:.25rem 1.5rem;font-weight:600;color:rgba(0,0,0,.65)}.bd-toc-link:hover{color:rgba(0,0,0,.85);text-decoration:none}.bd-toc-item.active{margin-bottom:1rem}.bd-toc-item.active:not(:first-child){margin-top:1rem}.bd-toc-item.active>.bd-toc-link{color:rgba(0,0,0,.85)}.bd-toc-item.active>.bd-toc-link:hover{background-color:transparent}.bd-toc-item.active>.bd-sidenav{display:block}nav.bd-links p.caption{font-size:var(--pst-sidebar-caption-font-size);text-transform:uppercase;font-weight:700;position:relative;margin-top:1.25em;margin-bottom:.5em;padding:0 1.5rem;color:rgba(var(--pst-color-sidebar-caption),1)}nav.bd-links p.caption:first-child{margin-top:0}.bd-sidebar .nav{font-size:var(--pst-sidebar-font-size)}.bd-sidebar .nav ul{list-style:none;padding:0 0 0 1.5rem}.bd-sidebar .nav li>a{display:block;padding:.25rem 1.5rem;color:rgba(var(--pst-color-sidebar-link),1)}.bd-sidebar .nav li>a:hover{color:rgba(var(--pst-color-sidebar-link-hover),1);text-decoration:none;background-color:transparent}.bd-sidebar .nav li>a.reference.external:after{font-family:Font Awesome\ 5 Free;font-weight:900;content:"\f35d";font-size:.75em;margin-left:.3em}.bd-sidebar .nav .active:hover>a,.bd-sidebar .nav .active>a{font-weight:600;color:rgba(var(--pst-color-sidebar-link-active),1)}.toc-h2{font-size:.85rem}.toc-h3{font-size:.75rem}.toc-h4{font-size:.65rem}.toc-entry>.nav-link.active{font-weight:600;color:#130654;color:rgba(var(--pst-color-toc-link-active),1);background-color:transparent;border-left:2px solid rgba(var(--pst-color-toc-link-active),1)}.nav-link:hover{border-style:none}#navbar-main-elements li.nav-item i{font-size:.7rem;padding-left:2px;vertical-align:middle}.bd-toc .nav .nav{display:none}.bd-toc .nav .nav.visible,.bd-toc .nav>.active>ul{display:block}.prev-next-bottom{margin:20px 0}.prev-next-bottom a.left-prev,.prev-next-bottom a.right-next{padding:10px;border:1px solid rgba(0,0,0,.2);max-width:45%;overflow-x:hidden;color:rgba(0,0,0,.65)}.prev-next-bottom a.left-prev{float:left}.prev-next-bottom a.left-prev:before{content:"<< "}.prev-next-bottom a.right-next{float:right}.prev-next-bottom a.right-next:after{content:" >>"}.alert{padding-bottom:0}.alert-info a{color:#e83e8c}#navbar-icon-links i.fa,#navbar-icon-links i.fab,#navbar-icon-links i.far,#navbar-icon-links i.fas{vertical-align:middle;font-style:normal;font-size:1.5rem;line-height:1.25}#navbar-icon-links i.fa-github-square:before{color:#333}#navbar-icon-links i.fa-twitter-square:before{color:#55acee}#navbar-icon-links i.fa-gitlab:before{color:#548}#navbar-icon-links i.fa-bitbucket:before{color:#0052cc}.tocsection{border-left:1px solid #eee;padding:.3rem 1.5rem}.tocsection i{padding-right:.5rem}.editthispage{padding-top:2rem}.editthispage a{color:#130754}.xr-wrap[hidden]{display:block!important}.toctree-checkbox{position:absolute;display:none}.toctree-checkbox~ul{display:none}.toctree-checkbox~label i{transform:rotate(0deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label i{transform:rotate(180deg)}.bd-sidebar li{position:relative}.bd-sidebar label{position:absolute;top:0;right:0;height:30px;width:30px;cursor:pointer;display:flex;justify-content:center;align-items:center}.bd-sidebar label:hover{background:rgba(var(--pst-color-sidebar-expander-background-hover),1)}.bd-sidebar label i{display:inline-block;font-size:.75rem;text-align:center}.bd-sidebar label i:hover{color:rgba(var(--pst-color-sidebar-link-hover),1)}.bd-sidebar li.has-children>.reference{padding-right:30px}div.doctest>div.highlight span.gp,span.linenos,table.highlighttable td.linenos{user-select:none!important;-webkit-user-select:text!important;-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important} \ No newline at end of file diff --git a/0.4/_static/css/theme.css b/0.4/_static/css/theme.css new file mode 100644 index 0000000..3f6e79d --- /dev/null +++ b/0.4/_static/css/theme.css @@ -0,0 +1,117 @@ +:root { + /***************************************************************************** + * Theme config + **/ + --pst-header-height: 60px; + + /***************************************************************************** + * Font size + **/ + --pst-font-size-base: 15px; /* base font size - applied at body / html level */ + + /* heading font sizes */ + --pst-font-size-h1: 36px; + --pst-font-size-h2: 32px; + --pst-font-size-h3: 26px; + --pst-font-size-h4: 21px; + --pst-font-size-h5: 18px; + --pst-font-size-h6: 16px; + + /* smaller then heading font sizes*/ + --pst-font-size-milli: 12px; + + --pst-sidebar-font-size: .9em; + --pst-sidebar-caption-font-size: .9em; + + /***************************************************************************** + * Font family + **/ + /* These are adapted from https://systemfontstack.com/ */ + --pst-font-family-base-system: -apple-system, BlinkMacSystemFont, Segoe UI, "Helvetica Neue", + Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; + --pst-font-family-monospace-system: "SFMono-Regular", Menlo, Consolas, Monaco, + Liberation Mono, Lucida Console, monospace; + + --pst-font-family-base: var(--pst-font-family-base-system); + --pst-font-family-heading: var(--pst-font-family-base); + --pst-font-family-monospace: var(--pst-font-family-monospace-system); + + /***************************************************************************** + * Color + * + * Colors are defined in rgb string way, "red, green, blue" + **/ + --pst-color-primary: 19, 6, 84; + --pst-color-success: 40, 167, 69; + --pst-color-info: 0, 123, 255; /*23, 162, 184;*/ + --pst-color-warning: 255, 193, 7; + --pst-color-danger: 220, 53, 69; + --pst-color-text-base: 51, 51, 51; + + --pst-color-h1: var(--pst-color-primary); + --pst-color-h2: var(--pst-color-primary); + --pst-color-h3: var(--pst-color-text-base); + --pst-color-h4: var(--pst-color-text-base); + --pst-color-h5: var(--pst-color-text-base); + --pst-color-h6: var(--pst-color-text-base); + --pst-color-paragraph: var(--pst-color-text-base); + --pst-color-link: 0, 91, 129; + --pst-color-link-hover: 227, 46, 0; + --pst-color-headerlink: 198, 15, 15; + --pst-color-headerlink-hover: 255, 255, 255; + --pst-color-preformatted-text: 34, 34, 34; + --pst-color-preformatted-background: 250, 250, 250; + --pst-color-inline-code: 232, 62, 140; + + --pst-color-active-navigation: 19, 6, 84; + --pst-color-navbar-link: 77, 77, 77; + --pst-color-navbar-link-hover: var(--pst-color-active-navigation); + --pst-color-navbar-link-active: var(--pst-color-active-navigation); + --pst-color-sidebar-link: 77, 77, 77; + --pst-color-sidebar-link-hover: var(--pst-color-active-navigation); + --pst-color-sidebar-link-active: var(--pst-color-active-navigation); + --pst-color-sidebar-expander-background-hover: 244, 244, 244; + --pst-color-sidebar-caption: 77, 77, 77; + --pst-color-toc-link: 119, 117, 122; + --pst-color-toc-link-hover: var(--pst-color-active-navigation); + --pst-color-toc-link-active: var(--pst-color-active-navigation); + + /***************************************************************************** + * Icon + **/ + + /* font awesome icons*/ + --pst-icon-check-circle: '\f058'; + --pst-icon-info-circle: '\f05a'; + --pst-icon-exclamation-triangle: '\f071'; + --pst-icon-exclamation-circle: '\f06a'; + --pst-icon-times-circle: '\f057'; + --pst-icon-lightbulb: '\f0eb'; + + /***************************************************************************** + * Admonitions + **/ + + --pst-color-admonition-default: var(--pst-color-info); + --pst-color-admonition-note: var(--pst-color-info); + --pst-color-admonition-attention: var(--pst-color-warning); + --pst-color-admonition-caution: var(--pst-color-warning); + --pst-color-admonition-warning: var(--pst-color-warning); + --pst-color-admonition-danger: var(--pst-color-danger); + --pst-color-admonition-error: var(--pst-color-danger); + --pst-color-admonition-hint: var(--pst-color-success); + --pst-color-admonition-tip: var(--pst-color-success); + --pst-color-admonition-important: var(--pst-color-success); + + --pst-icon-admonition-default: var(--pst-icon-info-circle); + --pst-icon-admonition-note: var(--pst-icon-info-circle); + --pst-icon-admonition-attention: var(--pst-icon-exclamation-circle); + --pst-icon-admonition-caution: var(--pst-icon-exclamation-triangle); + --pst-icon-admonition-warning: var(--pst-icon-exclamation-triangle); + --pst-icon-admonition-danger: var(--pst-icon-exclamation-triangle); + --pst-icon-admonition-error: var(--pst-icon-times-circle); + --pst-icon-admonition-hint: var(--pst-icon-lightbulb); + --pst-icon-admonition-tip: var(--pst-icon-lightbulb); + --pst-icon-admonition-important: var(--pst-icon-exclamation-circle); + +} diff --git a/0.4/_static/custom.css b/0.4/_static/custom.css new file mode 100644 index 0000000..668b842 --- /dev/null +++ b/0.4/_static/custom.css @@ -0,0 +1,120 @@ +h1.site-logo { + font-size: 30px !important; +} + +h1.site-logo small { + font-size: 20px !important; +} + +code { + display: inline-block; + border-radius: 4px; + padding: 0 4px; + background-color: #eee; + color: rgb(232, 62, 140); +} + +.right-next, .left-prev { + border-radius: 8px; + border-width: 0px !important; + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2); +} + +.right-next:hover, .left-prev:hover { + text-decoration: none; +} + +.admonition { + border-radius: 8px; + border-width: 0; + box-shadow: 0 0 0 !important; +} + +.note { background-color: rgba(0, 123, 255, 0.1); } +.note * { color: rgb(69 94 121); } + +.warning { background-color: rgb(220 150 40 / 10%); } +.warning * { color: rgb(105 72 28); } + +.input_area, .output_area, .output_area img { + border-radius: 8px !important; + border-width: 0 !important; + margin: 8px 0 8px 0; +} + +.output_area { + padding: 4px; + background-color: hsl(227 60% 11% / 0.7) !important; +} + +.output_area pre { + color: #fff; + line-height: 20px !important; +} + +.input_area pre { + background-color: rgba(0 0 0 / 3%) !important; + padding: 12px !important; + line-height: 20px; +} + +.ansi-green-intense-fg { + color: #64d88b !important; +} + +#site-navigation { + background-color: #fafafa; +} + +.container, .container-lg, .container-md, .container-sm, .container-xl { + max-width: inherit !important; +} + +h1, h2 { + font-weight: bold !important; +} + +#main-content .section { + max-width: 900px !important; + margin: 0 auto !important; + font-size: 16px; +} + +p.caption { + font-weight: bold; +} + +h2 { + padding-bottom: 5px; + border-bottom: 1px solid #ccc; +} + +h3 { + margin-top: 1.5rem; +} + +tbody, thead, pre { + border: 1px solid rgba(0, 0, 0, 0.25); +} + +table td, th { + padding: 8px; +} + +table p { + margin-bottom: 0; +} + +table td code { + white-space: nowrap; +} + +table tr, +table th { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +table tr:last-child { + border-bottom: 0; +} + diff --git a/0.4/_static/doctools.js b/0.4/_static/doctools.js new file mode 100644 index 0000000..d06a71d --- /dev/null +++ b/0.4/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/0.4/_static/documentation_options.js b/0.4/_static/documentation_options.js new file mode 100644 index 0000000..c141a07 --- /dev/null +++ b/0.4/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.4', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'dirhtml', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: true, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/0.4/_static/file.png b/0.4/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/0.4/_static/images/logo_binder.svg b/0.4/_static/images/logo_binder.svg new file mode 100644 index 0000000..45fecf7 --- /dev/null +++ b/0.4/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/0.4/_static/images/logo_colab.png b/0.4/_static/images/logo_colab.png new file mode 100644 index 0000000000000000000000000000000000000000..b7560ec216b2d1b6f77855525fe966c741833428 GIT binary patch literal 7601 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 diff --git a/0.4/_static/js/index.1c5a1a01449ed65a7b51.js b/0.4/_static/js/index.1c5a1a01449ed65a7b51.js new file mode 100644 index 0000000..b71f7fc --- /dev/null +++ b/0.4/_static/js/index.1c5a1a01449ed65a7b51.js @@ -0,0 +1,32 @@ +!function(t){var e={};function n(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return t[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(i,o,function(e){return t[e]}.bind(null,o));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=2)}([function(t,e){t.exports=jQuery},function(t,e,n){"use strict";n.r(e),function(t){ +/**! + * @fileOverview Kickass library to create and place poppers near their reference elements. + * @version 1.16.1 + * @license + * Copyright (c) 2016 Federico Zivolo and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +var n="undefined"!=typeof window&&"undefined"!=typeof document&&"undefined"!=typeof navigator,i=function(){for(var t=["Edge","Trident","Firefox"],e=0;e=0)return 1;return 0}();var o=n&&window.Promise?function(t){var e=!1;return function(){e||(e=!0,window.Promise.resolve().then((function(){e=!1,t()})))}}:function(t){var e=!1;return function(){e||(e=!0,setTimeout((function(){e=!1,t()}),i))}};function r(t){return t&&"[object Function]"==={}.toString.call(t)}function s(t,e){if(1!==t.nodeType)return[];var n=t.ownerDocument.defaultView.getComputedStyle(t,null);return e?n[e]:n}function a(t){return"HTML"===t.nodeName?t:t.parentNode||t.host}function l(t){if(!t)return document.body;switch(t.nodeName){case"HTML":case"BODY":return t.ownerDocument.body;case"#document":return t.body}var e=s(t),n=e.overflow,i=e.overflowX,o=e.overflowY;return/(auto|scroll|overlay)/.test(n+o+i)?t:l(a(t))}function c(t){return t&&t.referenceNode?t.referenceNode:t}var u=n&&!(!window.MSInputMethodContext||!document.documentMode),h=n&&/MSIE 10/.test(navigator.userAgent);function f(t){return 11===t?u:10===t?h:u||h}function d(t){if(!t)return document.documentElement;for(var e=f(10)?document.body:null,n=t.offsetParent||null;n===e&&t.nextElementSibling;)n=(t=t.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&"BODY"!==i&&"HTML"!==i?-1!==["TH","TD","TABLE"].indexOf(n.nodeName)&&"static"===s(n,"position")?d(n):n:t?t.ownerDocument.documentElement:document.documentElement}function p(t){return null!==t.parentNode?p(t.parentNode):t}function m(t,e){if(!(t&&t.nodeType&&e&&e.nodeType))return document.documentElement;var n=t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING,i=n?t:e,o=n?e:t,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var s,a,l=r.commonAncestorContainer;if(t!==l&&e!==l||i.contains(o))return"BODY"===(a=(s=l).nodeName)||"HTML"!==a&&d(s.firstElementChild)!==s?d(l):l;var c=p(t);return c.host?m(c.host,e):m(t,p(e).host)}function g(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"top",n="top"===e?"scrollTop":"scrollLeft",i=t.nodeName;if("BODY"===i||"HTML"===i){var o=t.ownerDocument.documentElement,r=t.ownerDocument.scrollingElement||o;return r[n]}return t[n]}function v(t,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],i=g(e,"top"),o=g(e,"left"),r=n?-1:1;return t.top+=i*r,t.bottom+=i*r,t.left+=o*r,t.right+=o*r,t}function _(t,e){var n="x"===e?"Left":"Top",i="Left"===n?"Right":"Bottom";return parseFloat(t["border"+n+"Width"])+parseFloat(t["border"+i+"Width"])}function b(t,e,n,i){return Math.max(e["offset"+t],e["scroll"+t],n["client"+t],n["offset"+t],n["scroll"+t],f(10)?parseInt(n["offset"+t])+parseInt(i["margin"+("Height"===t?"Top":"Left")])+parseInt(i["margin"+("Height"===t?"Bottom":"Right")]):0)}function y(t){var e=t.body,n=t.documentElement,i=f(10)&&getComputedStyle(n);return{height:b("Height",e,n,i),width:b("Width",e,n,i)}}var w=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},E=function(){function t(t,e){for(var n=0;n2&&void 0!==arguments[2]&&arguments[2],i=f(10),o="HTML"===e.nodeName,r=D(t),a=D(e),c=l(t),u=s(e),h=parseFloat(u.borderTopWidth),d=parseFloat(u.borderLeftWidth);n&&o&&(a.top=Math.max(a.top,0),a.left=Math.max(a.left,0));var p=S({top:r.top-a.top-h,left:r.left-a.left-d,width:r.width,height:r.height});if(p.marginTop=0,p.marginLeft=0,!i&&o){var m=parseFloat(u.marginTop),g=parseFloat(u.marginLeft);p.top-=h-m,p.bottom-=h-m,p.left-=d-g,p.right-=d-g,p.marginTop=m,p.marginLeft=g}return(i&&!n?e.contains(c):e===c&&"BODY"!==c.nodeName)&&(p=v(p,e)),p}function k(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=t.ownerDocument.documentElement,i=N(t,n),o=Math.max(n.clientWidth,window.innerWidth||0),r=Math.max(n.clientHeight,window.innerHeight||0),s=e?0:g(n),a=e?0:g(n,"left"),l={top:s-i.top+i.marginTop,left:a-i.left+i.marginLeft,width:o,height:r};return S(l)}function O(t){var e=t.nodeName;if("BODY"===e||"HTML"===e)return!1;if("fixed"===s(t,"position"))return!0;var n=a(t);return!!n&&O(n)}function A(t){if(!t||!t.parentElement||f())return document.documentElement;for(var e=t.parentElement;e&&"none"===s(e,"transform");)e=e.parentElement;return e||document.documentElement}function I(t,e,n,i){var o=arguments.length>4&&void 0!==arguments[4]&&arguments[4],r={top:0,left:0},s=o?A(t):m(t,c(e));if("viewport"===i)r=k(s,o);else{var u=void 0;"scrollParent"===i?"BODY"===(u=l(a(e))).nodeName&&(u=t.ownerDocument.documentElement):u="window"===i?t.ownerDocument.documentElement:i;var h=N(u,s,o);if("HTML"!==u.nodeName||O(s))r=h;else{var f=y(t.ownerDocument),d=f.height,p=f.width;r.top+=h.top-h.marginTop,r.bottom=d+h.top,r.left+=h.left-h.marginLeft,r.right=p+h.left}}var g="number"==typeof(n=n||0);return r.left+=g?n:n.left||0,r.top+=g?n:n.top||0,r.right-=g?n:n.right||0,r.bottom-=g?n:n.bottom||0,r}function x(t){return t.width*t.height}function j(t,e,n,i,o){var r=arguments.length>5&&void 0!==arguments[5]?arguments[5]:0;if(-1===t.indexOf("auto"))return t;var s=I(n,i,r,o),a={top:{width:s.width,height:e.top-s.top},right:{width:s.right-e.right,height:s.height},bottom:{width:s.width,height:s.bottom-e.bottom},left:{width:e.left-s.left,height:s.height}},l=Object.keys(a).map((function(t){return C({key:t},a[t],{area:x(a[t])})})).sort((function(t,e){return e.area-t.area})),c=l.filter((function(t){var e=t.width,i=t.height;return e>=n.clientWidth&&i>=n.clientHeight})),u=c.length>0?c[0].key:l[0].key,h=t.split("-")[1];return u+(h?"-"+h:"")}function L(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=i?A(e):m(e,c(n));return N(n,o,i)}function P(t){var e=t.ownerDocument.defaultView.getComputedStyle(t),n=parseFloat(e.marginTop||0)+parseFloat(e.marginBottom||0),i=parseFloat(e.marginLeft||0)+parseFloat(e.marginRight||0);return{width:t.offsetWidth+i,height:t.offsetHeight+n}}function F(t){var e={left:"right",right:"left",bottom:"top",top:"bottom"};return t.replace(/left|right|bottom|top/g,(function(t){return e[t]}))}function R(t,e,n){n=n.split("-")[0];var i=P(t),o={width:i.width,height:i.height},r=-1!==["right","left"].indexOf(n),s=r?"top":"left",a=r?"left":"top",l=r?"height":"width",c=r?"width":"height";return o[s]=e[s]+e[l]/2-i[l]/2,o[a]=n===a?e[a]-i[c]:e[F(a)],o}function M(t,e){return Array.prototype.find?t.find(e):t.filter(e)[0]}function B(t,e,n){return(void 0===n?t:t.slice(0,function(t,e,n){if(Array.prototype.findIndex)return t.findIndex((function(t){return t[e]===n}));var i=M(t,(function(t){return t[e]===n}));return t.indexOf(i)}(t,"name",n))).forEach((function(t){t.function&&console.warn("`modifier.function` is deprecated, use `modifier.fn`!");var n=t.function||t.fn;t.enabled&&r(n)&&(e.offsets.popper=S(e.offsets.popper),e.offsets.reference=S(e.offsets.reference),e=n(e,t))})),e}function H(){if(!this.state.isDestroyed){var t={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};t.offsets.reference=L(this.state,this.popper,this.reference,this.options.positionFixed),t.placement=j(this.options.placement,t.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),t.originalPlacement=t.placement,t.positionFixed=this.options.positionFixed,t.offsets.popper=R(this.popper,t.offsets.reference,t.placement),t.offsets.popper.position=this.options.positionFixed?"fixed":"absolute",t=B(this.modifiers,t),this.state.isCreated?this.options.onUpdate(t):(this.state.isCreated=!0,this.options.onCreate(t))}}function q(t,e){return t.some((function(t){var n=t.name;return t.enabled&&n===e}))}function Q(t){for(var e=[!1,"ms","Webkit","Moz","O"],n=t.charAt(0).toUpperCase()+t.slice(1),i=0;i1&&void 0!==arguments[1]&&arguments[1],n=Z.indexOf(t),i=Z.slice(n+1).concat(Z.slice(0,n));return e?i.reverse():i}var et="flip",nt="clockwise",it="counterclockwise";function ot(t,e,n,i){var o=[0,0],r=-1!==["right","left"].indexOf(i),s=t.split(/(\+|\-)/).map((function(t){return t.trim()})),a=s.indexOf(M(s,(function(t){return-1!==t.search(/,|\s/)})));s[a]&&-1===s[a].indexOf(",")&&console.warn("Offsets separated by white space(s) are deprecated, use a comma (,) instead.");var l=/\s*,\s*|\s+/,c=-1!==a?[s.slice(0,a).concat([s[a].split(l)[0]]),[s[a].split(l)[1]].concat(s.slice(a+1))]:[s];return(c=c.map((function(t,i){var o=(1===i?!r:r)?"height":"width",s=!1;return t.reduce((function(t,e){return""===t[t.length-1]&&-1!==["+","-"].indexOf(e)?(t[t.length-1]=e,s=!0,t):s?(t[t.length-1]+=e,s=!1,t):t.concat(e)}),[]).map((function(t){return function(t,e,n,i){var o=t.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),r=+o[1],s=o[2];if(!r)return t;if(0===s.indexOf("%")){var a=void 0;switch(s){case"%p":a=n;break;case"%":case"%r":default:a=i}return S(a)[e]/100*r}if("vh"===s||"vw"===s){return("vh"===s?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0))/100*r}return r}(t,o,e,n)}))}))).forEach((function(t,e){t.forEach((function(n,i){X(n)&&(o[e]+=n*("-"===t[i-1]?-1:1))}))})),o}var rt={placement:"bottom",positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(t){var e=t.placement,n=e.split("-")[0],i=e.split("-")[1];if(i){var o=t.offsets,r=o.reference,s=o.popper,a=-1!==["bottom","top"].indexOf(n),l=a?"left":"top",c=a?"width":"height",u={start:T({},l,r[l]),end:T({},l,r[l]+r[c]-s[c])};t.offsets.popper=C({},s,u[i])}return t}},offset:{order:200,enabled:!0,fn:function(t,e){var n=e.offset,i=t.placement,o=t.offsets,r=o.popper,s=o.reference,a=i.split("-")[0],l=void 0;return l=X(+n)?[+n,0]:ot(n,r,s,a),"left"===a?(r.top+=l[0],r.left-=l[1]):"right"===a?(r.top+=l[0],r.left+=l[1]):"top"===a?(r.left+=l[0],r.top-=l[1]):"bottom"===a&&(r.left+=l[0],r.top+=l[1]),t.popper=r,t},offset:0},preventOverflow:{order:300,enabled:!0,fn:function(t,e){var n=e.boundariesElement||d(t.instance.popper);t.instance.reference===n&&(n=d(n));var i=Q("transform"),o=t.instance.popper.style,r=o.top,s=o.left,a=o[i];o.top="",o.left="",o[i]="";var l=I(t.instance.popper,t.instance.reference,e.padding,n,t.positionFixed);o.top=r,o.left=s,o[i]=a,e.boundaries=l;var c=e.priority,u=t.offsets.popper,h={primary:function(t){var n=u[t];return u[t]l[t]&&!e.escapeWithReference&&(i=Math.min(u[n],l[t]-("right"===t?u.width:u.height))),T({},n,i)}};return c.forEach((function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";u=C({},u,h[e](t))})),t.offsets.popper=u,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!G(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,a=r.popper,l=r.reference,c=-1!==["left","right"].indexOf(o),u=c?"height":"width",h=c?"Top":"Left",f=h.toLowerCase(),d=c?"left":"top",p=c?"bottom":"right",m=P(i)[u];l[p]-ma[p]&&(t.offsets.popper[f]+=l[f]+m-a[p]),t.offsets.popper=S(t.offsets.popper);var g=l[f]+l[u]/2-m/2,v=s(t.instance.popper),_=parseFloat(v["margin"+h]),b=parseFloat(v["border"+h+"Width"]),y=g-t.offsets.popper[f]-_-b;return y=Math.max(Math.min(a[u]-m,y),0),t.arrowElement=i,t.offsets.arrow=(T(n={},f,Math.round(y)),T(n,d,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(t,e){if(q(t.instance.modifiers,"inner"))return t;if(t.flipped&&t.placement===t.originalPlacement)return t;var n=I(t.instance.popper,t.instance.reference,e.padding,e.boundariesElement,t.positionFixed),i=t.placement.split("-")[0],o=F(i),r=t.placement.split("-")[1]||"",s=[];switch(e.behavior){case et:s=[i,o];break;case nt:s=tt(i);break;case it:s=tt(i,!0);break;default:s=e.behavior}return s.forEach((function(a,l){if(i!==a||s.length===l+1)return t;i=t.placement.split("-")[0],o=F(i);var c=t.offsets.popper,u=t.offsets.reference,h=Math.floor,f="left"===i&&h(c.right)>h(u.left)||"right"===i&&h(c.left)h(u.top)||"bottom"===i&&h(c.top)h(n.right),m=h(c.top)h(n.bottom),v="left"===i&&d||"right"===i&&p||"top"===i&&m||"bottom"===i&&g,_=-1!==["top","bottom"].indexOf(i),b=!!e.flipVariations&&(_&&"start"===r&&d||_&&"end"===r&&p||!_&&"start"===r&&m||!_&&"end"===r&&g),y=!!e.flipVariationsByContent&&(_&&"start"===r&&p||_&&"end"===r&&d||!_&&"start"===r&&g||!_&&"end"===r&&m),w=b||y;(f||v||w)&&(t.flipped=!0,(f||v)&&(i=s[l+1]),w&&(r=function(t){return"end"===t?"start":"start"===t?"end":t}(r)),t.placement=i+(r?"-"+r:""),t.offsets.popper=C({},t.offsets.popper,R(t.instance.popper,t.offsets.reference,t.placement)),t=B(t.instance.modifiers,t,"flip"))})),t},behavior:"flip",padding:5,boundariesElement:"viewport",flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=F(e),t.offsets.popper=S(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!G(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=M(t.instance.modifiers,(function(t){return"preventOverflow"===t.name})).boundaries;if(e.bottomn.right||e.top>n.bottom||e.right2&&void 0!==arguments[2]?arguments[2]:{};w(this,t),this.scheduleUpdate=function(){return requestAnimationFrame(i.update)},this.update=o(this.update.bind(this)),this.options=C({},t.Defaults,s),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=e&&e.jquery?e[0]:e,this.popper=n&&n.jquery?n[0]:n,this.options.modifiers={},Object.keys(C({},t.Defaults.modifiers,s.modifiers)).forEach((function(e){i.options.modifiers[e]=C({},t.Defaults.modifiers[e]||{},s.modifiers?s.modifiers[e]:{})})),this.modifiers=Object.keys(this.options.modifiers).map((function(t){return C({name:t},i.options.modifiers[t])})).sort((function(t,e){return t.order-e.order})),this.modifiers.forEach((function(t){t.enabled&&r(t.onLoad)&&t.onLoad(i.reference,i.popper,i.options,t,i.state)})),this.update();var a=this.options.eventsEnabled;a&&this.enableEventListeners(),this.state.eventsEnabled=a}return E(t,[{key:"update",value:function(){return H.call(this)}},{key:"destroy",value:function(){return W.call(this)}},{key:"enableEventListeners",value:function(){return Y.call(this)}},{key:"disableEventListeners",value:function(){return z.call(this)}}]),t}();st.Utils=("undefined"!=typeof window?window:t).PopperUtils,st.placements=J,st.Defaults=rt,e.default=st}.call(this,n(4))},function(t,e,n){t.exports=n(5)},function(t,e,n){ +/*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e,n){"use strict";function i(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};c.jQueryDetection(),e.fn.emulateTransitionEnd=l,e.event.special[c.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(t){if(e(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var u="alert",h=e.fn[u],f=function(){function t(t){this._element=t}var n=t.prototype;return n.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},n.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},n._getRootElement=function(t){var n=c.getSelectorFromElement(t),i=!1;return n&&(i=document.querySelector(n)),i||(i=e(t).closest(".alert")[0]),i},n._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},n._removeElement=function(t){var n=this;if(e(t).removeClass("show"),e(t).hasClass("fade")){var i=c.getTransitionDurationFromElement(t);e(t).one(c.TRANSITION_END,(function(e){return n._destroyElement(t,e)})).emulateTransitionEnd(i)}else this._destroyElement(t)},n._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.alert");o||(o=new t(this),i.data("bs.alert",o)),"close"===n&&o[n](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',f._handleDismiss(new f)),e.fn[u]=f._jQueryInterface,e.fn[u].Constructor=f,e.fn[u].noConflict=function(){return e.fn[u]=h,f._jQueryInterface};var d=e.fn.button,p=function(){function t(t){this._element=t}var n=t.prototype;return n.toggle=function(){var t=!0,n=!0,i=e(this._element).closest('[data-toggle="buttons"]')[0];if(i){var o=this._element.querySelector('input:not([type="hidden"])');if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains("active"))t=!1;else{var r=i.querySelector(".active");r&&e(r).removeClass("active")}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains("active")),e(o).trigger("change")),o.focus(),n=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(n&&this._element.setAttribute("aria-pressed",!this._element.classList.contains("active")),t&&e(this._element).toggleClass("active"))},n.dispose=function(){e.removeData(this._element,"bs.button"),this._element=null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.button");i||(i=new t(this),e(this).data("bs.button",i)),"toggle"===n&&i[n]()}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=t.target,i=n;if(e(n).hasClass("btn")||(n=e(n).closest(".btn")[0]),!n||n.hasAttribute("disabled")||n.classList.contains("disabled"))t.preventDefault();else{var o=n.querySelector('input:not([type="hidden"])');if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();"LABEL"===i.tagName&&o&&"checkbox"===o.type&&t.preventDefault(),p._jQueryInterface.call(e(n),"toggle")}})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=e(t.target).closest(".btn")[0];e(n).toggleClass("focus",/^focus(in)?$/.test(t.type))})),e(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var n=t.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&e(this._element).is(":visible")&&"hidden"!==e(this._element).css("visibility")&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(c.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var n=this;this._activeElement=this._element.querySelector(".active.carousel-item");var i=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)e(this._element).one("slid.bs.carousel",(function(){return n.to(t)}));else{if(i===t)return this.pause(),void this.cycle();var o=t>i?"next":"prev";this._slide(o,this._items[t])}},n.dispose=function(){e(this._element).off(g),e.removeData(this._element,"bs.carousel"),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=a(a({},_),t),c.typeCheckConfig(m,t,b),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&e(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&e(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var n=function(e){t._pointerEvent&&y[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},i=function(e){t._pointerEvent&&y[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};e(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(e(this._element).on("pointerdown.bs.carousel",(function(t){return n(t)})),e(this._element).on("pointerup.bs.carousel",(function(t){return i(t)})),this._element.classList.add("pointer-event")):(e(this._element).on("touchstart.bs.carousel",(function(t){return n(t)})),e(this._element).on("touchmove.bs.carousel",(function(e){return function(e){e.originalEvent.touches&&e.originalEvent.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),e(this._element).on("touchend.bs.carousel",(function(t){return i(t)})))}},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+("prev"===t?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},n._triggerSlideEvent=function(t,n){var i=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(".active.carousel-item")),r=e.Event("slide.bs.carousel",{relatedTarget:t,direction:n,from:o,to:i});return e(this._element).trigger(r),r},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var n=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));e(n).removeClass("active");var i=this._indicatorsElement.children[this._getItemIndex(t)];i&&e(i).addClass("active")}},n._slide=function(t,n){var i,o,r,s=this,a=this._element.querySelector(".active.carousel-item"),l=this._getItemIndex(a),u=n||a&&this._getItemByDirection(t,a),h=this._getItemIndex(u),f=Boolean(this._interval);if("next"===t?(i="carousel-item-left",o="carousel-item-next",r="left"):(i="carousel-item-right",o="carousel-item-prev",r="right"),u&&e(u).hasClass("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(u,r).isDefaultPrevented()&&a&&u){this._isSliding=!0,f&&this.pause(),this._setActiveIndicatorElement(u);var d=e.Event("slid.bs.carousel",{relatedTarget:u,direction:r,from:l,to:h});if(e(this._element).hasClass("slide")){e(u).addClass(o),c.reflow(u),e(a).addClass(i),e(u).addClass(i);var p=parseInt(u.getAttribute("data-interval"),10);p?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=p):this._config.interval=this._config.defaultInterval||this._config.interval;var m=c.getTransitionDurationFromElement(a);e(a).one(c.TRANSITION_END,(function(){e(u).removeClass(i+" "+o).addClass("active"),e(a).removeClass("active "+o+" "+i),s._isSliding=!1,setTimeout((function(){return e(s._element).trigger(d)}),0)})).emulateTransitionEnd(m)}else e(a).removeClass("active"),e(u).addClass("active"),this._isSliding=!1,e(this._element).trigger(d);f&&this.cycle()}},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.carousel"),o=a(a({},_),e(this).data());"object"==typeof n&&(o=a(a({},o),n));var r="string"==typeof n?n:o.slide;if(i||(i=new t(this,o),e(this).data("bs.carousel",i)),"number"==typeof n)i.to(n);else if("string"==typeof r){if(void 0===i[r])throw new TypeError('No method named "'+r+'"');i[r]()}else o.interval&&o.ride&&(i.pause(),i.cycle())}))},t._dataApiClickHandler=function(n){var i=c.getSelectorFromElement(this);if(i){var o=e(i)[0];if(o&&e(o).hasClass("carousel")){var r=a(a({},e(o).data()),e(this).data()),s=this.getAttribute("data-slide-to");s&&(r.interval=!1),t._jQueryInterface.call(e(o),r),s&&e(o).data("bs.carousel").to(s),n.preventDefault()}}},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return _}}]),t}();e(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",w._dataApiClickHandler),e(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),n=0,i=t.length;n0&&(this._selector=s,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var n=t.prototype;return n.toggle=function(){e(this._element).hasClass("show")?this.hide():this.show()},n.show=function(){var n,i,o=this;if(!(this._isTransitioning||e(this._element).hasClass("show")||(this._parent&&0===(n=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains("collapse")}))).length&&(n=null),n&&(i=e(n).not(this._selector).data("bs.collapse"))&&i._isTransitioning))){var r=e.Event("show.bs.collapse");if(e(this._element).trigger(r),!r.isDefaultPrevented()){n&&(t._jQueryInterface.call(e(n).not(this._selector),"hide"),i||e(n).data("bs.collapse",null));var s=this._getDimension();e(this._element).removeClass("collapse").addClass("collapsing"),this._element.style[s]=0,this._triggerArray.length&&e(this._triggerArray).removeClass("collapsed").attr("aria-expanded",!0),this.setTransitioning(!0);var a="scroll"+(s[0].toUpperCase()+s.slice(1)),l=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,(function(){e(o._element).removeClass("collapsing").addClass("collapse show"),o._element.style[s]="",o.setTransitioning(!1),e(o._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(l),this._element.style[s]=this._element[a]+"px"}}},n.hide=function(){var t=this;if(!this._isTransitioning&&e(this._element).hasClass("show")){var n=e.Event("hide.bs.collapse");if(e(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",c.reflow(this._element),e(this._element).addClass("collapsing").removeClass("collapse show");var o=this._triggerArray.length;if(o>0)for(var r=0;r0},i._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=a(a({},e.offsets),t._config.offset(e.offsets,t._element)||{}),e}:e.offset=this._config.offset,e},i._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),a(a({},t),this._config.popperConfig)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.dropdown");if(i||(i=new t(this,"object"==typeof n?n:null),e(this).data("bs.dropdown",i)),"string"==typeof n){if(void 0===i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},t._clearMenus=function(n){if(!n||3!==n.which&&("keyup"!==n.type||9===n.which))for(var i=[].slice.call(document.querySelectorAll('[data-toggle="dropdown"]')),o=0,r=i.length;o0&&s--,40===n.which&&sdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:M,popperConfig:null},K={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},$=function(){function t(t,e){if(void 0===n)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var i=t.prototype;return i.enable=function(){this._isEnabled=!0},i.disable=function(){this._isEnabled=!1},i.toggleEnabled=function(){this._isEnabled=!this._isEnabled},i.toggle=function(t){if(this._isEnabled)if(t){var n=this.constructor.DATA_KEY,i=e(t.currentTarget).data(n);i||(i=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(e(this.getTipElement()).hasClass("show"))return void this._leave(null,this);this._enter(null,this)}},i.dispose=function(){clearTimeout(this._timeout),e.removeData(this.element,this.constructor.DATA_KEY),e(this.element).off(this.constructor.EVENT_KEY),e(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&e(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},i.show=function(){var t=this;if("none"===e(this.element).css("display"))throw new Error("Please use show on visible elements");var i=e.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){e(this.element).trigger(i);var o=c.findShadowRoot(this.element),r=e.contains(null!==o?o:this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!r)return;var s=this.getTipElement(),a=c.getUID(this.constructor.NAME);s.setAttribute("id",a),this.element.setAttribute("aria-describedby",a),this.setContent(),this.config.animation&&e(s).addClass("fade");var l="function"==typeof this.config.placement?this.config.placement.call(this,s,this.element):this.config.placement,u=this._getAttachment(l);this.addAttachmentClass(u);var h=this._getContainer();e(s).data(this.constructor.DATA_KEY,this),e.contains(this.element.ownerDocument.documentElement,this.tip)||e(s).appendTo(h),e(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,s,this._getPopperConfig(u)),e(s).addClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().on("mouseover",null,e.noop);var f=function(){t.config.animation&&t._fixTransition();var n=t._hoverState;t._hoverState=null,e(t.element).trigger(t.constructor.Event.SHOWN),"out"===n&&t._leave(null,t)};if(e(this.tip).hasClass("fade")){var d=c.getTransitionDurationFromElement(this.tip);e(this.tip).one(c.TRANSITION_END,f).emulateTransitionEnd(d)}else f()}},i.hide=function(t){var n=this,i=this.getTipElement(),o=e.Event(this.constructor.Event.HIDE),r=function(){"show"!==n._hoverState&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),e(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()};if(e(this.element).trigger(o),!o.isDefaultPrevented()){if(e(i).removeClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().off("mouseover",null,e.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,e(this.tip).hasClass("fade")){var s=c.getTransitionDurationFromElement(i);e(i).one(c.TRANSITION_END,r).emulateTransitionEnd(s)}else r();this._hoverState=""}},i.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},i.isWithContent=function(){return Boolean(this.getTitle())},i.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-tooltip-"+t)},i.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},i.setContent=function(){var t=this.getTipElement();this.setElementContent(e(t.querySelectorAll(".tooltip-inner")),this.getTitle()),e(t).removeClass("fade show")},i.setElementContent=function(t,n){"object"!=typeof n||!n.nodeType&&!n.jquery?this.config.html?(this.config.sanitize&&(n=q(n,this.config.whiteList,this.config.sanitizeFn)),t.html(n)):t.text(n):this.config.html?e(n).parent().is(t)||t.empty().append(n):t.text(e(n).text())},i.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},i._getPopperConfig=function(t){var e=this;return a(a({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),this.config.popperConfig)},i._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=a(a({},e.offsets),t.config.offset(e.offsets,t.element)||{}),e}:e.offset=this.config.offset,e},i._getContainer=function(){return!1===this.config.container?document.body:c.isElement(this.config.container)?e(this.config.container):e(document).find(this.config.container)},i._getAttachment=function(t){return z[t.toUpperCase()]},i._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(n){if("click"===n)e(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==n){var i="hover"===n?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o="hover"===n?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;e(t.element).on(i,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},e(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=a(a({},this.config),{},{trigger:"manual",selector:""}):this._fixTitle()},i._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},i._enter=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e(n.getTipElement()).hasClass("show")||"show"===n._hoverState?n._hoverState="show":(clearTimeout(n._timeout),n._hoverState="show",n.config.delay&&n.config.delay.show?n._timeout=setTimeout((function(){"show"===n._hoverState&&n.show()}),n.config.delay.show):n.show())},i._leave=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState="out",n.config.delay&&n.config.delay.hide?n._timeout=setTimeout((function(){"out"===n._hoverState&&n.hide()}),n.config.delay.hide):n.hide())},i._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},i._getConfig=function(t){var n=e(this.element).data();return Object.keys(n).forEach((function(t){-1!==V.indexOf(t)&&delete n[t]})),"number"==typeof(t=a(a(a({},this.constructor.Default),n),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),c.typeCheckConfig(Q,t,this.constructor.DefaultType),t.sanitize&&(t.template=q(t.template,t.whiteList,t.sanitizeFn)),t},i._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},i._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(U);null!==n&&n.length&&t.removeClass(n.join(""))},i._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},i._fixTransition=function(){var t=this.getTipElement(),n=this.config.animation;null===t.getAttribute("x-placement")&&(e(t).removeClass("fade"),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.tooltip"),o="object"==typeof n&&n;if((i||!/dispose|hide/.test(n))&&(i||(i=new t(this,o),e(this).data("bs.tooltip",i)),"string"==typeof n)){if(void 0===i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return X}},{key:"NAME",get:function(){return Q}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return K}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Y}}]),t}();e.fn[Q]=$._jQueryInterface,e.fn[Q].Constructor=$,e.fn[Q].noConflict=function(){return e.fn[Q]=W,$._jQueryInterface};var G="popover",J=e.fn[G],Z=new RegExp("(^|\\s)bs-popover\\S+","g"),tt=a(a({},$.Default),{},{placement:"right",trigger:"click",content:"",template:''}),et=a(a({},$.DefaultType),{},{content:"(string|element|function)"}),nt={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},it=function(t){var n,i;function r(){return t.apply(this,arguments)||this}i=t,(n=r).prototype=Object.create(i.prototype),n.prototype.constructor=n,n.__proto__=i;var s=r.prototype;return s.isWithContent=function(){return this.getTitle()||this._getContent()},s.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},s.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},s.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(".popover-body"),n),t.removeClass("fade show")},s._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},s._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(Z);null!==n&&n.length>0&&t.removeClass(n.join(""))},r._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new r(this,i),e(this).data("bs.popover",n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(r,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return tt}},{key:"NAME",get:function(){return G}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return nt}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return et}}]),r}($);e.fn[G]=it._jQueryInterface,e.fn[G].Constructor=it,e.fn[G].noConflict=function(){return e.fn[G]=J,it._jQueryInterface};var ot="scrollspy",rt=e.fn[ot],st={offset:10,method:"auto",target:""},at={offset:"number",method:"string",target:"(string|element)"},lt=function(){function t(t,n){var i=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(n),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,e(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return i._process(t)})),this.refresh(),this._process()}var n=t.prototype;return n.refresh=function(){var t=this,n=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?n:this._config.method,o="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var n,r=c.getSelectorFromElement(t);if(r&&(n=document.querySelector(r)),n){var s=n.getBoundingClientRect();if(s.width||s.height)return[e(n)[i]().top+o,r]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){e.removeData(this._element,"bs.scrollspy"),e(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=a(a({},st),"object"==typeof t&&t?t:{})).target&&c.isElement(t.target)){var n=e(t.target).attr("id");n||(n=c.getUID(ot),e(t.target).attr("id",n)),t.target="#"+n}return c.typeCheckConfig(ot,t,at),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t li > .active":".active";i=(i=e.makeArray(e(o).find(s)))[i.length-1]}var a=e.Event("hide.bs.tab",{relatedTarget:this._element}),l=e.Event("show.bs.tab",{relatedTarget:i});if(i&&e(i).trigger(a),e(this._element).trigger(l),!l.isDefaultPrevented()&&!a.isDefaultPrevented()){r&&(n=document.querySelector(r)),this._activate(this._element,o);var u=function(){var n=e.Event("hidden.bs.tab",{relatedTarget:t._element}),o=e.Event("shown.bs.tab",{relatedTarget:i});e(i).trigger(n),e(t._element).trigger(o)};n?this._activate(n,n.parentNode,u):u()}}},n.dispose=function(){e.removeData(this._element,"bs.tab"),this._element=null},n._activate=function(t,n,i){var o=this,r=(!n||"UL"!==n.nodeName&&"OL"!==n.nodeName?e(n).children(".active"):e(n).find("> li > .active"))[0],s=i&&r&&e(r).hasClass("fade"),a=function(){return o._transitionComplete(t,r,i)};if(r&&s){var l=c.getTransitionDurationFromElement(r);e(r).removeClass("show").one(c.TRANSITION_END,a).emulateTransitionEnd(l)}else a()},n._transitionComplete=function(t,n,i){if(n){e(n).removeClass("active");var o=e(n.parentNode).find("> .dropdown-menu .active")[0];o&&e(o).removeClass("active"),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(e(t).addClass("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),c.reflow(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&e(t.parentNode).hasClass("dropdown-menu")){var r=e(t).closest(".dropdown")[0];if(r){var s=[].slice.call(r.querySelectorAll(".dropdown-toggle"));e(s).addClass("active")}t.setAttribute("aria-expanded",!0)}i&&i()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.tab");if(o||(o=new t(this),i.data("bs.tab",o)),"string"==typeof n){if(void 0===o[n])throw new TypeError('No method named "'+n+'"');o[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ut._jQueryInterface.call(e(this),"show")})),e.fn.tab=ut._jQueryInterface,e.fn.tab.Constructor=ut,e.fn.tab.noConflict=function(){return e.fn.tab=ct,ut._jQueryInterface};var ht=e.fn.toast,ft={animation:"boolean",autohide:"boolean",delay:"number"},dt={animation:!0,autohide:!0,delay:500},pt=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var n=t.prototype;return n.show=function(){var t=this,n=e.Event("show.bs.toast");if(e(this._element).trigger(n),!n.isDefaultPrevented()){this._config.animation&&this._element.classList.add("fade");var i=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),e(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),c.reflow(this._element),this._element.classList.add("showing"),this._config.animation){var o=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,i).emulateTransitionEnd(o)}else i()}},n.hide=function(){if(this._element.classList.contains("show")){var t=e.Event("hide.bs.toast");e(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},n.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains("show")&&this._element.classList.remove("show"),e(this._element).off("click.dismiss.bs.toast"),e.removeData(this._element,"bs.toast"),this._element=null,this._config=null},n._getConfig=function(t){return t=a(a(a({},dt),e(this._element).data()),"object"==typeof t&&t?t:{}),c.typeCheckConfig("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;e(this._element).on("click.dismiss.bs.toast",'[data-dismiss="toast"]',(function(){return t.hide()}))},n._close=function(){var t=this,n=function(){t._element.classList.add("hide"),e(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var i=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,n).emulateTransitionEnd(i)}else n()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.toast");if(o||(o=new t(this,"object"==typeof n&&n),i.data("bs.toast",o)),"string"==typeof n){if(void 0===o[n])throw new TypeError('No method named "'+n+'"');o[n](this)}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"DefaultType",get:function(){return ft}},{key:"Default",get:function(){return dt}}]),t}();e.fn.toast=pt._jQueryInterface,e.fn.toast.Constructor=pt,e.fn.toast.noConflict=function(){return e.fn.toast=ht,pt._jQueryInterface},t.Alert=f,t.Button=p,t.Carousel=w,t.Collapse=D,t.Dropdown=x,t.Modal=F,t.Popover=it,t.Scrollspy=lt,t.Tab=ut,t.Toast=pt,t.Tooltip=$,t.Util=c,Object.defineProperty(t,"__esModule",{value:!0})}(e,n(0),n(1))},function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e,n){"use strict";n.r(e);n(0),n(3),n.p;$(document).ready(()=>{!function(){var t=document.getElementById("bd-docs-nav");let e=parseInt(sessionStorage.getItem("sidebar-scroll-top"),10);if(isNaN(e)){var n,i=t.querySelectorAll(".active"),o=0;for(n=i.length-1;n>0;n--){var r=i[n];void 0!==r&&(o+=r.offsetTop)}o-=t.offsetTop,void 0!==r&&o>.5*t.clientHeight&&(t.scrollTop=o-.2*t.clientHeight)}else t.scrollTop=e;window.addEventListener("beforeunload",()=>{sessionStorage.setItem("sidebar-scroll-top",t.scrollTop)})}(),$(window).on("activate.bs.scrollspy",(function(){document.querySelectorAll("#bd-toc-nav a").forEach(t=>{t.parentElement.classList.remove("active")}),document.querySelectorAll("#bd-toc-nav a.active").forEach(t=>{t.parentElement.classList.add("active")})}))})}]); \ No newline at end of file diff --git a/0.4/_static/language_data.js b/0.4/_static/language_data.js new file mode 100644 index 0000000..250f566 --- /dev/null +++ b/0.4/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/0.4/_static/minus.png b/0.4/_static/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..d96755fdaf8bb2214971e0db9c1fd3077d7c419d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu=nj kDsEF_5m^0CR;1wuP-*O&G^0G}KYk!hp00i_>zopr08q^qX#fBK literal 0 HcmV?d00001 diff --git a/0.4/_static/nbsphinx-broken-thumbnail.svg b/0.4/_static/nbsphinx-broken-thumbnail.svg new file mode 100644 index 0000000..4919ca8 --- /dev/null +++ b/0.4/_static/nbsphinx-broken-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/0.4/_static/nbsphinx-code-cells.css b/0.4/_static/nbsphinx-code-cells.css new file mode 100644 index 0000000..a3fb27c --- /dev/null +++ b/0.4/_static/nbsphinx-code-cells.css @@ -0,0 +1,259 @@ +/* remove conflicting styling from Sphinx themes */ +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt *, +div.nbinput.container div.input_area pre, +div.nboutput.container div.output_area pre, +div.nbinput.container div.input_area .highlight, +div.nboutput.container div.output_area .highlight { + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +div.nbinput.container > div[class*=highlight], +div.nboutput.container > div[class*=highlight] { + margin: 0; +} + +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt * { + background: none; +} + +div.nboutput.container div.output_area .highlight, +div.nboutput.container div.output_area pre { + background: unset; +} + +div.nboutput.container div.output_area div.highlight { + color: unset; /* override Pygments text color */ +} + +/* avoid gaps between output lines */ +div.nboutput.container div[class*=highlight] pre { + line-height: normal; +} + +/* input/output containers */ +div.nbinput.container, +div.nboutput.container { + display: -webkit-flex; + display: flex; + align-items: flex-start; + margin: 0; + width: 100%; +} +@media (max-width: 540px) { + div.nbinput.container, + div.nboutput.container { + flex-direction: column; + } +} + +/* input container */ +div.nbinput.container { + padding-top: 5px; +} + +/* last container */ +div.nblast.container { + padding-bottom: 5px; +} + +/* input prompt */ +div.nbinput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nbinput.container div.prompt pre > code { + color: #307FC1; +} + +/* output prompt */ +div.nboutput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nboutput.container div.prompt pre > code { + color: #BF5B3D; +} + +/* all prompts */ +div.nbinput.container div.prompt, +div.nboutput.container div.prompt { + width: 4.5ex; + padding-top: 5px; + position: relative; + user-select: none; +} + +div.nbinput.container div.prompt > div, +div.nboutput.container div.prompt > div { + position: absolute; + right: 0; + margin-right: 0.3ex; +} + +@media (max-width: 540px) { + div.nbinput.container div.prompt, + div.nboutput.container div.prompt { + width: unset; + text-align: left; + padding: 0.4em; + } + div.nboutput.container div.prompt.empty { + padding: 0; + } + + div.nbinput.container div.prompt > div, + div.nboutput.container div.prompt > div { + position: unset; + } +} + +/* disable scrollbars and line breaks on prompts */ +div.nbinput.container div.prompt pre, +div.nboutput.container div.prompt pre { + overflow: hidden; + white-space: pre; +} + +/* input/output area */ +div.nbinput.container div.input_area, +div.nboutput.container div.output_area { + -webkit-flex: 1; + flex: 1; + overflow: auto; +} +@media (max-width: 540px) { + div.nbinput.container div.input_area, + div.nboutput.container div.output_area { + width: 100%; + } +} + +/* input area */ +div.nbinput.container div.input_area { + border: 1px solid #e0e0e0; + border-radius: 2px; + /*background: #f5f5f5;*/ +} + +/* override MathJax center alignment in output cells */ +div.nboutput.container div[class*=MathJax] { + text-align: left !important; +} + +/* override sphinx.ext.imgmath center alignment in output cells */ +div.nboutput.container div.math p { + text-align: left; +} + +/* standard error */ +div.nboutput.container div.output_area.stderr { + background: #fdd; +} + +/* ANSI colors */ +.ansi-black-fg { color: #3E424D; } +.ansi-black-bg { background-color: #3E424D; } +.ansi-black-intense-fg { color: #282C36; } +.ansi-black-intense-bg { background-color: #282C36; } +.ansi-red-fg { color: #E75C58; } +.ansi-red-bg { background-color: #E75C58; } +.ansi-red-intense-fg { color: #B22B31; } +.ansi-red-intense-bg { background-color: #B22B31; } +.ansi-green-fg { color: #00A250; } +.ansi-green-bg { background-color: #00A250; } +.ansi-green-intense-fg { color: #007427; } +.ansi-green-intense-bg { background-color: #007427; } +.ansi-yellow-fg { color: #DDB62B; } +.ansi-yellow-bg { background-color: #DDB62B; } +.ansi-yellow-intense-fg { color: #B27D12; } +.ansi-yellow-intense-bg { background-color: #B27D12; } +.ansi-blue-fg { color: #208FFB; } +.ansi-blue-bg { background-color: #208FFB; } +.ansi-blue-intense-fg { color: #0065CA; } +.ansi-blue-intense-bg { background-color: #0065CA; } +.ansi-magenta-fg { color: #D160C4; } +.ansi-magenta-bg { background-color: #D160C4; } +.ansi-magenta-intense-fg { color: #A03196; } +.ansi-magenta-intense-bg { background-color: #A03196; } +.ansi-cyan-fg { color: #60C6C8; } +.ansi-cyan-bg { background-color: #60C6C8; } +.ansi-cyan-intense-fg { color: #258F8F; } +.ansi-cyan-intense-bg { background-color: #258F8F; } +.ansi-white-fg { color: #C5C1B4; } +.ansi-white-bg { background-color: #C5C1B4; } +.ansi-white-intense-fg { color: #A1A6B2; } +.ansi-white-intense-bg { background-color: #A1A6B2; } + +.ansi-default-inverse-fg { color: #FFFFFF; } +.ansi-default-inverse-bg { background-color: #000000; } + +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } + + +div.nbinput.container div.input_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight].math, +div.nboutput.container div.output_area.rendered_html, +div.nboutput.container div.output_area > div.output_javascript, +div.nboutput.container div.output_area:not(.rendered_html) > img{ + padding: 5px; + margin: 0; +} + +/* fix copybtn overflow problem in chromium (needed for 'sphinx_copybutton') */ +div.nbinput.container div.input_area > div[class^='highlight'], +div.nboutput.container div.output_area > div[class^='highlight']{ + overflow-y: hidden; +} + +/* hide copy button on prompts for 'sphinx_copybutton' extension ... */ +.prompt .copybtn, +/* ... and 'sphinx_immaterial' theme */ +.prompt .md-clipboard.md-icon { + display: none; +} + +/* Some additional styling taken form the Jupyter notebook CSS */ +.jp-RenderedHTMLCommon table, +div.rendered_html table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 12px; + table-layout: fixed; +} +.jp-RenderedHTMLCommon thead, +div.rendered_html thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} +.jp-RenderedHTMLCommon tr, +.jp-RenderedHTMLCommon th, +.jp-RenderedHTMLCommon td, +div.rendered_html tr, +div.rendered_html th, +div.rendered_html td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +.jp-RenderedHTMLCommon th, +div.rendered_html th { + font-weight: bold; +} +.jp-RenderedHTMLCommon tbody tr:nth-child(odd), +div.rendered_html tbody tr:nth-child(odd) { + background: #f5f5f5; +} +.jp-RenderedHTMLCommon tbody tr:hover, +div.rendered_html tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + diff --git a/0.4/_static/nbsphinx-gallery.css b/0.4/_static/nbsphinx-gallery.css new file mode 100644 index 0000000..365c27a --- /dev/null +++ b/0.4/_static/nbsphinx-gallery.css @@ -0,0 +1,31 @@ +.nbsphinx-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 5px; + margin-top: 1em; + margin-bottom: 1em; +} + +.nbsphinx-gallery > a { + padding: 5px; + border: 1px dotted currentColor; + border-radius: 2px; + text-align: center; +} + +.nbsphinx-gallery > a:hover { + border-style: solid; +} + +.nbsphinx-gallery img { + max-width: 100%; + max-height: 100%; +} + +.nbsphinx-gallery > a > div:first-child { + display: flex; + align-items: start; + justify-content: center; + height: 120px; + margin-bottom: 5px; +} diff --git a/0.4/_static/nbsphinx-no-thumbnail.svg b/0.4/_static/nbsphinx-no-thumbnail.svg new file mode 100644 index 0000000..9dca758 --- /dev/null +++ b/0.4/_static/nbsphinx-no-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/0.4/_static/plus.png b/0.4/_static/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..7107cec93a979b9a5f64843235a16651d563ce2d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu>-2 m3q%Vub%g%s<8sJhVPMczOq}xhg9DJoz~JfX=d#Wzp$Pyb1r*Kz literal 0 HcmV?d00001 diff --git a/0.4/_static/pygments.css b/0.4/_static/pygments.css new file mode 100644 index 0000000..f227e5c --- /dev/null +++ b/0.4/_static/pygments.css @@ -0,0 +1,83 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #204a87; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #000000; font-style: italic } /* Generic.Output */ +.highlight .gp { color: #8f5902 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #204a87 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8 } /* Text.Whitespace */ +.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */ +.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ +.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ +.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ +.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/0.4/_static/searchtools.js b/0.4/_static/searchtools.js new file mode 100644 index 0000000..7918c3f --- /dev/null +++ b/0.4/_static/searchtools.js @@ -0,0 +1,574 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + `Search finished, found ${resultCount} page(s) matching the search query.` + ); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() }); + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent !== undefined) return docContent.textContent; + console.warn( + "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + /** + * execute search (requires search index to be loaded) + */ + query: (query) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + // array of [docname, title, anchor, descr, score, filename] + let results = []; + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + let score = Math.round(100 * queryLower.length / title.length) + results.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id] of foundEntries) { + let score = Math.round(100 * queryLower.length / entry.length) + results.push([ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // lookup as object + objectTerms.forEach((term) => + results.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + results.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item))); + + // now sort the results by score (in opposite order of appearance, since the + // display function below uses pop() to retrieve items) and then + // alphabetically + results.sort((a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; + }); + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + results = results.reverse(); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord) && !terms[word]) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord) && !titleTerms[word]) + arr.push({ files: titleTerms[word], score: Scorer.partialTitle }); + }); + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); + else fileMap.set(file, [word]); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords) => { + const text = Search.htmlToText(htmlText); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/0.4/_static/sphinx-book-theme.12a9622fbb08dcb3a2a40b2c02b83a57.js b/0.4/_static/sphinx-book-theme.12a9622fbb08dcb3a2a40b2c02b83a57.js new file mode 100644 index 0000000..b8b8704 --- /dev/null +++ b/0.4/_static/sphinx-book-theme.12a9622fbb08dcb3a2a40b2c02b83a57.js @@ -0,0 +1,18 @@ +var initTriggerNavBar=()=>{if($(window).width()<768){$("#navbar-toggler").trigger("click")}} +var scrollToActive=()=>{var navbar=document.getElementById('site-navigation') +var active_pages=navbar.querySelectorAll(".active") +var active_page=active_pages[active_pages.length-1] +if(active_page!==undefined&&active_page.offsetTop>($(window).height()*.5)){navbar.scrollTop=active_page.offsetTop-($(window).height()*.2)}} +var sbRunWhenDOMLoaded=cb=>{if(document.readyState!='loading'){cb()}else if(document.addEventListener){document.addEventListener('DOMContentLoaded',cb)}else{document.attachEvent('onreadystatechange',function(){if(document.readyState=='complete')cb()})}} +function toggleFullScreen(){var navToggler=$("#navbar-toggler");if(!document.fullscreenElement){document.documentElement.requestFullscreen();if(!navToggler.hasClass("collapsed")){navToggler.click();}}else{if(document.exitFullscreen){document.exitFullscreen();if(navToggler.hasClass("collapsed")){navToggler.click();}}}} +var initTooltips=()=>{$(document).ready(function(){$('[data-toggle="tooltip"]').tooltip();});} +var initTocHide=()=>{var scrollTimeout;var throttle=200;var tocHeight=$("#bd-toc-nav").outerHeight(true)+$(".bd-toc").outerHeight(true);var hideTocAfter=tocHeight+200;var checkTocScroll=function(){var margin_content=$(".margin, .tag_margin, .full-width, .full_width, .tag_full-width, .tag_full_width, .sidebar, .tag_sidebar, .popout, .tag_popout");margin_content.each((index,item)=>{var topOffset=$(item).offset().top-$(window).scrollTop();var bottomOffset=topOffset+$(item).outerHeight(true);var topOverlaps=((topOffset>=0)&&(topOffset=0)&&(bottomOffset20){$("div.bd-toc").removeClass("show") +return false}else{$("div.bd-toc").addClass("show")};})};var manageScrolledClassOnBody=function(){if(window.scrollY>0){document.body.classList.add("scrolled");}else{document.body.classList.remove("scrolled");}} +$(window).on('scroll',function(){if(!scrollTimeout){scrollTimeout=setTimeout(function(){checkTocScroll();manageScrolledClassOnBody();scrollTimeout=null;},throttle);}});} +var initThebeSBT=()=>{var title=$("div.section h1")[0] +if(!$(title).next().hasClass("thebe-launch-button")){$("").insertAfter($(title))} +initThebe();} +sbRunWhenDOMLoaded(initTooltips) +sbRunWhenDOMLoaded(initTriggerNavBar) +sbRunWhenDOMLoaded(scrollToActive) +sbRunWhenDOMLoaded(initTocHide) diff --git a/0.4/_static/sphinx-book-theme.acff12b8f9c144ce68a297486a2fa670.css b/0.4/_static/sphinx-book-theme.acff12b8f9c144ce68a297486a2fa670.css new file mode 100644 index 0000000..6c5887c --- /dev/null +++ b/0.4/_static/sphinx-book-theme.acff12b8f9c144ce68a297486a2fa670.css @@ -0,0 +1,5 @@ +/*! sphinx-book-theme CSS + * BSD 3-Clause License + * Copyright (c) 2020, EBP + * All rights reserved. + */:root{--color-primary: 0, 123, 255;--color-info: 255, 193, 7;--color-warning: 253, 126, 20;--color-danger: 220, 53, 69}body{padding-top:0px !important}body img{max-width:100%}code{font-size:87.5% !important}main.bd-content{padding-top:3em !important;padding-bottom:0px !important}main.bd-content #main-content{padding-top:1.5em}main.bd-content #main-content a.headerlink{opacity:0;margin-left:.2em}main.bd-content #main-content a.headerlink:hover{background-color:transparent;color:#0071bc;opacity:1 !important}main.bd-content #main-content a,main.bd-content #main-content a:visited{color:#0071bc}main.bd-content #main-content h1,main.bd-content #main-content h2,main.bd-content #main-content h3,main.bd-content #main-content h4,main.bd-content #main-content h5{color:black}main.bd-content #main-content h1:hover a.headerlink,main.bd-content #main-content h2:hover a.headerlink,main.bd-content #main-content h3:hover a.headerlink,main.bd-content #main-content h4:hover a.headerlink,main.bd-content #main-content h5:hover a.headerlink{opacity:.5}main.bd-content #main-content h1 a.toc-backref,main.bd-content #main-content h2 a.toc-backref,main.bd-content #main-content h3 a.toc-backref,main.bd-content #main-content h4 a.toc-backref,main.bd-content #main-content h5 a.toc-backref{color:inherit}main.bd-content #main-content>div>div>div.section,main.bd-content #main-content .prev-next-bottom{padding-right:1em}main.bd-content #main-content div.section{overflow:visible !important}main.bd-content #main-content div.section ul p,main.bd-content #main-content div.section ol p{margin-bottom:0}main.bd-content #main-content span.eqno{float:right;font-size:1.2em}main.bd-content #main-content div.math{overflow-x:auto}main.bd-content #main-content img.align-center{margin-left:auto;margin-right:auto;display:block}main.bd-content #main-content img.align-left{clear:left;float:left;margin-right:1em}main.bd-content #main-content img.align-right{clear:right;float:right;margin-left:1em}main.bd-content #main-content div.figure{width:100%;margin-bottom:1em;text-align:center}main.bd-content #main-content div.figure.align-left{text-align:left}main.bd-content #main-content div.figure.align-left p.caption{margin-left:0}main.bd-content #main-content div.figure.align-right{text-align:right}main.bd-content #main-content div.figure.align-right p.caption{margin-right:0}main.bd-content #main-content div.figure p.caption{margin:.5em 10%}main.bd-content #main-content div.figure.margin p.caption,main.bd-content #main-content div.figure.margin-caption p.caption{margin:.5em 0}main.bd-content #main-content div.figure.margin-caption p.caption{text-align:left}main.bd-content #main-content div.figure span.caption-number{font-weight:bold}main.bd-content #main-content div.figure span{font-size:.9rem}main.bd-content #main-content div.contents{padding:1em}main.bd-content #main-content div.contents p.topic-title{font-size:1.5em;padding:.5em 0 0 1em}main.bd-content #main-content p.centered{text-align:center}main.bd-content #main-content div.sphinx-tabs>div.sphinx-menu{padding:0}main.bd-content #main-content div.sphinx-tabs>div.sphinx-menu>a.item{width:auto;margin:0px 0px -1px 0px}main.bd-content #main-content span.brackets:before,main.bd-content #main-content a.brackets:before{content:"["}main.bd-content #main-content span.brackets:after,main.bd-content #main-content a.brackets:after{content:"]"}main.bd-content #main-content .footnote-reference,main.bd-content #main-content a.bibtex.internal{font-size:1em}main.bd-content #main-content dl.simple dd,main.bd-content #main-content dl.field-list dd{margin-left:1.5em}main.bd-content #main-content dl.simple dd:not(:last-child),main.bd-content #main-content dl.field-list dd:not(:last-child){margin-bottom:0px}main.bd-content #main-content dl.simple dd:not(:last-child) p:last-child,main.bd-content #main-content dl.field-list dd:not(:last-child) p:last-child{margin-bottom:0px}main.bd-content #main-content dl.glossary dd{margin-left:1.5em}main.bd-content #main-content dl.footnote span.fn-backref{font-size:1em;padding-left:.1em}main.bd-content #main-content dl.footnote dd{font-size:.9em;margin-left:3em}main.bd-content #main-content dl.citation{margin-left:3em}main.bd-content #main-content dl.footnote dt.label{float:left}main.bd-content #main-content dl.footnote dd p{padding-left:1.5em}main.bd-content #main-content dl.module,main.bd-content #main-content dl.class,main.bd-content #main-content dl.exception,main.bd-content #main-content dl.function,main.bd-content #main-content dl.decorator,main.bd-content #main-content dl.data,main.bd-content #main-content dl.method,main.bd-content #main-content dl.attribute{margin-bottom:24px}main.bd-content #main-content dl.module dt,main.bd-content #main-content dl.class dt,main.bd-content #main-content dl.exception dt,main.bd-content #main-content dl.function dt,main.bd-content #main-content dl.decorator dt,main.bd-content #main-content dl.data dt,main.bd-content #main-content dl.method dt,main.bd-content #main-content dl.attribute dt{font-weight:bold}main.bd-content #main-content dl.module dt .headerlink,main.bd-content #main-content dl.class dt .headerlink,main.bd-content #main-content dl.exception dt .headerlink,main.bd-content #main-content dl.function dt .headerlink,main.bd-content #main-content dl.decorator dt .headerlink,main.bd-content #main-content dl.data dt .headerlink,main.bd-content #main-content dl.method dt .headerlink,main.bd-content #main-content dl.attribute dt .headerlink{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:inherit;visibility:hidden;font-size:14px}main.bd-content #main-content dl.module dt .headerlink:before,main.bd-content #main-content dl.class dt .headerlink:before,main.bd-content #main-content dl.exception dt .headerlink:before,main.bd-content #main-content dl.function dt .headerlink:before,main.bd-content #main-content dl.decorator dt .headerlink:before,main.bd-content #main-content dl.data dt .headerlink:before,main.bd-content #main-content dl.method dt .headerlink:before,main.bd-content #main-content dl.attribute dt .headerlink:before{-webkit-font-smoothing:antialiased;font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}main.bd-content #main-content dl.module dt .headerlink:after,main.bd-content #main-content dl.class dt .headerlink:after,main.bd-content #main-content dl.exception dt .headerlink:after,main.bd-content #main-content dl.function dt .headerlink:after,main.bd-content #main-content dl.decorator dt .headerlink:after,main.bd-content #main-content dl.data dt .headerlink:after,main.bd-content #main-content dl.method dt .headerlink:after,main.bd-content #main-content dl.attribute dt .headerlink:after{content:"";font-family:FontAwesome}main.bd-content #main-content dl.module dt .fa-pull-left.headerlink,main.bd-content #main-content dl.class dt .fa-pull-left.headerlink,main.bd-content #main-content dl.exception dt .fa-pull-left.headerlink,main.bd-content #main-content dl.function dt .fa-pull-left.headerlink,main.bd-content #main-content dl.decorator dt .fa-pull-left.headerlink,main.bd-content #main-content dl.data dt .fa-pull-left.headerlink,main.bd-content #main-content dl.method dt .fa-pull-left.headerlink,main.bd-content #main-content dl.attribute dt .fa-pull-left.headerlink{margin-right:.3em}main.bd-content #main-content dl.module dt .fa-pull-right.headerlink,main.bd-content #main-content dl.class dt .fa-pull-right.headerlink,main.bd-content #main-content dl.exception dt .fa-pull-right.headerlink,main.bd-content #main-content dl.function dt .fa-pull-right.headerlink,main.bd-content #main-content dl.decorator dt .fa-pull-right.headerlink,main.bd-content #main-content dl.data dt .fa-pull-right.headerlink,main.bd-content #main-content dl.method dt .fa-pull-right.headerlink,main.bd-content #main-content dl.attribute dt .fa-pull-right.headerlink{margin-left:.3em}main.bd-content #main-content dl.module dt .pull-left.headerlink,main.bd-content #main-content dl.class dt .pull-left.headerlink,main.bd-content #main-content dl.exception dt .pull-left.headerlink,main.bd-content #main-content dl.function dt .pull-left.headerlink,main.bd-content #main-content dl.decorator dt .pull-left.headerlink,main.bd-content #main-content dl.data dt .pull-left.headerlink,main.bd-content #main-content dl.method dt .pull-left.headerlink,main.bd-content #main-content dl.attribute dt .pull-left.headerlink{margin-right:.3em}main.bd-content #main-content dl.module dt .pull-right.headerlink,main.bd-content #main-content dl.class dt .pull-right.headerlink,main.bd-content #main-content dl.exception dt .pull-right.headerlink,main.bd-content #main-content dl.function dt .pull-right.headerlink,main.bd-content #main-content dl.decorator dt .pull-right.headerlink,main.bd-content #main-content dl.data dt .pull-right.headerlink,main.bd-content #main-content dl.method dt .pull-right.headerlink,main.bd-content #main-content dl.attribute dt .pull-right.headerlink{margin-left:.3em}main.bd-content #main-content dl.module dt a .headerlink,main.bd-content #main-content dl.class dt a .headerlink,main.bd-content #main-content dl.exception dt a .headerlink,main.bd-content #main-content dl.function dt a .headerlink,main.bd-content #main-content dl.decorator dt a .headerlink,main.bd-content #main-content dl.data dt a .headerlink,main.bd-content #main-content dl.method dt a .headerlink,main.bd-content #main-content dl.attribute dt a .headerlink{display:inline-block;text-decoration:inherit}main.bd-content #main-content dl.module dt .btn .headerlink,main.bd-content #main-content dl.class dt .btn .headerlink,main.bd-content #main-content dl.exception dt .btn .headerlink,main.bd-content #main-content dl.function dt .btn .headerlink,main.bd-content #main-content dl.decorator dt .btn .headerlink,main.bd-content #main-content dl.data dt .btn .headerlink,main.bd-content #main-content dl.method dt .btn .headerlink,main.bd-content #main-content dl.attribute dt .btn .headerlink{display:inline}main.bd-content #main-content dl.module dt .btn .fa-large.headerlink,main.bd-content #main-content dl.class dt .btn .fa-large.headerlink,main.bd-content #main-content dl.exception dt .btn .fa-large.headerlink,main.bd-content #main-content dl.function dt .btn .fa-large.headerlink,main.bd-content #main-content dl.decorator dt .btn .fa-large.headerlink,main.bd-content #main-content dl.data dt .btn .fa-large.headerlink,main.bd-content #main-content dl.method dt .btn .fa-large.headerlink,main.bd-content #main-content dl.attribute dt .btn .fa-large.headerlink{line-height:.9em}main.bd-content #main-content dl.module dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.class dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.exception dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.function dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.decorator dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.data dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.method dt .btn .fa-spin.headerlink,main.bd-content #main-content dl.attribute dt .btn .fa-spin.headerlink{display:inline-block}main.bd-content #main-content dl.module dt .nav .headerlink,main.bd-content #main-content dl.class dt .nav .headerlink,main.bd-content #main-content dl.exception dt .nav .headerlink,main.bd-content #main-content dl.function dt .nav .headerlink,main.bd-content #main-content dl.decorator dt .nav .headerlink,main.bd-content #main-content dl.data dt .nav .headerlink,main.bd-content #main-content dl.method dt .nav .headerlink,main.bd-content #main-content dl.attribute dt .nav .headerlink{display:inline}main.bd-content #main-content dl.module dt .nav .fa-large.headerlink,main.bd-content #main-content dl.class dt .nav .fa-large.headerlink,main.bd-content #main-content dl.exception dt .nav .fa-large.headerlink,main.bd-content #main-content dl.function dt .nav .fa-large.headerlink,main.bd-content #main-content dl.decorator dt .nav .fa-large.headerlink,main.bd-content #main-content dl.data dt .nav .fa-large.headerlink,main.bd-content #main-content dl.method dt .nav .fa-large.headerlink,main.bd-content #main-content dl.attribute dt .nav .fa-large.headerlink{line-height:.9em}main.bd-content #main-content dl.module dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.class dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.exception dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.function dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.decorator dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.data dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.method dt .nav .fa-spin.headerlink,main.bd-content #main-content dl.attribute dt .nav .fa-spin.headerlink{display:inline-block}main.bd-content #main-content dl.module dt .btn.headerlink:before,main.bd-content #main-content dl.class dt .btn.headerlink:before,main.bd-content #main-content dl.exception dt .btn.headerlink:before,main.bd-content #main-content dl.function dt .btn.headerlink:before,main.bd-content #main-content dl.decorator dt .btn.headerlink:before,main.bd-content #main-content dl.data dt .btn.headerlink:before,main.bd-content #main-content dl.method dt .btn.headerlink:before,main.bd-content #main-content dl.attribute dt .btn.headerlink:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}main.bd-content #main-content dl.module dt .btn.headerlink:hover:before,main.bd-content #main-content dl.class dt .btn.headerlink:hover:before,main.bd-content #main-content dl.exception dt .btn.headerlink:hover:before,main.bd-content #main-content dl.function dt .btn.headerlink:hover:before,main.bd-content #main-content dl.decorator dt .btn.headerlink:hover:before,main.bd-content #main-content dl.data dt .btn.headerlink:hover:before,main.bd-content #main-content dl.method dt .btn.headerlink:hover:before,main.bd-content #main-content dl.attribute dt .btn.headerlink:hover:before{opacity:1}main.bd-content #main-content dl.module dt .btn-mini .headerlink:before,main.bd-content #main-content dl.class dt .btn-mini .headerlink:before,main.bd-content #main-content dl.exception dt .btn-mini .headerlink:before,main.bd-content #main-content dl.function dt .btn-mini .headerlink:before,main.bd-content #main-content dl.decorator dt .btn-mini .headerlink:before,main.bd-content #main-content dl.data dt .btn-mini .headerlink:before,main.bd-content #main-content dl.method dt .btn-mini .headerlink:before,main.bd-content #main-content dl.attribute dt .btn-mini .headerlink:before{font-size:14px;vertical-align:-15%}main.bd-content #main-content dl.module dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.class dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.exception dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.function dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.decorator dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.data dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.method dt .rst-versions .rst-current-version .headerlink,main.bd-content #main-content dl.attribute dt .rst-versions .rst-current-version .headerlink{color:#fcfcfc}main.bd-content #main-content dl.module dt:hover .headerlink:after,main.bd-content #main-content dl.class dt:hover .headerlink:after,main.bd-content #main-content dl.exception dt:hover .headerlink:after,main.bd-content #main-content dl.function dt:hover .headerlink:after,main.bd-content #main-content dl.decorator dt:hover .headerlink:after,main.bd-content #main-content dl.data dt:hover .headerlink:after,main.bd-content #main-content dl.method dt:hover .headerlink:after,main.bd-content #main-content dl.attribute dt:hover .headerlink:after{visibility:visible}main.bd-content #main-content dl.module p,main.bd-content #main-content dl.class p,main.bd-content #main-content dl.exception p,main.bd-content #main-content dl.function p,main.bd-content #main-content dl.decorator p,main.bd-content #main-content dl.data p,main.bd-content #main-content dl.method p,main.bd-content #main-content dl.attribute p{margin-bottom:12px !important}main.bd-content #main-content dl.module table,main.bd-content #main-content dl.class table,main.bd-content #main-content dl.exception table,main.bd-content #main-content dl.function table,main.bd-content #main-content dl.decorator table,main.bd-content #main-content dl.data table,main.bd-content #main-content dl.method table,main.bd-content #main-content dl.attribute table{margin-bottom:12px !important}main.bd-content #main-content dl.module ul,main.bd-content #main-content dl.class ul,main.bd-content #main-content dl.exception ul,main.bd-content #main-content dl.function ul,main.bd-content #main-content dl.decorator ul,main.bd-content #main-content dl.data ul,main.bd-content #main-content dl.method ul,main.bd-content #main-content dl.attribute ul{margin-bottom:12px !important}main.bd-content #main-content dl.module ol,main.bd-content #main-content dl.class ol,main.bd-content #main-content dl.exception ol,main.bd-content #main-content dl.function ol,main.bd-content #main-content dl.decorator ol,main.bd-content #main-content dl.data ol,main.bd-content #main-content dl.method ol,main.bd-content #main-content dl.attribute ol{margin-bottom:12px !important}main.bd-content #main-content dl.module dd,main.bd-content #main-content dl.class dd,main.bd-content #main-content dl.exception dd,main.bd-content #main-content dl.function dd,main.bd-content #main-content dl.decorator dd,main.bd-content #main-content dl.data dd,main.bd-content #main-content dl.method dd,main.bd-content #main-content dl.attribute dd{margin:0 0 12px 24px}main.bd-content #main-content dl.module:not(.docutils),main.bd-content #main-content dl.class:not(.docutils),main.bd-content #main-content dl.exception:not(.docutils),main.bd-content #main-content dl.function:not(.docutils),main.bd-content #main-content dl.decorator:not(.docutils),main.bd-content #main-content dl.data:not(.docutils),main.bd-content #main-content dl.method:not(.docutils),main.bd-content #main-content dl.attribute:not(.docutils){margin-bottom:24px}main.bd-content #main-content dl.module:not(.docutils) dt,main.bd-content #main-content dl.class:not(.docutils) dt,main.bd-content #main-content dl.exception:not(.docutils) dt,main.bd-content #main-content dl.function:not(.docutils) dt,main.bd-content #main-content dl.decorator:not(.docutils) dt,main.bd-content #main-content dl.data:not(.docutils) dt,main.bd-content #main-content dl.method:not(.docutils) dt,main.bd-content #main-content dl.attribute:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}main.bd-content #main-content dl.module:not(.docutils) dt:before,main.bd-content #main-content dl.class:not(.docutils) dt:before,main.bd-content #main-content dl.exception:not(.docutils) dt:before,main.bd-content #main-content dl.function:not(.docutils) dt:before,main.bd-content #main-content dl.decorator:not(.docutils) dt:before,main.bd-content #main-content dl.data:not(.docutils) dt:before,main.bd-content #main-content dl.method:not(.docutils) dt:before,main.bd-content #main-content dl.attribute:not(.docutils) dt:before{color:#6ab0de}main.bd-content #main-content dl.module:not(.docutils) dt .headerlink,main.bd-content #main-content dl.class:not(.docutils) dt .headerlink,main.bd-content #main-content dl.exception:not(.docutils) dt .headerlink,main.bd-content #main-content dl.function:not(.docutils) dt .headerlink,main.bd-content #main-content dl.decorator:not(.docutils) dt .headerlink,main.bd-content #main-content dl.data:not(.docutils) dt .headerlink,main.bd-content #main-content dl.method:not(.docutils) dt .headerlink,main.bd-content #main-content dl.attribute:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}main.bd-content #main-content dl.module:not(.docutils) dt:first-child,main.bd-content #main-content dl.class:not(.docutils) dt:first-child,main.bd-content #main-content dl.exception:not(.docutils) dt:first-child,main.bd-content #main-content dl.function:not(.docutils) dt:first-child,main.bd-content #main-content dl.decorator:not(.docutils) dt:first-child,main.bd-content #main-content dl.data:not(.docutils) dt:first-child,main.bd-content #main-content dl.method:not(.docutils) dt:first-child,main.bd-content #main-content dl.attribute:not(.docutils) dt:first-child{margin-top:0}main.bd-content #main-content dl.module:not(.docutils) dl dt,main.bd-content #main-content dl.class:not(.docutils) dl dt,main.bd-content #main-content dl.exception:not(.docutils) dl dt,main.bd-content #main-content dl.function:not(.docutils) dl dt,main.bd-content #main-content dl.decorator:not(.docutils) dl dt,main.bd-content #main-content dl.data:not(.docutils) dl dt,main.bd-content #main-content dl.method:not(.docutils) dl dt,main.bd-content #main-content dl.attribute:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}main.bd-content #main-content dl.module:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.class:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.exception:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.function:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.decorator:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.data:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.method:not(.docutils) dl dt .headerlink,main.bd-content #main-content dl.attribute:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}main.bd-content #main-content dl.module:not(.docutils) tt,main.bd-content #main-content dl.class:not(.docutils) tt,main.bd-content #main-content dl.exception:not(.docutils) tt,main.bd-content #main-content dl.function:not(.docutils) tt,main.bd-content #main-content dl.decorator:not(.docutils) tt,main.bd-content #main-content dl.data:not(.docutils) tt,main.bd-content #main-content dl.method:not(.docutils) tt,main.bd-content #main-content dl.attribute:not(.docutils) tt{font-weight:bold;font-weight:bold}main.bd-content #main-content dl.module:not(.docutils) code,main.bd-content #main-content dl.class:not(.docutils) code,main.bd-content #main-content dl.exception:not(.docutils) code,main.bd-content #main-content dl.function:not(.docutils) code,main.bd-content #main-content dl.decorator:not(.docutils) code,main.bd-content #main-content dl.data:not(.docutils) code,main.bd-content #main-content dl.method:not(.docutils) code,main.bd-content #main-content dl.attribute:not(.docutils) code{font-weight:bold}main.bd-content #main-content dl.module:not(.docutils) tt.descname,main.bd-content #main-content dl.class:not(.docutils) tt.descname,main.bd-content #main-content dl.exception:not(.docutils) tt.descname,main.bd-content #main-content dl.function:not(.docutils) tt.descname,main.bd-content #main-content dl.decorator:not(.docutils) tt.descname,main.bd-content #main-content dl.data:not(.docutils) tt.descname,main.bd-content #main-content dl.method:not(.docutils) tt.descname,main.bd-content #main-content dl.attribute:not(.docutils) tt.descname{background-color:transparent;background-color:transparent;border:none;border:none;padding:0;padding:0;font-size:100% !important;font-size:100% !important;font-weight:bold;font-weight:bold}main.bd-content #main-content dl.module:not(.docutils) tt.descclassname,main.bd-content #main-content dl.class:not(.docutils) tt.descclassname,main.bd-content #main-content dl.exception:not(.docutils) tt.descclassname,main.bd-content #main-content dl.function:not(.docutils) tt.descclassname,main.bd-content #main-content dl.decorator:not(.docutils) tt.descclassname,main.bd-content #main-content dl.data:not(.docutils) tt.descclassname,main.bd-content #main-content dl.method:not(.docutils) tt.descclassname,main.bd-content #main-content dl.attribute:not(.docutils) tt.descclassname{background-color:transparent;background-color:transparent;border:none;border:none;padding:0;padding:0;font-size:100% !important;font-size:100% !important}main.bd-content #main-content dl.module:not(.docutils) code.descname,main.bd-content #main-content dl.class:not(.docutils) code.descname,main.bd-content #main-content dl.exception:not(.docutils) code.descname,main.bd-content #main-content dl.function:not(.docutils) code.descname,main.bd-content #main-content dl.decorator:not(.docutils) code.descname,main.bd-content #main-content dl.data:not(.docutils) code.descname,main.bd-content #main-content dl.method:not(.docutils) code.descname,main.bd-content #main-content dl.attribute:not(.docutils) code.descname{background-color:transparent;border:none;padding:0;font-size:100% !important;font-weight:bold}main.bd-content #main-content dl.module:not(.docutils) code.descclassname,main.bd-content #main-content dl.class:not(.docutils) code.descclassname,main.bd-content #main-content dl.exception:not(.docutils) code.descclassname,main.bd-content #main-content dl.function:not(.docutils) code.descclassname,main.bd-content #main-content dl.decorator:not(.docutils) code.descclassname,main.bd-content #main-content dl.data:not(.docutils) code.descclassname,main.bd-content #main-content dl.method:not(.docutils) code.descclassname,main.bd-content #main-content dl.attribute:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}main.bd-content #main-content dl.module:not(.docutils) .optional,main.bd-content #main-content dl.class:not(.docutils) .optional,main.bd-content #main-content dl.exception:not(.docutils) .optional,main.bd-content #main-content dl.function:not(.docutils) .optional,main.bd-content #main-content dl.decorator:not(.docutils) .optional,main.bd-content #main-content dl.data:not(.docutils) .optional,main.bd-content #main-content dl.method:not(.docutils) .optional,main.bd-content #main-content dl.attribute:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}main.bd-content #main-content dl.module:not(.docutils) .property,main.bd-content #main-content dl.class:not(.docutils) .property,main.bd-content #main-content dl.exception:not(.docutils) .property,main.bd-content #main-content dl.function:not(.docutils) .property,main.bd-content #main-content dl.decorator:not(.docutils) .property,main.bd-content #main-content dl.data:not(.docutils) .property,main.bd-content #main-content dl.method:not(.docutils) .property,main.bd-content #main-content dl.attribute:not(.docutils) .property{display:inline-block;padding-right:8px}main.bd-content #main-content dl.module .viewcode-link,main.bd-content #main-content dl.class .viewcode-link,main.bd-content #main-content dl.exception .viewcode-link,main.bd-content #main-content dl.function .viewcode-link,main.bd-content #main-content dl.decorator .viewcode-link,main.bd-content #main-content dl.data .viewcode-link,main.bd-content #main-content dl.method .viewcode-link,main.bd-content #main-content dl.attribute .viewcode-link{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}div.cell div.cell_output{padding-right:0}div.cell.tag_output_scroll div.cell_output,div.cell.tag_scroll-output div.cell_output{max-height:24em;overflow-y:auto}div.cell.tag_scroll-input div.cell_input{max-height:24em;overflow-y:auto}.highlighttable .linenos{vertical-align:baseline}.toggle.admonition button.toggle-button{top:0.5em !important}.admonition.seealso{border-color:#28a7464b}.admonition.seealso .admonition-title{background-color:rgba(40,167,70,0.1)}.admonition.seealso .admonition-title:before{color:#28a745;content:"\f064"}button.toggle-button-hidden:before{bottom:0.2em !important}div.sidebar,div.margin,div.margin-caption p.caption,.cell.tag_popout,.cell.tag_margin{width:40%;float:right;border-left:1px #a4a6a7 solid;margin-left:0.5em;padding:.2em 0 .2em 1em}div.sidebar p,div.margin p,div.margin-caption p.caption p,.cell.tag_popout p,.cell.tag_margin p{margin-bottom:0}div.sidebar p.sidebar-title,div.margin p.sidebar-title,div.margin-caption p.caption p.sidebar-title,.cell.tag_popout p.sidebar-title,.cell.tag_margin p.sidebar-title{font-weight:bold;font-size:1.2em}@media (min-width: 768px){div.cell.tag_popout,div.cell.tag_margin,div.margin,div.margin-caption p.caption{border:none;clear:right;width:31% !important;margin:0 -35% 0 0 !important;padding:0 !important;font-size:0.9rem;line-height:1.3;vertical-align:baseline;position:relative}div.cell.tag_popout p,div.cell.tag_margin p,div.margin p,div.margin-caption p.caption p{margin-bottom:.5em}div.cell.tag_popout p.sidebar-title,div.cell.tag_margin p.sidebar-title,div.margin p.sidebar-title,div.margin-caption p.caption p.sidebar-title{font-size:1em}div.cell.tag_margin .cell_output{padding-left:0}div.sidebar:not(.margin){width:60%;margin-left:1.5em;margin-right:-28%}}@media (min-width: 768px){div.cell.tag_full-width,div.cell.tag_full_width,div.full_width,div.full-width{width:136% !important}}blockquote{margin:1em;padding:.2em 1.5em;border-left:4px solid #ccc}blockquote.pull-quote,blockquote.epigraph,blockquote.highlights{font-size:1.25em;border-left:none}blockquote div>p{margin-bottom:.5em}blockquote div>p+p.attribution{font-style:normal;font-size:.9em;text-align:right;color:#6c757d;padding-right:2em}div.highlight{background:none;margin-bottom:1em}div.cell div.highlight{margin-bottom:0em}.thebelab-cell{border:none !important}button.thebe-launch-button{height:2.5em;font-size:1em}div.tableofcontents-wrapper p.caption{font-weight:600 !important;margin-bottom:0em !important}.topbar,.topbar-contents,.topbar-main{height:3em}.topbar{background-color:white;transition:left .2s}.scrolled .topbar{box-shadow:0 6px 6px -6px rgba(0,0,0,0.3)}.topbar .topbar-main{padding-top:0.25rem;padding-bottom:0.25rem;padding-right:0}.topbar .topbar-main>button,.topbar .topbar-main>div,.topbar .topbar-main>a{float:left;height:100%}.topbar .topbar-main button.topbarbtn{margin:0 .1em;background-color:white;color:#5a5a5a;border:none;padding-top:.1rem;padding-bottom:.1rem;font-size:1.4em}.topbar .topbar-main button.topbarbtn i.fab{vertical-align:baseline;line-height:1}.topbar .topbar-main div.dropdown-buttons-trigger,.topbar .topbar-main a.edit-button,.topbar .topbar-main a.full-screen-button{float:right}.bd-topbar-whitespace{width:275px;flex:auto;transition:flex 0.2s ease 0s}@media (max-width: 768px){.bd-topbar-whitespace{border-bottom:1px solid transparent}.bd-topbar-whitespace.show,.bd-topbar-whitespace.collapsing,body.scrolled .bd-topbar-whitespace{border-color:rgba(0,0,0,0.1);position:absolute;bottom:0;width:100%;display:block}}span.topbar-button-text{margin-left:0.4em}@media (max-width: 768px){span.topbar-button-text{display:none}}div.dropdown-buttons-trigger div.dropdown-buttons{display:none;position:absolute;max-width:130px;margin-top:.2em;z-index:1000}div.dropdown-buttons-trigger div.dropdown-buttons.sourcebuttons .topbarbtn i{padding-right:6px;margin-left:-5px;font-size:.9em !important}div.dropdown-buttons-trigger div.dropdown-buttons button.topbarbtn{padding-top:.35rem;padding-bottom:.35rem;min-width:120px !important;border:1px white solid !important;background-color:#5a5a5a;color:white;font-size:1em}div.dropdown-buttons-trigger:hover div.dropdown-buttons{display:block}a.dropdown-buttons i{margin-right:.5em}button.topbarbtn img{height:1.15em;padding-right:6px;margin-left:-5px}#navbar-toggler{position:relative;margin-right:1em;margin-left:.5em;color:#5a5a5a}#navbar-toggler i{transition:opacity .3s, transform .3s;position:absolute;top:16%;left:0;display:block;font-size:1.2em}#navbar-toggler i.fa-bars{opacity:0;transform:rotate(180deg) scale(0.5)}#navbar-toggler i.fa-arrow-left,#navbar-toggler i.fa-arrow-up{opacity:1}#navbar-toggler.collapsed i.fa-bars{opacity:1;transform:rotate(0) scale(1)}#navbar-toggler.collapsed i.fa-arrow-left,#navbar-toggler.collapsed i.fa-arrow-up{opacity:0;transform:rotate(-180deg) scale(0.5)}@media (max-width: 768px){#navbar-toggler i.fa-arrow-up{display:none}}@media (min-width: 768px){#navbar-toggler i.fa-arrow-up{display:none}#navbar-toggler i.fa-arrow-left{display:inherit}}@media (min-width: 768px){.bd-topbar-whitespace{max-width:275px}}.bd-toc{padding:0px !important;right:-1em;z-index:999;height:auto}.bd-toc div.onthispage,.bd-toc .toc-entry a{color:#5a5a5a}.bd-toc nav{opacity:0;max-height:0;transition:opacity 0.2s ease, max-height .7s ease;overflow-y:hidden;background:white;scrollbar-width:thin}.bd-toc nav::-webkit-scrollbar{width:5px}.bd-toc nav::-webkit-scrollbar{background:#f1f1f1}.bd-toc nav::-webkit-scrollbar-thumb{background:#c1c1c1}.bd-toc nav::-webkit-scrollbar-thumb:hover{background:#a0a0a0}@media (min-width: 992px){.bd-toc nav:not(:hover){-ms-overflow-style:none}.bd-toc nav:not(:hover)::-webkit-scrollbar{background:#FFFFFF}.bd-toc nav:not(:hover)::-webkit-scrollbar-thumb{background:#FFFFFF}}.bd-toc nav a:hover,.bd-toc nav li.active>a.active{color:#0071bc}.bd-toc nav li.active>a.active{border-left:2px solid #0071bc}.bd-toc nav>.nav{border-left:1px solid #eee}.bd-toc nav>.nav .nav{border-left:none}.bd-toc:hover nav,.bd-toc.show nav{max-height:100vh;opacity:1;overflow-y:auto}.bd-toc:hover .tocsection:after,.bd-toc.show .tocsection:after{opacity:0}.bd-toc .tocsection{padding:.5rem 0 .5rem 1rem !important}.bd-toc .tocsection:after{content:"\f107";font-family:"Font Awesome 5 Free";font-weight:900;padding-left:.5em;transition:opacity .3s ease}.bd-toc .toc-entry a{padding:.125rem 1rem !important}.bd-toc div.editthispage{display:none}#site-navigation{height:100vh !important;width:275px;flex:auto;top:0px !important;margin-left:0;overflow-y:auto;background:white;transition:margin-left .2s ease 0s, opacity .2s ease 0s, visibility .2s ease 0s;z-index:2000 !important;scrollbar-width:thin}#site-navigation::-webkit-scrollbar{width:5px}#site-navigation::-webkit-scrollbar{background:#f1f1f1}#site-navigation::-webkit-scrollbar-thumb{background:#c1c1c1}#site-navigation::-webkit-scrollbar-thumb:hover{background:#a0a0a0}@media (min-width: 992px){#site-navigation:not(:hover){-ms-overflow-style:none}#site-navigation:not(:hover)::-webkit-scrollbar{background:#FFFFFF}#site-navigation:not(:hover)::-webkit-scrollbar-thumb{background:#FFFFFF}}@media (max-width: 768px){#site-navigation{position:fixed;margin-top:3em;border-right:1px solid rgba(0,0,0,0.1)}#site-navigation.single-page{display:none}}#site-navigation nav ul.nav li a,#site-navigation nav ul.nav ul li a{color:#5a5a5a}#site-navigation nav ul.nav a:hover,#site-navigation nav ul.nav li.active>a,#site-navigation nav ul.nav li.active>a:hover{color:#0071bc}#site-navigation h1.site-logo{margin:.5em 0 0 0;font-size:1.1em;color:black;text-align:center}#site-navigation div.navbar_extra_footer{text-align:center;font-size:.9em;color:#5a5a5a;margin-bottom:3em}#site-navigation.single-page{border-right:0}@media (min-width: 768px){div.navbar-brand-box{padding-top:2em}}div.navbar-brand-box a.navbar-brand{width:100%;height:auto}div.navbar-brand-box a.navbar-brand img{display:block;height:auto;width:auto;max-height:10vh;max-width:100%;margin:0 auto}@media (min-width: 768px){div.navbar-brand-box a.navbar-brand img{max-height:15vh !important}}nav.bd-links{margin-left:0px;overflow-y:visible;max-height:none}nav.bd-links p.caption,nav.bd-links .toctree-l1 a{padding-left:0em}@media (min-width: 768px){.bd-sidebar{max-width:275px}}.prev-next-bottom{height:3em}ul.ablog-archive{padding-left:0px}ul.postlist{padding-left:0}ul.postlist>li>p:first-child{font-size:1.5em}ul.postlist li+li{margin-top:2em}ul.postlist li>p>a{font-style:normal;font-size:1.3em}div.bd-sidebar h2{font-size:1.5em}div.bd-sidebar h3{font-size:1.4em}div.bd-sidebar>ul{list-style:none;padding-left:0}@media print{.tag_popout,div.margin{float:right;clear:right;width:50%;margin-right:-56%;margin-top:0;margin-bottom:0;padding-right:1em;font-size:0.9rem;line-height:1.3;vertical-align:baseline;position:relative;border-left:none;padding-left:0}.bd-content div#main-content>div{flex:0 0 75%;max-width:75%}h1,h2,h3,h4{break-after:avoid}table{break-inside:avoid}pre{word-wrap:break-word}a.copybtn,a.headerlink{display:none}.tag-fullwidth{width:145%;clear:both}div.toggle-hidden{visibility:inherit;opacity:1;height:auto}button.toggle-button{display:none}blockquote.epigraph{border:none}div.container{min-width:50% !important}div.bd-sidebar,div.prev-next-bottom{display:none}div.topbar{height:0;padding:0;position:inherit}div.topbar div.topbar-main{opacity:0}div.topbar div.bd-toc{flex:0 0 25%;max-width:25%;height:auto !important}div.topbar div.bd-toc nav,div.topbar div.bd-toc nav>ul.nav,div.topbar div.bd-toc nav>ul.nav>li>ul.nav{opacity:1;display:block}div.topbar div.bd-toc .nav-link.active{font-weight:inherit;color:inherit;background-color:inherit;border-left:inherit}} diff --git a/0.4/_static/sphinx-book-theme.css b/0.4/_static/sphinx-book-theme.css new file mode 100644 index 0000000..6db3a76 --- /dev/null +++ b/0.4/_static/sphinx-book-theme.css @@ -0,0 +1 @@ +body{padding-top:0px !important}body img{max-width:100%}code{font-size:87.5% !important}main.bd-content{padding-top:3em !important;padding-bottom:0px !important}main.bd-content #main-content a.headerlink{opacity:0;margin-left:.2em}main.bd-content #main-content a.headerlink:hover{background-color:transparent;color:#0071bc;opacity:1 !important}main.bd-content #main-content a,main.bd-content #main-content a:visited{color:#0071bc}main.bd-content #main-content h1,main.bd-content #main-content h2,main.bd-content #main-content h3,main.bd-content #main-content h4,main.bd-content #main-content h5{color:black}main.bd-content #main-content h1:hover a.headerlink,main.bd-content #main-content h2:hover a.headerlink,main.bd-content #main-content h3:hover a.headerlink,main.bd-content #main-content h4:hover a.headerlink,main.bd-content #main-content h5:hover a.headerlink{opacity:.5}main.bd-content #main-content h1 a.toc-backref,main.bd-content #main-content h2 a.toc-backref,main.bd-content #main-content h3 a.toc-backref,main.bd-content #main-content h4 a.toc-backref,main.bd-content #main-content h5 a.toc-backref{color:inherit}main.bd-content #main-content div.section{padding-right:1em;overflow:visible !important}main.bd-content #main-content div.section ul p,main.bd-content #main-content div.section ol p{margin-bottom:0}main.bd-content #main-content span.eqno{float:right;font-size:1.2em}main.bd-content #main-content div.figure{width:100%;margin-bottom:1em;text-align:center}main.bd-content #main-content div.figure.align-left{text-align:left}main.bd-content #main-content div.figure.align-left p.caption{margin-left:0}main.bd-content #main-content div.figure.align-right{text-align:right}main.bd-content #main-content div.figure.align-right p.caption{margin-right:0}main.bd-content #main-content div.figure p.caption{margin:.5em 10%}main.bd-content #main-content div.figure.margin p.caption,main.bd-content #main-content div.figure.margin-caption p.caption{margin:.5em 0}main.bd-content #main-content div.figure.margin-caption p.caption{text-align:left}main.bd-content #main-content div.figure span.caption-number{font-weight:bold}main.bd-content #main-content div.figure span{font-size:.9rem}main.bd-content #main-content dl.glossary dd{margin-left:1.5em}main.bd-content #main-content div.contents{padding:1em}main.bd-content #main-content div.contents p.topic-title{font-size:1.5em;padding:.5em 0 0 1em}main.bd-content #main-content p.centered{text-align:center}main.bd-content #main-content div.sphinx-tabs>div.sphinx-menu{padding:0}main.bd-content #main-content div.sphinx-tabs>div.sphinx-menu>a.item{width:auto;margin:0px 0px -1px 0px}main.bd-content #main-content span.brackets:before,main.bd-content #main-content a.brackets:before{content:"["}main.bd-content #main-content span.brackets:after,main.bd-content #main-content a.brackets:after{content:"]"}main.bd-content #main-content .footnote-reference,main.bd-content #main-content a.bibtex.internal{font-size:1em}main.bd-content #main-content dl.footnote span.fn-backref{font-size:1em;padding-left:.1em}main.bd-content #main-content dl.footnote dd{font-size:.9em;margin-left:3em}main.bd-content #main-content dl.citation{margin-left:3em}main.bd-content #main-content dl.footnote dt.label{float:left}main.bd-content #main-content dl.footnote dd p{padding-left:1.5em}div.cell div.cell_output{padding-right:0}div.cell.tag_output_scroll div.cell_output{max-height:24em;overflow-y:auto}.toggle.admonition button.toggle-button{top:0.5em !important}button.toggle-button-hidden:before{bottom:0.2em !important}div.sidebar,div.margin,div.margin-caption p.caption,.cell.tag_popout,.cell.tag_margin{width:40%;float:right;border-left:1px #a4a6a7 solid;margin-left:0.5em;padding:.2em 0 .2em 1em}div.sidebar p,div.margin p,div.margin-caption p.caption p,.cell.tag_popout p,.cell.tag_margin p{margin-bottom:0}div.sidebar p.sidebar-title,div.margin p.sidebar-title,div.margin-caption p.caption p.sidebar-title,.cell.tag_popout p.sidebar-title,.cell.tag_margin p.sidebar-title{font-weight:bold;font-size:1.2em}@media (min-width: 768px){div.cell.tag_popout,div.cell.tag_margin,div.margin,div.margin-caption p.caption{border:none;clear:right;width:31% !important;margin:0 -35% 0 0 !important;padding:0 !important;font-size:0.9rem;line-height:1.3;vertical-align:baseline;position:relative}div.cell.tag_popout p,div.cell.tag_margin p,div.margin p,div.margin-caption p.caption p{margin-bottom:.5em}div.cell.tag_popout p.sidebar-title,div.cell.tag_margin p.sidebar-title,div.margin p.sidebar-title,div.margin-caption p.caption p.sidebar-title{font-size:1em}div.cell.tag_margin .cell_output{padding-left:0}div.sidebar:not(.margin){width:60%;margin-left:1.5em;margin-right:-28%}}@media (min-width: 768px){div.cell.tag_full-width,div.cell.tag_full_width,div.full_width,div.full-width{width:136% !important}}blockquote{margin:1em;padding:.2em 1.5em;border-left:4px solid #ccc}blockquote.pull-quote,blockquote.epigraph,blockquote.highlights{font-size:1.25em;border-left:none}blockquote div>p{margin-bottom:.5em}blockquote div>p+p.attribution{font-style:normal;font-size:.9em;text-align:right;color:#6c757d;padding-right:2em}div.highlight{background:none}.thebelab-cell{border:none !important}button.thebe-launch-button{height:2.5em;font-size:1em}div.tableofcontents-wrapper p.caption{font-weight:600 !important;margin-bottom:0em !important}.topbar{margin:0em auto 1em auto !important;padding-top:.25em;padding-bottom:.25em;background-color:white;height:3em;transition:left .2s}.topbar>div{height:2.5em;top:0px}.topbar .topbar-main>button,.topbar .topbar-main>div,.topbar .topbar-main>a{float:left;height:100%}.topbar .topbar-main button.topbarbtn{margin:0 .1em;background-color:white;color:#5a5a5a;border:none;padding-top:.1rem;padding-bottom:.1rem;font-size:1.4em}.topbar .topbar-main button.topbarbtn i.fab{vertical-align:baseline;line-height:1}.topbar .topbar-main div.dropdown-buttons-trigger,.topbar .topbar-main a.edit-button,.topbar .topbar-main a.full-screen-button{float:right}.bd-topbar-whitespace{padding-right:none}@media (max-width: 768px){.bd-topbar-whitespace{display:none}}span.topbar-button-text{margin-left:0.4em}@media (max-width: 768px){span.topbar-button-text{display:none}}div.dropdown-buttons-trigger div.dropdown-buttons{display:none;position:absolute;max-width:130px;margin-top:.2em;z-index:1000}div.dropdown-buttons-trigger div.dropdown-buttons.sourcebuttons .topbarbtn i{padding-right:6px;margin-left:-5px;font-size:.9em !important}div.dropdown-buttons-trigger div.dropdown-buttons button.topbarbtn{padding-top:.35rem;padding-bottom:.35rem;min-width:120px !important;border:1px white solid !important;background-color:#5a5a5a;color:white;font-size:1em}div.dropdown-buttons-trigger:hover div.dropdown-buttons{display:block}a.dropdown-buttons i{margin-right:.5em}button.topbarbtn img{height:1.15em;padding-right:6px;margin-left:-5px}#navbar-toggler{position:relative;margin-right:1em;margin-left:.5em;color:#5a5a5a}#navbar-toggler i{transition:opacity .3s, transform .3s;position:absolute;top:16%;left:0;display:block;font-size:1.2em}#navbar-toggler i.fa-bars{opacity:0;transform:rotate(180deg) scale(0.5)}#navbar-toggler i.fa-arrow-left,#navbar-toggler i.fa-arrow-up{opacity:1}#navbar-toggler.collapsed i.fa-bars{opacity:1;transform:rotate(0) scale(1)}#navbar-toggler.collapsed i.fa-arrow-left,#navbar-toggler.collapsed i.fa-arrow-up{opacity:0;transform:rotate(-180deg) scale(0.5)}@media (max-width: 768px){#navbar-toggler i.fa-arrow-up{display:inherit}#navbar-toggler i.fa-arrow-left{display:none}}@media (min-width: 768px){#navbar-toggler i.fa-arrow-up{display:none}#navbar-toggler i.fa-arrow-left{display:inherit}}.bd-toc{padding:0px !important;overflow-y:visible;background:white;right:0;z-index:999;transition:height .35s ease}.bd-toc div.onthispage,.bd-toc .toc-entry a{color:#5a5a5a}.bd-toc nav{opacity:0;max-height:0;transition:opacity 0.2s ease, max-height .7s ease;overflow-y:hidden;background:white}.bd-toc nav a:hover,.bd-toc nav li.active>a.active{color:#0071bc}.bd-toc nav li.active>a.active{border-left:2px solid #0071bc}.bd-toc:hover nav,.bd-toc.show nav{max-height:100vh;opacity:1}.bd-toc:hover .tocsection:after,.bd-toc.show .tocsection:after{opacity:0}.bd-toc .tocsection{padding:.5rem 0 .5rem 1rem !important}.bd-toc .tocsection:after{content:"\f107";font-family:"Font Awesome 5 Free";font-weight:900;padding-left:.5em;transition:opacity .3s ease}.bd-toc .toc-entry a{padding:.125rem 1rem !important}.bd-toc div.editthispage{display:none}.bd-sidebar{top:0px !important;overflow-y:auto;height:100vh !important;scrollbar-width:thin}.bd-sidebar nav ul.nav li a,.bd-sidebar nav ul.nav ul li a{color:#5a5a5a}.bd-sidebar nav ul.nav a:hover,.bd-sidebar nav ul.nav li.active>a,.bd-sidebar nav ul.nav li.active>a:hover{color:#0071bc}.bd-sidebar::-webkit-scrollbar{width:5px}.bd-sidebar::-webkit-scrollbar{background:#f1f1f1}.bd-sidebar::-webkit-scrollbar-thumb{background:#c1c1c1}@media (min-width: 992px){.bd-sidebar:not(:hover){-ms-overflow-style:none}.bd-sidebar:not(:hover)::-webkit-scrollbar{background:#FFFFFF}.bd-sidebar:not(:hover)::-webkit-scrollbar-thumb{background:#FFFFFF}}.bd-sidebar h1.site-logo{margin:.5em 0 0 0;font-size:1.1em;color:black;text-align:center}.bd-sidebar div.navbar_extra_footer{text-align:center;font-size:.9em;color:#5a5a5a;margin-bottom:3em}.bd-sidebar.collapsing{border:none;overflow:hidden;position:relative;padding-top:0}.bd-sidebar p.caption{margin-top:1.25em;margin-bottom:0;font-size:1.2em}.bd-sidebar li>a>i{font-size:.8em;margin-left:0.3em}.toc-h1,.toc-h2,.toc-h3,.toc-h4{font-size:1em}.toc-h1>a,.toc-h2>a,.toc-h3>a,.toc-h4>a{font-size:0.9em}.site-navigation,.site-navigation.collapsing{transition:flex .2s ease 0s, height .35s ease, opacity 0.2s ease}@media (max-width: 768px){#site-navigation{position:fixed;margin-top:3em;z-index:2000;background:white}}@media (max-width: 768px){.bd-sidebar{height:60vh !important;border-bottom:3px solid #c3c3c3}.bd-sidebar.collapsing{height:0px !important}.bd-sidebar.single-page{display:none}}@media (min-width: 768px){.bd-sidebar{z-index:2000 !important}.site-navigation.collapsing{flex:0 0 0px;padding:0}}@media (min-width: 768px){div.navbar-brand-box{padding-top:2em}}div.navbar-brand-box a.navbar-brand{width:100%;height:auto}div.navbar-brand-box a.navbar-brand img{display:block;height:auto;width:auto;max-height:10vh;max-width:100%;margin:0 auto}@media (min-width: 768px){div.navbar-brand-box a.navbar-brand img{max-height:15vh !important}}nav.bd-links{margin-left:0px;max-height:none !important}nav.bd-links p.caption{font-size:0.9em;text-transform:uppercase;font-weight:bold}nav.bd-links p.caption:first-child{margin-top:0}nav.bd-links ul{list-style:none}nav.bd-links li{width:100%}nav.bd-links li.toctree-l1,nav.bd-links li.toctree-l2,nav.bd-links li.toctree-l3,nav.bd-links li.toctree-l4,nav.bd-links li.toctree-l5{font-size:1em}nav.bd-links li.toctree-l1>a,nav.bd-links li.toctree-l2>a,nav.bd-links li.toctree-l3>a,nav.bd-links li.toctree-l4>a,nav.bd-links li.toctree-l5>a{font-size:0.9em}nav.bd-links>ul.nav{padding-left:0}nav.bd-links>ul.nav ul{padding:0 0 0 1rem}nav.bd-links>ul.nav a{padding:.25rem 0 !important}@media (min-width: 768px){.bd-sidebar,.bd-topbar-whitespace{max-width:275px}}.prev-next-bottom{height:3em}@media print{.tag_popout,div.margin{float:right;clear:right;width:50%;margin-right:-56%;margin-top:0;margin-bottom:0;padding-right:1em;font-size:0.9rem;line-height:1.3;vertical-align:baseline;position:relative;border-left:none;padding-left:0}.bd-content div#main-content>div{flex:0 0 75%;max-width:75%}h1,h2,h3,h4{break-after:avoid}table{break-inside:avoid}pre{word-wrap:break-word}a.copybtn,a.headerlink{display:none}.tag-fullwidth{width:145%;clear:both}div.toggle-hidden{visibility:inherit;opacity:1;height:auto}button.toggle-button{display:none}blockquote.epigraph{border:none}div.container{min-width:50% !important}div.bd-sidebar,div.prev-next-bottom{display:none}div.topbar{height:0;padding:0;position:inherit}div.topbar div.topbar-main{opacity:0}div.topbar div.bd-toc{flex:0 0 25%;max-width:25%;height:auto !important}div.topbar div.bd-toc nav,div.topbar div.bd-toc nav>ul.nav,div.topbar div.bd-toc nav>ul.nav>li>ul.nav{opacity:1;display:block}div.topbar div.bd-toc .nav-link.active{font-weight:inherit;color:inherit;background-color:inherit;border-left:inherit}} diff --git a/0.4/_static/sphinx_highlight.js b/0.4/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/0.4/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/0.4/_static/vendor/fontawesome/5.13.0/LICENSE.txt b/0.4/_static/vendor/fontawesome/5.13.0/LICENSE.txt new file mode 100644 index 0000000..f31bef9 --- /dev/null +++ b/0.4/_static/vendor/fontawesome/5.13.0/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/0.4/_static/vendor/fontawesome/5.13.0/css/all.min.css b/0.4/_static/vendor/fontawesome/5.13.0/css/all.min.css new file mode 100644 index 0000000..3d28ab2 --- /dev/null +++ b/0.4/_static/vendor/fontawesome/5.13.0/css/all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.fab,.far{font-weight:400}@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:"Font Awesome 5 Free"}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.eot b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..a1bc094ab14d8c7d84d8c59aa511de9e69440d0a GIT binary patch literal 133034 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℘oIO z&0TkO*4ZsXTrwU?hKlj1ok&*NXU!r)ihe%88Nbu$lI6UW45dQxk`<57S~Y7`P%Z^M zKnG&83%E{#sep6P^@o9^GR&vK-^6MLJ8MA33XrQ2G^Spw*T|LJ$*WeJbIyum>I3PX zY%{x{zuFMaBxel{4mO*EuY|1Cr=GLtoN6_d9n4i1Hj)z(t_%(a=nezD1K1KYk2Z#> z6+*R2>w#$kY9$cnfi#6A524eeAn!CA*|B+ZkC)AQX3Vl;EY?ZY0{(W`E(5Mcnpc;NmPzXV!nc(fPc**)ls%w^;{8+f5HjtA;~F_p|Fx@&o-mU zq}7OaN0YMKA)AH%SVwP1Io6-I?ZPwppu3wvBX?U8BX4Asn!ssn>}| zkFDL^v#0gL-OvwtCc-eG94A#sv*6b(FziBx)aH;vh3qbT{_`Xx?AIR<>kYnFJdtcO zAK}TKsa*pDU~QimHO=zUMB-EZ(`$Vn>a);DRMxCX{v48P(m%3q?A52;Rr0QDoT+cx zWSKVgjSwoiWyOjU+Pk{8t*WmAwQz^`lB+;BJ#f-u_DUY)hXi`eQ=-fln$?;I&R6dt zQod04n)O!U+`?IbG?q!?!twr?(XVV!IVoi5g!5AZ zx5l7(PB!HlM0}n&YQ%JOO;p06gasz(>oMCi4M~eQIXg>!5?)pB;^$3F%o`>|mjB(B z$N(Y7E+@;5WhFhFu!q-drSe2pO$GcPl7sw>@)}+N>|!_?Bnq82j5++QY!x(Gq-&9+ zxfB6m68aBbPlaU#l!;#AAGDb!LbC#IDKsQah=-liy1KsHQBffZ-%Ez$Q$PCn$H_ds zSV*hN#?DY2%$sOh$(it@$90BVNg@8)D)@vCPaS@hT!}fdnF8c{p9GGaFVBI(*Z{fv zCCT?_PA?f{n>5F}cY& zj{3NuMhkT&K*dsJ4ne)Bdk(>u16uY$wK*K5pnP9G6u%>OiTzmU>Wi`$4LtK^+teLx z*HzjpJ((-g4;I&yKDj5lXX=HvwiOre*#n0-*8btcc+S)0Zal?eeDJ!{874fHbIh2!%Fbe>@cYf zvsBx7Wb4Gl)`>NvU0tJHzqCBt_BMgMCuS$EOw7(1wghb^mkUx8-B9XTp3^gOtPp8u z**32blQTMU3Cpuax|PjUf^1SlwG&%EjB2|^@1gZt4O-^}YymE(XVY%t%H&8cPqnp) zso&sUuT5y(N^3moIef}VM?5Kb5VDQ;(Vov_{+{;2(_=L=W=B)nf_kY`Fa03+{s2k+ z8W(2%XXvPb2^9%arRc@ee}2z?$=#HlAFgUnM% zDfam!z%`DC^2$z~Ajnvzcd|rMre6BwC&8B`@YP}P)pih1pnt${DdcC9fYH!Zo`M7h zBkH7@Kod%MhIpi**K-}CX`@%wI@&m5El}5}>Y74M;RV*?vxbxG>GE$;=jpsTUb5Bm z)m7@%iHMF_mc|h55jcsCto?tW5fQBIGS%Cxw_S zh!lEy#2XOB0kKOVe7_{7Wv86z_`odP#!78aMOoPhtwzyBqBV58}^L!Cw&RlE7#~JplMYmw{M;QA`=KN9N4dhqGo+DAcQY{VCa;-(wAm ze8fxXL&@4;w#29Oa+v$(oK$%Q35UVIypGD81z_=t2pcASR+($dnl8a31AS$Bd}jCs z6KX>L0$-r~fy4RZk3a4n#D{E;f8#5keuVwT&&gf+mvAq57c<6=k!u1T(2cO0V6}TL zKm9w0K;JCnEz*K5s7@1cp+yrZ*F(tH9Mg2Hsn;CKa?HQ>v8u@R+g;t^#G+_FC#r1U zoaG`rCNBSk>6$bDA6?CIs<2kpC0ko7s2saGfAQkQ;7D*N9Db5~i#!c38Uru7oVkU0 zkof^oNjsSUEf*?aI^aXlIi);=Tf?u4Qog@Wbi4@VH*~_mokUzU*#b@Uut)>22sAay zuQ2vV;tv+iD2O7>!o-Bp?LphYLH{^F6Mnf-OJg39hkNr9aIHokBwLeOthgP-hC-k! zkJioItzjSLw}E6ZjUXb%P= z)CVMAv56u0A<;`DU}5SdNwQR(mz-EejzmRE5F{1~rC^vm(Rg09I$huzvXzVZSp!%J z(N#TXgba$az)pKwU}gM^qKKOm&Qj%6pDF4fR3bv*;6L`H9+Vc*i5VKl8!8b^Uf_Aj27DA^i3&!qih|}xT_qbi%+pQ+8_~n8 zjt=vZB)b~GCo-SfcPwBdYQo|R%$j0)%?WABDnMvDhD|D1-y+X()Lz)J_$|7L}eGd7y(5?ZP@QA43e$yEd zWLaW0o>)XMbdd$@WKKP()%hYgm% zg=RF%l8f^uXujfN0pP?WegoOw9f2q#cZ$w%K~WYTLl#zc)H;j#X6V=HzMQz_q~d+e zP-9U^QqMSRRnM-Pk)3_q@0Xc#=BLQxfmF=Eh%#MIwU?|a#Pp#Fz3jA^T|+sNn0w8t zk@R4X8V+Y!?xB+DFI(*%(=)Kq)mDH7f{#=E@@eq#1hbl241sN1!3v@V9%bWDqH$0I zFR;MTC!nF=Adoo}F#)&>IU`}Q2X_YC5<-xC$sg}2158|dF@NOjN8J<03tGqw2_0_C z(Ij0G=B=O0DRNkesTm=p8&M~mG#u5m7T%UF_l*1e#cLz}k+bu<+lL}p)Vw51o*fbt zO^RmP+O@E$s-gtoLEtn+x8O()8HsYxx5vmXL@9dAoIrcAh{;V$NU+8hh##oV0QTpQ zQ36R$b+y1E%tNdWjIuO~t&@qAEA_Fw5;9hUaIY}ja4ZIzmZkrY6!tht(H095kCnu# z(ndw&1ergL6QNB!nav$gWLx66Pg%0$lW&AlOC(Jbr@qRvXUJHwtXGmWT`_nWuqsw1 z=7bCM)4iCJFl>w?$a+hYsSlu8jhcgzr#e3beVR7A(QIWZ2EIe+RAl*{>FzO2p9O?`Drvu$mPim4CZ9NcmEIUB9GRu&lvd#7TsC|mg_Hb0%h>>ahg{tH5X#j)>;Jw*|5@s-m z7=kKf0Yo@1vpKFkPejoOdyd6PiCGf(uB)~=I#29G@77|z+jM7X8XuW$1Jwrq;U#{v z?S!48qDbLcQL@CER|EQ~n&GG)m1DfBdurLt*{ZG$4)h$~r-oEx_EyDMZ@P-g^CIu< z3WXyht^-w8qJ3f&@*tv%%fP`$nI*8yHzAV>Q%KeZwb9F5u-uTMf|KTn5NTABHG&}p zvuHRC$V?H^F}M$UEk>IPyfnlj3Z_B6W$=Ea-k ztdYeIB-1$0jm;ZSP8b_oE#qr`b#2)eOJZnAQ#Zv4eQq}G8^%aFn>bPRL{V^k9fCv@ z6lyq|;Uo`M&B{YXl{2RPd+F?nrHgp3V!E1;NcBUEx%GNy |xk~oikk&D!xjeU;p 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 literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.svg b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.svg new file mode 100644 index 0000000..46ad237 --- /dev/null +++ b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.ttf b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..948a2a6cc76a4d7cb692d0d08e352f126ec48a9d GIT binary patch literal 132728 zcmeFad7K?pnLk{o_Py%fs#|sUed&F1OhtKdB3M_cL)tx2 zPMtc-bDrh--^9zi;N{OaE7 zxV7#9h9Mh}-g@%c+s^%uAO6{782M`qBfP$I+Xd$yIf9y4aRArloo9XI)HB!rbc|ub zE<^tH<8*O3^{Ny z!?@pL&^K}%tN-`@@tr4GBX2Ppi`y9Hr8}={oBr)R@|PozvR9G)NXty?GH$}(z9ah> zhTV2Wkn*jRntDf@k4A za&C|go{9RxN87PG$PdlbceMOICPH2d>Xw6RZ!$8yC#ZYoxAn~IG0%IiPqcqyxS!UK zBX}N7GmJXZ@0t2=J%TzUc8oEQN5?OUJVNh7zKDG0?;cEWz4eTJOwV*X4E#FN=OUgj z2ImaJ&$NZ6r|WL@XY9z4;2IsvvDQJ`NZW@)VaJZVi@uQHzJ18s9pnddi0kYzWodmG z9Q2t-$CTcOawZOr{3>WO9n0`cJ@}pJYZlLmvCNVG#&wt8cg&Fm%>^_t^St2Neg^pr zPtS4Bd*iea{H&p z0BN%S2%d%C);+DhAl|)==$|S~})>Yt8M(nqpdY;pFoeGyD?nf4H`r_TnpM9oYVC5JmWc_2bvD*;!r+F<5vpub~75v z&~uX=LmBj)Jo(;pgLY5fPumrg*?r_-aPQ0*{=fL0Zik6_O?H0kIjFZaeppYzc+>R0 z;7GhLP3H#k6bGeIjb1}PXk8BSX67uQhne;v-$$QJnhx&8FRdHvWeWF(gY%<#t#Jy< zjxjzv#;~{tYn=WDYs#O#_ixkn1?!LIp|1CM0Bt87|IY>Mgg#e6nMjb=YBP_zgT5i3 z!}Xc|>Nt;}E_x5@l=Qm$H;2dZi7&)-SS|rbaLtBmdP_G&zU@T^76?&lh;k&I(hr#eUsmqd}Q*m$sbR?H2Is!{RcN4yzt;< z2X`O5>fkj8KY8$T2S0!CO9$^g_^pFa9Nc&CM+cui`0T;w559cxm4m-K_{PEiJb3Wn zy9cKZ35SeB{-NBV!lC{{%MPtNwEoZuhyMA{?+?BEmiyMH-n#Rxd)|8Bt#7^cowuHR z>&I_B`_|9jI`G!vxBmLJ^>+B}Id6}@{l&K*di$BTpMCq~!~Eg?!uR46~ z;TsR%a`^Vc-#q;7!{0mneT^;&)fQyB-=F^X3)7c-{WyFQ&(97RGDmfin-Bd*FftA33la<8^H?UZ2N!?LF|w zf$tr7=D_m@{&?U&4!k+ZPRf(Uq&?Y>@fw~y4&$|T@@$OPrIWiSublk!!DY{}0FOpQgvFFBq>&rpN2%!=F3+5XS2t zr^o9RjMuB4s(OK#$0KxFdJso>@$m|V}{L;X`6z{o1F1C zP4Nw1@epo-KPwF4mPuEY=H)#K+J+D2hJ*|CTdqVq; z_6_YT+Wp$q+7;Tzv@^BSwb|M%^;PwG^&iwn)%(@m>RPo!wN*Fe@NabpVC^VSfT&_=l^g7 zhzLG5T{ckj!5ZK9nYsAB%>U0OHZnIcFO$9GHP&Tc;>Nj0d7dBPFXrD6Y++E?AUq?g z;<$LT_?Y;Hv|D;adQs-(G5IF>dHFBOfTnBbYy0&P{TlsoBX4}qc+)%;>+&^ghqd1} z?GgJl`wyXWLr;d;@QUzF;lH?|dvzooIXCi4Z`gal*ZrPoNA#@dW3h|lO#H>f?qo-D zMe?@fA5vRVze&sKYtnCK8kt9O3-V%7El!pmEGNr*D}3e2${X!=`)K=>?Z4^R*m-g1 zV_iL6S9cxiPIqtVzPvBmx4mzF|5^P{R@2ob)jhT0+MNUPz>c4EH8-Hy!nxAd{ zVelJ6qeH8PZXSAYI68dmtn{qyv#uPGM=l?^Z+3C^<+Gohy?-=3x^eW;IqaO9$Lz67 z$KIKH-FSNZu?c=+@5JQ%`24N&A6;e8$3yXxPsX0Lwm6HBl8_iI;Nx8S=QB5a=E2WA_t|G}+i}~yxBcMu2X6nt=TH6oZJ&Scj&tr*@66u0;?6yH zK7Z%yU;6CcReNu`%f4&!?sa$HcK2WJIrW~$?|I|iv3qa5_b*?b_vQWfU4Gw-_jld@ z_yae6CH$339~2+F`>P{gJ?(4u*Y&SI{P6fA_kL%^clJJd);~P(*sjN;k8gPVj>lj6 z$DV(@_j~U5u6d&Gi5*Wo`@}m>_B^@k`{UpL{11D6`1((-dxm*t!!s}a^qil&KX3d( z|3%v`mj7bUFJ61$@)utJ<&Iz8{m;c0$6vho#r^-X=3g%Tmv>%@zO>_|yI=anzkUAY z@XHVU+WYn7Z??a3=_|k5Kd}G&SL3h#^0#;YcJg-*{C?#3kNtk?4_p7J|M8MPzVTYe zYrp){($~e;FMs{TKihvE{qycO%r}<4@fWJ39obLzk-dz?%mO#6dXbPT7IOJ=Jv7j$ z35}*G(W6|*i+;HxM%*;PZ^ZR{v63b=v5>ELwdyFT50IPPWYV45OTS64V>(Vmc0DJV zX_Hph71@++GwqwUn2!{@6{)8=w4t5sJ$l1dH~A&gm082r`AA<*7_-7^BrFn{M2iB) zy0Iil53lK_njyI2beoGuTii+NwPv9{FpJdDRFkAgAySJJrn}K<+O@;l{I*Ook!p|5 znR9K>%<1Rfwy3u}TkyKl%|*AN@n{QWvi#BKSd0t3vwrkxN5{vh)eDhEwJ}i6i!R2X zhs8Ka1!UIh7`|$nO%bx!a2#W5H~rp8CPG=?&w6A`v~y8ECy}uU+t+P@C3|NI5fq); z{o;!U{fzHtrry3Vnon7-nR@YzSXy;09X2nc9MiUS%!$m&%<0Uz%!irFn5+IyE9vym z8Kl$Z;k0g&d=IIVD}9xL8nVEMdbs*Pv)pWW8Pcfwqpa^qUc@z7d?!i7EsNz^LmDXL zBW|r)7Axh3Uul#j(f2Bi3VyvZenjL|@-*{UTKIi+_O|)+H;-0z!ipi&RD55HkAaTS_S(d~KX4>;YZj4m~d{{|kqmgjLOPd0# zM9g-gjuC+f9LI_R!5K?L`Y??khX_jWWC^(>kl2(Kfl$CM@58tUBi33G&3u_IjAjoz zn|OX1gG(1qKotJ;q9Q*fZHsp;*sxeL7Y`Tes;Zb~wCs7YUaNB67(2VOCsWMm`E=SN zZM;%HeSM>sbhl>+C!{-LtUR~}8l$mh8 zD4C>O=^?!&@25$Iq}l2m;$kF4WTKs1t6JO~RA2VrmoDEYh=Q2o1Ugr3m^XneoVROY z^Kr5g@}fn@)bUTv8+vALS_qdEOZUxiJ{B=JHIh|D2bAkdCdcz`)RQEl$V<1}*u`S1 zaVR)IJba$zL4iE4aP%Y8cu)u7WtJdY5?QdQ-DBs@TmA7ED~n`~n?-e|*OqjmR;I)m zvZ5%wW^tuVESGHaKxMke^Lj`#2(kFEBsX^=UUu-9P<&6%-H3V{>>m zha(Ndi_XdnM#SUwBa z-|`%(Ja>!M7MrEpC(NC_YQt!1NaaM<=XlP~cg`K^$_=CvVYQf0VLRmGydp_Fdc4z( z^&~D|zVnu2`1vZVA&8$>oo|@vY`SjArKD`1ms*Q(TYBL*S4Tz_R966 z$w7^i2Y5pBP zO((Dd*+C~On3r1BZ&JqXdZjv*DpaV|>c?`|U7dAy%Mh20hmxUUJZdMBmG)V)h>)V6 z4{*lsHM(RuZzV&iP`qTt~9CZC*AgK)VsqifY6Oj`*XlL0hdXuEigV9haa?^M-IHx*7xY&f!rA1U!NI|1bMWPmwfgjP_nup= zrm};%>cU2HLc*27!2sQ1pmzXUg67f2FttLcR%tyjO+c*#!aR_skmMnBdKBcHW+OW` zZ|?E3S_O!JM1n>f(&_Q~5x^SBGsCqsf13Vr>B6$*(qmr1f;3wc; zvq*K6Emu8PMB^WK!g@$@3S=m(B-*phXfkOvqTSJ?>~_dzp+DBq+fk17=PkK1)|S`1 zB7KogBj4sEJIc|%bYE|LN5OFl?ses&8`Yd5Kda~@Y=r|4((IVs)|}fHjrl3tRTbAt z`7yue!-YPPwwVv}Wbf3jfdR0#PmP*pd1)f?nf~duz902j zXe26Y)+B!p$u;RevVZKAr`%QYuB)A?Z`)*(DF_Yf&xsC&(NEz$s?M$$Y~l}OcVRH-QH;YF$H zjY4kWtUwyeByr(*f6VAtHmIBwGIYZEDS=yK&^#xbat$IrM;tX`I=Ut*;ZVW?6ZG|% z?U{z8MVy?SB|i(Vs(118CMM<$6C%t1YD;8*kmHw=WyiCU9!}W9YqnB(BCDnX{twAP z{ziEXuK;#291RkMP8-G?epa>$8ZFYbNYY%2fG`RD1FxsTvI5FPFYyQ3OcSA5fwvSI zk|xB%&KX@@U+t)<5QXn0!||z~eDafIo?a}ZRb^vmC=TXLG_B-J_|X$O!>yzce{B_f z!bhf#yh8S1j%=m?`Q9giBj?L=pfEN-?tV$~J(|-?M%gCK@$Ok5t8+zxo!`ZY;#>`K z^yU*eK^)hlO(e6ZT~fzI;V+l!ig3wNLDAR!_xiNQ_3;5|MlmZKWA%`cfUgKGH)N$zeuNX6f_DU?yPsT+`naG*3w0(y#ZqMsLA|JZ4#AfLTJ}M; zIUJ;*{7^m|zdd$|{b=Z_4`)9-@bsH)Q@6KWTWPQKWUfd*P+U{`^xo*+sps3;R$RPy zFC5}n`$vx8IZu(h@Dz*j!RyXsnD8{4@}>g^spVI(7}QRp|P>B36g9YR)U{qhe>UirP{{BTPG&APOKU2>Kg6(wdL8iw+ZAu zF*~s*F*|426116IE=WyuL#bzZPS41(LZqE#+q^?o6x$I)_BZw_>_~5cvA2nWE=0J zJ)g<^PudGlkJZeW9ZP8o>ZMY>^rPVW10?k-F3kMR&{0cgVO5>ZF=L6H0i7c%-4%a~-2;qgT~B+Bjk@ zP}ivHnnF(D1=i!UhLh~+@^4n>>AX2!vegUJRq9oVh>ls7#t`fgIEjv~{lB9T5v=Vp z)$hu)@pLo>c0LZe^hLB~7TS_#Yt>^Xg_tae6nc8Z8xX|-u}dL*za*w*r<~~cz%1R$ zN^MX@S=k7!M$tv0IvC=lPI04|OolPu9LD=Htkn$FP)e>3;?GmTUl8e%z-U7~0Qf@DcPLgV-1RY#7pTz$=YDH#HaLfnEUpeRCxsnhrz$R zj>?<`VDX9w8zy~DnQP0MF2N%MePw!lX7~jYYC``4U!eSf!}(*6J?0+5his33>uaBV znElo-$({KZaW8lmGscdQs{t=*UESftqG&%Ss%+n!@`` zzQ0d&ya?qtbi%=%L|ir50!{R=NCU74G&RbvF!o5|A1s_v5Jj4Wi3y|IgSLZ%{^I~m z_~k||jd?^K?#)ZUwHkepY)xvh;&u=l3W2ITT33S#Ta1g8=Njb}Ee0GAEuiKo%K2WS zPK(xq;>fHusGVD()+{tr8iSe;MV{^z{aqkJs5~kACP?6CWhdLL@$wmg{hY$$x?M*a$*@d z5)~~$kXR^`f?@JR<9XHUbb)KgRxaje4PYfiSM{6`GAPahJMAffmGLKvB5qPROO;c7 zrl^BZi3o**7m4nPET_ZH71cN!<^_wlcvIIcOl=$Ms=6w(3MU#G&%u_JcrI4dqt(n% z0(S{ES4awiSmFga!Apjth=LG~8Ir}KmC-N(+5%%+lXajS(S*)>P+CMMW@sF5s6;e* zf#)R~@KJ~*Dj2;g3Ys5vm2Bh)Pdf>0L=Uq%I?PLw>}mj?$b4$wv4D-J35zcYq;vuupxiAb;7ycmLwhhA$D!IpVRfJVi!78gzR)RHA38_WB)lY(hvCT+8=&{3lG z3L%>eX&L}4i-Z;QIpo_yy9QvwBcg))O=m=qWr@{zViCd6MHaA=IrV^6=ZoN63Tpt% z0F9Vo(UC=75G}bH5&@+q^@y0Xb#3?hs03nKKp|L8i^bgppUGrv z9w|m0JFJ^wOI37UvH7sgiA2&^J|@^&k}La0gk^m#q>}=VN3yEQ%5+vGUR5ctk&iqb z;7AgrRh-E~-)=C74V=*-N)#Fd7DKgBoQ$#+n$avvF3y{v`HG7LfD@DW4P<+_2cnGJ zAv(haMOl0tSyn!G*p;yy=IdRJ=#e18f#-fs>o^{Tuo?SH~JNtw`EHmfKPm#w0 zshEKgWxAefFIiQH=|dBG*=aMohH@k^_v%$6>A@Z~9L}=bgC)~nw%R?eXJDhNtpE!I zAE)}|Q{dwXW;L}K0^7EN6+{g@%EqBY1V2v#hKTw?k?9U;i1d^QUYJo+VhgcmLWoZ^$Cle`G>SK8& zWUL0^USYW5SPV2ROaCS*>(n;w#0Ftv1G|7 z-wLIcNSY>2eVt{`lCff0uOw-@V(>CxRjf+P2^Z+6`!FYA*ceBV^_C`6A3(DjH3uV4 zb$$r?G;MaH*~*~k{&Wtx#?M)CYwBS?m-BD5wbhyp*@N$=Y;C1v09=z+YS7+u{?x-L zm-D}RS(~4m`udb++u9TrQy;x4xaB5#K20kvJx!uI)(o`^uCVzLJ~G<|stx|bOZ;Zr2|Gnak;1d0WQjGe2J}-k!%;sf$9Pru z)UuhgRb3k#=sB@Z4XMWLt%|YUbQP87Mc&&L3P(m<2db<@`@|~b0Yn#OoM#O z;3a`zB%c#wVNDm*a!uzPTwK%^Ofo8XH?J<7<9(ZP^w} zVrWTIH^m8kZZ_>3#z;DwI9c^XQE+`7fFkN6i+HYL zx|)zk^+Sxg^?GN|>8op!IFEjj534;J`yAb=uhO_sWa`;eQd8=QL|m3@z={>sQDxT+ zak2s&8JaIQ&0imZC&!^!y#ufutO!ff$ye|V#YKcuW;Sr$i2LHI@!`gn;r2O$-E%Z; zPIvQTxkkm?a1rrWU6q}`arWu+)hRcfT-npJGMV0B)#fZ~|Dtu>rj3{atg~mxZt^Gt zY!y5kIt*3r!Q2{66EReH6jT^`p6?MV4GN+*>OEYe3GK-uu<`~*`KoVny$}qeoJV$B z)&6eN%xoLd^}dxUr_|eJWtyc-NP=fYbey>Fhjq3%P>8_J;LVgJIE6A;fWRA8G+uB> zx})7RC4)1{<>n{K6;_p{(M8c&HHlZcD~6~>B6_h4T!)u!H{%*&GGpj@htrD1M%?J= zY|{-A!;?amElVXxMwV?a@w%^JOnD{*8|ER{FglP6myT*P2aerIq&j&0Y&I7u)pL*lFe>hPlcTDXdcg#(cr*@PRyS!9o9{QycKKBB?r3O`^=OTTy;EZ{x zb`>m8C)quB?EUu2XXg-ZxuSS>wj+IN67GhO5f>dxFC=p@v$h5^9QbMW2mV|cBamV` znE_^;IgVLR{WO8KKa2R(`2+reWZh|*0x1GGjE)ptqrf@^T7;%6v{b;2sd~({0EF2D z-mQTE`2lviC0R};o!)4ol1x?-y>0?0y$-T2a*{^GAhlzzI|x#Ge~F}X)%)&9I{yzh zpvtMA9aCimY?|UKGy00a^a5L-%PeMAG6-O#z^>q0q!KWd0{Dnm^J;)tTIqZNy{-EB z0L=wEf=W&`Ygipvk5K9dW-yX);NtbFqo*{+$tkwCG%6D%?+t>o1x?TOE{iIxT<}CW zwWYawHcPBXBz(hMz}Urf`fz->Z>~KONQ|qm|Izi=|A_cgug7}Dju>-`X5stRG)moN*whB3Msf#d?DKqUl2}Nlh>bO3GB>Ytj?(zj$9!qP&)kW)oT4g3g)U zsm$tit*~R2?s%E!Uv`E%n%y;@=ZC&xZ9Tro^H@6p4*WEE4gN&b4L>E;XUo^n1O61a z8t?$P7(DXYRcFEhxpUk6iR=l>nj6Oo%}j{9@eTg*Gp@ma&Yhdla?7f7H~Fw**LjE> zouLC6JYW{6U@Z`SO4XF0XAl=ZFO>ndxdEtyZdJ+__!||xnEvUfQ>P`tf~GPGrWV8& zBox&V>9WBZ`DMg3G0a{ttA?o-tD!+;n3^Tc7FefRR4m&OEp~Y$(O+~47~xu;7lhE# z5cGTAn25j{iNKV)$x>Cza<2;jb>y?Zfjbxq64{|iS9xz zd?-XsAA_6_+^oW*jxiOE;;05}>=Lx9tyJ5cX}$;R5z9Hydcl^wR%cqt=CMH^Ie2iV z2Pn`w0++}C93twg!U&NBY_?TlbV@K6nZkNo(=1EV>=Ro*_QS_I=XaP|rQytivPBgz zTOJo!w^7!ta741%HN&%3v$hmDl^4QcR416G&_7)ZO1I9wI~`yG*Ie>0H9^*zP_cnE zTA0DjY(k6YWLfDL8R<|ZITt@FXQ|)1#05s(s{%^ER03=uFtvHu zJv=w{>4*0z5)RO`j3bNqimjk-1w1YSnU=#$0;P0-e^HdY5Gm0UMB+4Yj(~?CQGuVd zg3}0&Rkf>DWyqqdu3EKfRpw{-$z(o1fDdt({_UZwH*dcBP@n+!>SzU~3Q3CnZCF8-Iu;NHWcvJrt4E+#WDnFVsSs(_)1>7ygkpdcNa<%`Lz5&8 zvA&21(nRU$V^_%3yP%D{8xD2X%&&K*MP4+#^nh+>NG{{^^Z3)f;OFjRmcb8uvtB8p zJg3%#;qe~gm_B<4&M|h9*3*NoI~CeHy!1Ox{qhH9XItfWYtgDwMmrH2Co2a`le5lB6dHt@*6%K()bY8fT z12ig;T2s;*QEss>ht;s`QoorjEiA}}E~&f<42mUs*G^6bo8*Wtg?~Y2sqv6&7P{1M zM9HT?hX7a*JcMWtXqcKbME90zZ<*s`XycY&UF! zb!@>8hr@rWjTS#|jq93+}hkuBngP87r*7_P16XH-ZenW>`}qg|I}$ zfSD$U357%Gm~17Y$uxpcvZA1}X=vG4QKss$Ixi3O05Bke1l`-t1Kp7lie##+AQHnZ zBqpNVH~E^mh46r%=nAFu&NEryBzs|#M}c<_Q|~H#kKjsyjDS3b?FPx8CJxnfX)a=R zV2d>>z<9hSolKMg)5z0QV8>F&It?Cxr8mlA!H_W}T8^E&eEF_;IjOW64wtpGP6Kf* zoKv)@6H?$JE6TFNb1<6~6_BBbF2vUPhGk2-I$Kloosnq4luW1A)}g3pWaH7reE?eq|$Q{vGJYQ5z)sGVJ zU-ZA_hrA2@?}rV03U~}vN+`@FffEB7hk8X#(jJOw!7in`W1uN|Qia9c|f)PVO zm;p_yVf4Z&04>6VX$z1n7)&5yO&?}ImB~#~Kp-IB=;#V1BkGQ>TF!P2*^OVZ&U6|v zF7HPRY0vKNvb?mWZv2gxF2u}Oh>H!mU3Svbc66U8w{7pM=xt5T#+5?cHM_b@Rm-R+ zXkec(A*h)}wAbi_7`3g5epm=}q5oqS8ia z)lgORLM)Qwn_VcZY1#&aiy(R(`)tR^g~0T~&^S_%9+0q2IwGAURI5f@aN#Ok3_iZ# zH!6Wk`>s@I$>Eb zJf7+9NlWq0w$znLZVm~3?pS92@bcxIh4|>k&8f=gigVgj!~j(7JQr5-JH8MbX!_mV zY197vF|7PD=ZbdKi1zJEc4j*}vUbAJdS$gB1+>}1rE-u%!&DNLS}|P}P#oF@QbRif zeL*TqP|Pr@f(QMqmq3N}Jzyyap__L6h|^9@^5*yj6Kk*ZP7=!TR0mnpHrQO&oE3Gi zl5g6+^OmWvSt-LXv##SRa;Pm5R{l^cm1?C`6_|L5eG?aqpK^R9n(a)Fkikl&(WsDh z#At54@|Hh}dO9T{&%#&OXBK7k7-?L>tfU#iy@ z*9N`tg-}x5zI?u~Fw2R>N^#v?o=A#1m~6QW7&8RLWXsH4o>#J>W?O2I43vtsT9K?| zjlR3w$HTgd718S341O2`W)9!hQ4Fz#Ez?*vk9u;zBwIPq6K3)-QGYjc z8f@GwD(co74Y&`~OIM3oS<{y7sP@^q%&K!f)+ZOiGgp0dd>~|C2hYd@b#YoDeOVDT zaVW!?qQb_j{J16?Qnvx8hQ`{VB)`;9ExFGY!m7!J?F^=;WuNUMW6&L3BqSeNltuaw z+1#vP5zk?3WNf>1;$23UvU#(#?hdn4*-lRWwz^wcqilXep+5|bVqxm+ZgL#>gO8Qh z4el_HSq`h@LS{GfY357JSD}%>vXtQVt5=!;rJB{q(bTc$VwDDdpwJu=oVCg~-YeZY z4GMVEReyHhx7w;Ps7DBRcqgGa2Q3eNUwW_RfyyyY^xn(= zkSGEN#X+4?^pl433V*__kTC**`4Weh8pWuWXl8-w_T zYm9oDpaXOK1sc7b)`}F#mKRD(smj2=n#`Te(7k!x7_#hFETJh|{=v;~Y0WPv92JG57EWR1U5Qf`H>45y+PShk_gbi0 zw@gUV#4P}nZ=@$TPSehB5I=MW&?i_mV!>o9h^T|t6;g9LtT7GklE(dlX9ds;ki|qS zc6^7YB|1w*b%`;nvpZAjZ1%SGVLuvF7+Fr5p`}j^hy$CVh8Uf7+~voeeSN)a)LGtN zo!dS#>=9Nq5?)U{It&>U;9%c_V)diIW{3f0$Re7mjkbtT>lF!sPDoUwW?dq0tLp2z{(A7Op0w;&Ut4z4lg&?@wCo4VmoMKDBuG}&@6g3N zWciMnlUY0G)GoN7HmAO%^b}6xFg2cPC8^&72G?A&2d!Dmd<^3Q#2cC}rA?>|6f6(0 zhQ2_-z=(k(_*AE=(~vbfW*x17h(Mu$OW3Pd;c%wHD7YU5(uC?J6krE4#c-oahzO`@ z%|;K4JiwDd4mpy~C_G|Y-NL^v2^D9lnx7JhSSk0)v9|oOc};^CNFo*KR7oV?TW$Dl z5vZte5|;A?nKM$3nV5I)=k3l}d?gxlVnbbc?m98O2@}7r=qEH zvKS&V3jjRdAC11?%qu!sA0BGblP^s8ywDHCkZ<=LA=W4n#d6-#!;Gb6ATFYvQrX{qWb^jyLr1{|k?q&Oi-D#yy{^bk(6j@gah}1FF#L z$Xx+#s_<*R1>EUG7wJB)k*TLP<3evULQI%QD>10I7kJsVOMnuz86lirrm` zhOiS^f#-lt8J;M}s%VHIJ7j7choSW}jl|MzE+N+pe)hDH(+hm8))-Hy@GOgP;HxpX zxh)`%E-z#`RYPdHkx}GY72m1=_!!DB$?~o&+nu$LiVzqHdnV#@hx$#ECE-N>yw0<4 zz;M8(W-uN!u3yI*X@@+X09w47+0I;#l~OLidWG&=p?@Auz)*eYxDZ$1$6hT0l7(*D zL|_F~Ihv6?u1VN5M2DXU5t{O(vJ|>S`$2sg! zpJHoPkA5g*Db=B&e&BmjGLueyING{sm^SujU_VRW5bJPJz2^ulnCbK0-^>8FXux%3 zef9i)#SBTQbS5d;_E#o2ZbArmMPi;F)xB7xD=hraEb3>Q)@4=b+j*t~Gqo9eFraa_ ztPVOoRgY@bjnW4JJ#5>dtX3-Y5X>H(L3*@yZh^fNnqV7rO8g39z6u!pH1)CqvZyt| zT`1S?rw|KbbLvoirAQUQU@TQ+PLYh5nadSu%!du{%<(fy6UB9Y>*M3~Tg8m$Yv!1j zv9o;&Xg!xv{I!NK0C@+&Xf8TMG7&9XF0xV93h|=q`0n}C^2L52Sr#w#@VV|7HTeWo zI*%psXhMr1xW&}7nrrzwhsB9_98)J7SmAW69Z0d`U_l3-p8~Q^3xP$4!KKy;{ThXv4BJ`q<$MK$2m`p< zp!SRBgXTwhNEJ|X0N44E3M^n6OMohVR5m2wnw1)kiIgM)OxC%T4AJ zsv%oRMCXTu;rgjw$npxQ_HN7e+CR2t1oVP1vXO{pyFvsm+_yM_|AoS(}$7m#_| z=Py4c#3wm>)x^+>;t&ufQ)yeb5RV5-1g7?V@?Es2ig-Ga3JWX}U{`o1eQcoMm?B66 ztgqG1U@ejFh`J6WC?1oou*_NK6bff!uZw4Jz;0MrM%OnTnO_91lJ);)b%w`+}zFaYX zD^{k{KhIfy{>p{t>B{+fIIL@iaVwk}nuegpZ_KkAZQ%u5w=Nhz`QIcB@Rce^IwwdU zLc&xfT_@3vW3CGy+N|cX`CFG`<0g;?{Pned?wCE$mr_?>xOz>F#!W$d>CiDzkAix1 ziyB)zyxadk=zE4}jPn9!%X_v~;Jt*g>j(Z=xImy(jfiywZzd625hTI%5M+oYi{9qo z+ytN=fVMe`YJ(ZpKvF&}(YU)-EY^yT(C^Z}Axf;UFx|+vl@S@nl5$(Vky%ubbZG4a zipWkqV#MT-J@vzAC^O#fI&c86S*K$jDd(uMmZc_s?~+-vARjn}e)S~1`! zz`zUjN-iW7;Pnm(^&EE3G;8D&<$0CM$cMhMY}+pKm8qYupSO&ha1K5@k1$_8@kFxZ z)BiN}m$5Nm-URghG1N&VObw&Dj#_BgLq#a}f*wlafgzr80@c|A`#^pHQ5jI91E82H zn4mR^0+}rC!oC42&eNoX5KAmuDpjC*15?>ya`st>l!FqevW6t|tKt$Z#hb8vV)2$r%`}y0j6&f+BaOPXx~gt{ZoAwZZq=AY25SuQkIVsUDgl^E-8~JA+6GN z`$LK$Uv0*OJgr=?+u$4dE$~GN5in^XZl0)#di(cdf}uViR;=$J(hfAmB3|Ox zdVd^6iD0Yz6kIFm<+YO4?yJ&lDIvGyAuf|W-%y0OOz zGQyWMIk_lS#IEKJH=H?J&S)X&P*L|jLL^p%8HmV0!kJ^hXL%o?lUXfmr^7KjL<28x z<0G+D90)b`7hWJMkZafsczo9huy1(n6N&M2X|%V1;5UJf6Z#w=mjMZ(|2_>-Z6E-ho>9LyuF)KX6@9Uu(GfQam9i2qJYQ3K&jAJW;icuK6w$)zKw>T4Q(VF?B(0qLxsla8BlZ>bt3#$s&Zb2rg_!o?u9NvfELf>CO-@k;lssu~IQKai^eAc4~l;}o3v)kvS%@Al`X z|CRekNdG`>pug6Ke_nr|gx`VwK8jk_{hITEapeR2iHgxe-fs+4M3-*trtWWWj7V(+ zdp-j3ic@x!n?7w8A}A0D4LFE^gw_YD*qcbzpNI?5;x%d&5l+f_z<}U&bG>G5+O}^3 z3P;!$tsLCe_i(iC#06R}t%O$kUXdWTm5F;$X~Z9>m;1|o1>kjJKRO?A``wxchSJ~H z=xg-%fjqEt61{D;4t>Egr4o+()}PuB#QI|RV=jaYzL2UkGasxV{E;Q^qY`H2P)DIj z8ikIbPuA=86D-7RAs|PF?FM%QLNKx1;MZ02Eb3* z2_-_Z8iU8l8deeXtFqWDhU+l|6W3)i8WrVykk54>6+;Oc_B7M9)-Qs(=|2R7(nitU z9UVg}`>9rqXg%M@Cba1f+J>`@$0*6CUa->dzOgHMxcw9D?Vq^Fl3)~;I5>|0?!fJe z{mn;D5rC~>Lztbie)20mIl9<(xRq@@;I4=d;Wuc@F!;@7BA*~4L*5|V*K7?jgVKoW((eM5aQ zl`;)4fuE|5o#~)MDIi^Fj#RQulO-5Db_deBNU(wm5upyLma8^Outve?;WuDqMABPw zfG)TA%Of`Hxp9Y+MIfug-Kk_Ub$+NI7sKAHQaoOYZ?Th(7KRm+%UauXQ8l_$QwKP1 z79*M{pXK?wYxpPHy{hUco{Er0QG)-=P(!xF>#`BEbw`Ca{E=I-r~Arb zDK3i*Xxi8_25f3C{3LN;*G-K7T*RnS#C9k1QRZ^ePZpDHkjZ>Y|8?eJ%?aJ4X#E&G}x(0{WBmlSbcPi%9H}B1&A^pG$~5j zGXq4&11LyewFUyExJWGmJp;bjS*V8&0F82(9ub)2K1Kr>WWKK10EiIlUrD4iEdnD3 z3s^MRoEsURa3eJ*++Hl65u2l`5v@{&mD(hWf2gV08X$&9>n8VhJ8DJg9;a|RnoM|Z z;#OZLFNjg-qkM}`CRiR0+B)G=lr>!gmNl+9G!ra{9gqaJy-g51IWfy69K8*y8z%{x zWDELEZ0pbnHfCaLh%LYY1EGxlw+LyJbzC|g_@X2lsw=D3Fc5SEj{rJjSFaLPN#y#7 zuT^CB)7YJX*eVy>GqNNr8@3tEtyfIjhyql=PduyAo(&ETL9+tjNC-9z!tF27{js(b zjV9x%P&7j~{y4d%x_@pcM54gaEntX1sXg<2*;pM>Y7F-(1VO{^)ZO@Q!e3?U_JdeLuU2V+n=*z6nW!_sM8 zY__snd-rHY*f&3EKL&uG)rTpFty(T?XtM6$#2uxaBRFd?2*OJke^Fcn*AfFWpzigmcVg8R4# zHX2doTx{JYhMX7zmDS*`E)=tIMwZyLDNibfr5HIuH505BPOy94WAMo@B!yFXG0t*P zLsk=bZJ-WxKpf+@V>>N^(91lZ%CISpJBOCzQpc1-vr#U`bEiQw?Z*SK_Y0c;j9irh z5BM0fg4qbob31b$=9DQxm=+MwDA5RCI$DTs`k;XIAqy#r<<~K7fdoZSl#Eaf3t?Av zj5nQ1NLBc25eh^ZS-nAnv*1OEKsgV<8Jdj{C_2rw9&D$fzH8its>mC%S`?J?;O)^7 zB`4ndo@`x`Na*2^4OCoDB+O7G?D~oMi9*B<$z1dlh`UTton0&z&xs^ccs~-GEgD}> zL=6|OoQj8h+!$wntHC_vS;bN;N2*KTKUSc=3+r2yl)R`odMJzqs^{g%(sV{X---xH zAnP>%GGXnqwZ`_#F8gFkN?n)Brt_L|pWLR@WFb{lLs2u5GL@8!S0q3r;^+sacsy1O z(=>V5&B0-~65Q9OEc$!NV%(poMq8_27dZs;;Z7VCv zN;pDfX~D#TWs>HHN?sH(Nf|dIYBDrIfwf1vD@&vTyc8y0NrTN|ydoQ>qpoc9DQ#v( zbwshZmgw+8vJ%7$?hoS3E$9Z!2V>z0?W5d}exr-j&cHJEt7$nPnjU4kgP=nEmlm2rJt z$+D{od^VDaXYrC3U2^2OpoK$-y5+F8o`#S6F+{)20^UYPf=0hUoYQ_m#|Q%DTOEWu zW1s?Eg(6NgE{YCHE{Gz`(RI)$!PcTT(-Q1ys%ffq;g+3CFHKDxbDk2XUZhD9jc*xT zeW6G?Xlm-UAT{;;^E5@j2=)-0er)G0Yvvt2*&2=Fym#or)iWn&ws!8qHtzwJ^eNyz z6~xaEGV`HtQ{FJm643r>?2rOK)IJ0$kMec&I?YQzGFM9@4&shLJ?EiM5vz-Ux}zBS9_#GH5fx&cc*?-5hjj45O9nl#%I#J(@bBcf`bfKXs^KF`7-={Pdna%>y6 zh+?Znx<4Y?fLH}i>46tV3r%#-Z9p?oQh$>B${Q}&G&E#-a=0K|mC_tjz7Jb0Yo*?J zGLmt&hY%1X5CDMm3*x%mk8es2m@U5sc%s&z zdnSP(`4p9x2#v^vAEjBx8kZoTCBM%{4lH--TNmn*-vnNRw`oWe->Ec_hWG&bQ~(fN zfWtt{1Msdudx-cb6oG>t#qhEOm=MtBQ4CMT3uyet9JbIzGEDA&`Y`gRETLxpvM4OUu<+;dMVhX(PbIlTH$( z)p%ZNHnin%hqv-XjZ7 zt(SjSuGh<5`)h!}ZbX1H*>Kv_+EQO%>Gzf9)YGK?QHWsI_@n{v8xQ*bHDt&afrvS% zy#i?oasm;-eO!VKMnTG^=Xid(LZg$#=FvP+{P?D5Y|{;7=Xv+%qLJK}&rKA=c5|Su zd*UOaxOPt=6VHEsr+!^LzUkva@y2brubl6s&%HnAO#P{E)O2IgW#ipv7Oq^!Yo~oN zPd5;86rIGR?ncy733e|-fQAN;m_yssz^1%%OKGM0ZYvM@8eHTp~XG-wzrSgp|O&V(S>6j9b*ee@&23!&29ANQ@Q@=U~lhWVTi1J zmjF2+tkc;(^-Vk0-X62b=0Yk}n0lT>Bjs{r>NQ{jwh!Ty*k$_t6j0 z3O+>3TzT~C9@L=RW-^xSZp+nz@k_n%>A%u9%8=D`~&j@^Ay9-SMT@;N5ij$ zu#ez;g^Horc#2x)OWB`(MJwFXq~h~g&s5yzjU7iJ*Vw}nmwJ`!Dq0^I?dmd z=F`^-Y2hM3nyzNqCNTiR$WtzREa zMRQ46agt6JZ%|7o6e6d9Cxt$uk!{-4m$VMD@85qz?wPvessA-~Nfe;!?mXe}^F;0i zeBYE?nZMA__vXupspeHfO+b^3;Cvh}yowp{Y$H8tss`Dqe&93pd_MmxRN?2f-_@7a z>r02B#tX7#;SB~MIK4Q@GHji<7U%On&*zura-?tSjP2VUVopu2CA(+M+LOz5=5l*9 z?VjM|Vnjyf^LtPlm1=ok%jY$iwRD|>>k#_?h0u2zbOid^u}~t2p1J~qx3{v?GfE-v z>0k0{uxosjexHF|M~_?De>W=-U;h<+V{fcB^$6WpcJ@InO%p zc`H|USEuTn!*uuL)HB^XJG-;9d1p4SR+|)7c_mr0WF<$*YXu8A$g&(U#)xDC25e&v z%K!Y28^4Ovm-8Q~^iAWWG11nj(Vr1pq=)SS~wuy;Gr!|!?wOzMWaO)=K z*6z97NS3vn_DZGOO2bJrwd=LByE4tm>51X=#PagQn8J6A2RvSCoY*n8x1DZGOiwg2 z<97qn%kN5iLtp8a!`C68H&9z?o3c1Ih8(INTA@V0czjvZ&fQVBOO(LNB zL51C23YTL=_mSv0uf-(7u`KfJx28gT(Hy0#Y!1C=zqB8GS+=@yE9uI;-!7q%TE zdpkY;M)kR?SBLj(ZTN}Egjex$8T=eGZgKCeu3wvplYg4@5Bh$-HaDA@-+E+Qc0xJD z<(XqQU9OMChH8Ql-_YV;jGOBysKE0?ZwLNFRJ5PTZs7U@|FJv|`ZZiFFxH^NvI22H z=$Ibd{pi(;vk%lN4_|%t)rVfK7JDDOvuybL?C}qVAi>k~ZeetGGV9xp1IbE)Q9q(- zQM#gmDTfk!e)qYnA1+q!?QXmJ;q}|@JhZl@cgLZHNqbMyD7@X&thAF*Ml!vlTFB$K z8nq19)2ssMP&0<(3_WwhUc1OxkpZa>q8GUlyamo1yz!s+^?p-boSE4ZJ#pde*$XG4 zduC=-dYKKV3qO*eJW%<>#^Sh`YnQnn;4V7JyM^tt;l5aP$k&ybC-smzAEY! zuX+lwKn|X}--8V9St4*B9QvuixAn!L-x>O&q34JG$IuJ-Halty9`CEvtJNFSThx>I zynjyps`{d6$##ZT+>uuRv26FyIorGmBH;D_^uv|Yz5x*6{OHm=I`a#B(jGEd!TaFx z4K_iqVZ#osB%9f);3ixSFrQonU^*cwM5*otZ7FDK%~YDrGU*H-wtRH>5Ce-g$?BPn z-CKfz8S- z;ciP1Fcq9pPa`Q?b+Z-kvp*IsXVKRUjX0bUBYb7V!?vH)oI0sh7Tm^@`;kkj+ z22aG1jH5tsi`{|e7aWkL%{vCL7-%Qfk2x8FM>e}ng2^(9x;|&b%}@julV49aQOX{| z7&4j_wqxp3UMuCfWQ0e;(8a{?S7}z!HnO8i%QKo_No6hKio`Awj%kFU^l(#7BHV}? zB!rPd2bfQuNg}yFW?-|Sr6}=^YY_;HN->{cW@CT0g(M*_LL=-MZ4CPf6ct?OZUkSr z=_bPA1PD1tvq;awuTEy2ZvP**7^?G3blvd~H65ay#t0-!Dmm3 z#7s2G_>L6B^gqqxsZCDSs*|5k$y|c|lC*1}`_1x{)eb4#nuhAdYpAluh(pZ6JX-YT z#|#3=s&4W4f$A~*LQ8CdKE<~m(terb?o2|n+BA5eUh$*Fd^Mi!@nA6rbzRS%fKVso z;_qN1C*HvEpDgCnDYE&XGGV`3H;;p_ZRs_l|0=ECR)&i&R$G5sWnD8IQUHjmrAWNbq&(1!eDiLU2w5083$5oPtp|M=@ImBo2E$vbDF*NU(aGr0b z)HlR+g~p5i1j2%jxGbI4nxaa8rao3^7mMxxBOe8Iyt6b?ajaydlPh*hk#0McJ#&2P z^}80Um58s8-TaY4yAZ6;ry2$Gk_Z>#j42cO%$8P1AmdIXo2(>i=ESI+5@)iBVYaaV zy6F8!;k!6L^kU{smRxQWJ8V|iP6XCEm}!gR1^(-0CK1`*SV*kOI^?Qt%ppWk<+Y7# z+~kCO=93RT_{j(FQ!d<2t>J>3a2H1klZ79I?H43w&c_Rc!VTAaXM$Cd>B>fy|BwF0 z3oGtC z(mWtvkOTq(-dHc-ZXn3T3KuKCBf)U+6rXH+o+3A28yUIvWx)UT}(=wFliIdYl1bh@myrF z8GDzt2d<7v2*G9PLRf#f1<=JV9<+(TN!k6_(-$s0ec?xHBO`ZH$s&@>Lo7k1sVXMOv2FJdpjN09^muYlm|S#lwfg`Q}54l+~wqLy}z{qv!L*g^IX6SbO27<01-xJs{{nhikInqP@Lt2kUE&#P_fIjn|xu?^|fkjZi{xuq8A)xFui zsI=TX!etU8LT8rL(3W(EAR>YCCuaG^%~O7TF@1;{53$tZA+PC14yBiB5T{$kv=N`3 zpCJfdB&jVDg~+Sd+}1pF$Dv*A`F6XIs3h)5TWs2N!?G!RIE*El%qBgOvO@8wW5ZPy zj>N;i**nx+8eTYbXra(*6^g9{&QL>SOb`RJkuX*1zrDcYV?k<&^(sC%;ol>~1LQ7n zB4lMu#HMK!5*#_#3rIxmB&_lFTrzsXUQ5QZ*JndB$)8HrEcG9MlK6{0a;9r`-` z0>J);%3(^Z5c8}noK%4;x7i>&!F#^z_yW^#BgCm5H|%f#D6%oT4FY5=2; zsNk)@GlB8n!TRmn0^I07dboK@fA`jv`tM~%o5eg%_vY;EzFDr1T1TC;Du;uBLXN(mD(>77IpXR@g|nN6XkrtyfN&}LIYlMJs{b|yWO0bDZ64>PR7JU`E} zgKRpbUV!!B>bF6en{&vn&=ZrbtNpRuVQ~V;-{$vV4yVE83ry|zO&89D`_F>+-@TY>Sra3RjiT*G zZ`hjlld}mpmwg1giZ(;_#AxBva5CB`xzxjx8aCp2y*pJ94lw7#z zB|EMSm>enIu=;qC#2O)#_Pus^{WH@$_V};Qy>@M1t+BjNEhHC@F6{N6IDY-z>#Iq8 z49(H(>e=6~Oc%zse`fb&jIuFBRok;V+>PgIdyd2s3BBE{9j{&3mpOXntIYBIV&`vL zi*+9a_t(P$>ap;GEtG?j{OO~*YklGU>^H@|E?f#)>~9b5>MqBqu13>7{JAXh-_xZe7N1W2k!ROX{c9XNdWq zC(=(YK504q9wQ)LMWh3cGJIm#dNPK~uv*g68$D5kwcX_Bf#r)0EG2hD*NSRTN&)%c zYEjX|D&fl7eoq|VGV=0}DtZt4c5?1S9^r-q5h9q54lDB6zF=yYab8p{j*M(QI!Cr! zc{n$=g)!FDI_GL_fuXizqz-~y`FqECUbSw%a;dd=V9&OhWVtmlIpvcPs;NwCW_Qza zzh|Y)*|*$v(p9loXkqoliT9+EkoJQWV|zPZbXJWq1nFiZ?*lPP zq!Vy+C)Yn`rL5UP`}_{DMdr*}%c;CW=L{S>m84E-GGTcf9l*6b56QjxJndx?>+=Yi z9z$xIJ|zW)@M3lXWftZukzeRia-T8g^z1?~BGY6fS`yr0vZsu|zeyS&_a3P#Fq<|h zq&;r52p;r0-~(pNiy3RH%hjozj53RD+nbu3EEnKxNuDf~6QLimhfhw=T-%N7P&?!@ z{r1Y@T1U5rVF+ia3LDm#$EX_$n{BW(s*H2gL*RhHwcJ$H9$872^5OS*1>4>-98Tx8 z_;{^6k&Bt9NA-T$4HaTJ=cl@{{mrL57DbEKGw7u#pg57@gu?g z`6Ty;Ec6yZR5I1fL{p`1`_P?g?!))LPd#_*srB_Bj`XkU%jyx>g@=TdOd3^Z3g-n$ z=Z4&aEDAIYokK9Ap3~1>wWCu+KaP;9ZC28uk#r^F&PQ^|cteA04mW81n!T#M^PbnP z`pwDF8R|qitz-_qtV17&#}>57Y}(hYJD|mqK0na$b-__ejHA_|uj03qD8{yxm1kF0 zu9n!s8=pOO>REz%m)iw8a1>32a-QpodIH0?DlP0QhR`un`lkf;w<@YUV{ z)cU{YLZwnDS9cN#C%CpK_v7!?Pa(ke*t&&FODEvv~a$qGOd}Ccb#Is=3!kvWYB+ z&g_}evBmiMpXbn|C?6RQC&T~M<}dv!%mv#nzP`wkTs)r5{zH2fN}wHQNB53-VQ;1r zAJO0DYMD&7PPR_!LA?{ZEuJL>)4|u2yj3d|tVu<}Xa1sU8E@w@&Bej10UBI(i6 z5`rXSJ3#}Gxh%FCV|aiKR3D9b7}kZ;>78fhgyD7n-ObN^X#4ol<3 z{%Mqm%}yfLDHZsg~5k$C{RF#XA~wYT7%pDg7U8 z(F!o`bZhoSf5$KyOsl^!Khqd%e8w!A!yE5b*o|X7hb=gU5}HwD=n2OnEu&<#f;HCv zg)jc!Gx&PO_|xkO<#ORVXm@MB!n+o{?-m{qh(hoa8QX|yv^Re+ex+uZD~7RR4nDb; zJlISBsy(b86@0=ll80v}N2BWa}0~m9v7FGNDK$$XI?(5elFceZRxE4x9MH|vBa6)&g-I}R9zne zNh6mqt+n~qj#woVa?;^+S#GK3N%@6{U8y*cjN2$$VHh)0DLayO|I#RSGMz2Er^a_! z-5qDX&MgbainRJzZwa7)E9}*pSbfxYtx9#>(yAEO%Fr zDX$ud9msWBYOI-a?X=(Xvt}wXJT^WWn^3FK1EtQ)PNIvBlSvlKBO1>NzRTy}yL^tb zH^Bpmc42oY>?T>?(!2wpb49cJowRtegORs2uGH9@!nw-CRAp-SV!7PkGdZ!VTlmP) z2iJ<%TvJ@r-mKE&r`G@PsqwSjqy2W;&Gc@0-)&oRxy1_y&piIA)xxz`7gvAbV$lBb z9KXZy!+6^g+%@hN_Y3d&Lbll{YyB_Z@Lv5?^^QBLr=FgloBP6r_kHD5?T$NYr@r>% zb2r_76E+BS-v0H{AEK-MGLazJi!bfi&&)QVIMe$+ceSgxP{{ob$-}+5+ZG?)qDOBj zfA@v(&wkih3{_J^5`OuUh!~SEi&v-V%tQPREKHx`zfQ)Z&vy-GiAp5m1e(S zq_Spx`?*`rZLgc_?;9KY*5u8j?K|%L=tmy{2U#uTa(I}x?r)>qCMSmXnT?MBzVD%ErUoXIIzQXgWCy=mmTS>E{Rn_MNq$i*{aKq+HlRlH` zxqNc{{S-Y(s@s$KmgU~HefwR&o7C;r`oFBMuAb~Yppswy@|W*kdEjj~s{3DBHNXBS z>l~b$J7}%HAAP*lT5Gr8GjadM_xeAzFtZ1JWS~pth3`2bky4P&%?_C`U?rxEY}=Ch z$D+>)&WP8Paq6*S3u)W3j~Dh?#caLSYWsy|E?U@=)k1CtK7swKZRL4(s~3%w^CJ`t zm{{JvcXq}+u^+k?gF>O{shx{=z*$R@+C-`TL^ZBR9kz-W3h9YF!4;8%n>ua`FZOM; z9|TSya_GdOy}Z(!O>iRcE1Kxv3Up!K#zXOm5wn!c&xDNbWe)IfIG_~Kaiy5NvUenQ1ul?tK`_n`KPoERJdRJ6 zimVSp6?S&nCYwQ=7T9xa6eK;7x(Zh=HB6fJoIA2G@X<(;VC+P1WU8EvOiKwD%P(c( zmgeqSZJp>1&*b4P(i2pK0!L|Eq$L1{3x}&!C!17U0*br$KefJ1F)i^u;hTyiLR-$x zcJe(Rv<729!cA#vtTJir-!*w=XKHe|bPLL%YdB>8XvYc((6yvh9v(?nDEy=AJ7^nV zq`s(ro$EJgv-^;c>2?Q)f+W52q=j@Yu=7d|}K)^-&tcMCgI||U- z=*r8QqV#jMHJnL#dcN5GNOvUOKD$&ehqY{B>yc8o-khb7uYaepbo7S2Hr*~259Tcu z+tngNdS(m)ytTurGe2W^)!l35EnCL!zIInQopDcYZ7g-HC|nesT%^DF#2%$iEKyK= z_{P=3=(tDvw&SNNsYnz7`SLcuA|o}Cj+aG&eT?jZS<~rq86810;aL=ah#feo#Ksu_ zcE*@kf4X>}VC(TztE{x!fH{x0|wc!N| zl7Z2seTC+)O)Y2r7=TGg#?5NVaEgU^FivmslivDLWR=<_#QC9dk^U~1i%n+p)S!QF z&OkFES0)xskUk$^x^Vx%i~qMji8zz`WK z5ppcNDwr)!v6i%)L@{rZhY2VExu8P`1WJhRZbV{P&r(tfhCf?N*h#;X&J_Km6W4@- zK{0X;3WNP;oIg=7NvfB4lUoHhvfRc)TS5% z|J^oqd6!b>bQrPFYf1BdWz&jG6)$gDL|$~VL0{gkXhHu$vwC@#`z>gm9J#kT&Ew?_ ztWer)UpRBQhbBeQE^AzH#=Np^$$0X-`Xlvmv`x@cM7HT@^rQ;I1^o^nC~jqZDFdTQ z1TC7?K(uDmS8dC>D;6pZx4W6dyuW%YNsgg-G5&^Me8U?~#agY%uJ!L_10na z^IFtcJJs7!PBhDI^w!nV-c-y>5XL+823EHtPJjK$-@Z0^>uTeu$U|$$Ko|W2a(*bt znYaS`4;&G6yIH%B5&zPg7Tm!NdD$DXC%b+5n;wznLN0q}Nozo{?vM)H*RyH6voI{9 zMG(UGz7Yu-nY3S?On7tKL)HqCd-sYJ-aZ#ePL>ndj1kU+WrfOQBHG;^F=d54D`sSS z*Go=T{B%YSeg8Y(`Obd{{=V>qFDUi48*X^p4c~e6vbB=z#`QdR<3Q{uZ0+2;xu1wX zSxKccAT_CxmQAICBQ1yJNYzV27cZ)zi*LLC{{Qj71M1kF-+A?`zrA$BzP&fzxR>Ej zbf<5ki~b^-_mr@o`&}F9fRPM4CW=6SIU~9O(Wi|QegXd?4;+0mGdy^_A}zD?4F1x@#nwX!vf8%bTewS-4&CFf1Tt0bS}-^G?T4ntA*@bGO5R27P z!_{8Yc9X+YOhwbTR64)EgR?)MO`xNila!Y5VukfDBuhS?#%Rs)?bKwYRx;A2Gv6PL zz>9-l+l|aZF)RM&$SfJE6nCHF&D7!e^p5(zb5^C*a1(NThf|=mgHvlqx}%eg(GylB zoptm{bv^-S5Zn-Ns~z=|lbmIgq-Ob(e7P*x-lF68>+wo@ZebXDa?Z3-Ym){`!3T*| zkL-c8|GsiI8-t|uo%wosSFef0%V?p(?{i*mjpRp~cg=Fid#*`Os0E8(KJ}ZGsp4%1 z^7*;tsZ-@dEclBTkGyMa$M6#joGE`)WQDZ(O&y{omdqx@`5>|+ssGY^HsNc5_)2BO zER@WtG!g_^L*?jpJwEf>1gQU4h(+%2&y&xi11P@VQ2AKW=Rd#wb%7N0-Q%|NU zmDKvr$fufefO|M*D_3d&Iwqyui#Rc%b5redV$93b@Tb4H^3|!z`d_{S%|z&ReK&z| z6OD(y<3yrBA}28=5>XxdJcNw>HTvN$H0d?s0w-YCJ4cVaFm%_@z33Vln9#+1+W5-K zT(dnZrl4fn2A@DJkjh6-sd z9netcqIIoDs9{Rmt<+w11l=RAZqhWbtUX4;#e~uxC0RBP$Bt5Q=tqZfO>5@j_1jfq z`mKFq@v+(UA8fyN>mO|2v+a@PecLCpd$P8Q!S%X(de7SKkxEf%jpB~sowQR8uFmJx z=kQw;z?pG(MSgUw4eec2G1>sp2(is*A2J@%CI)dOX%e`UVGS%TwYJ`O@O4LyzIH8J zfH>d0C$X?)OSiklr}9)H?Lquj9IuaVcZ-eea(v{bombv{*QxVche=m7x3+d1J96aM z(Icjp1k#_&O4tia3U8UVtS*A&D1ZSvJp>rk7hO6~52dvd9F$fm3w**s{S5LAmy1Sx zp)*6=RY&p)Ni|DA_n;<7$;M@KntCnJ5@L6vQptW0mRO{N@9y07JJoKFSX`_;uT>!5 z#v}ZYx&~e-$OOXCDk<@Ws1uGxD7%bG1*8@tOFa&`mtiML5a)tUC%HhWTs;D}jQ;6b zMVqI>Bgt61)y_CO_SAJduDW@J9hk=O&g*B_S_R<~TmMC+KB7^Hxw<^kp%(Q#1T@Nu z5Cn&ZO4AdCoLQVGrinRv&DLT$J60`HWk>f%Yc&!ZexbECd&9~w_N^OPx%sLcRC0MK z|7TMU0ms{vLT0ZmEZy`8^zE1iKnlxb!x*_o3p+pXRCxQ7kY{Q4A`~mj@VgA`h?6w!=9fdWrrbMmm30ThE~B2`!kHTJ?fjIoOBR5G@@qI z46>{PU=F%oMoq$3=Qz}<3Q@%awO9VDe9F|3cqmq=3qlJ!3vr>jqjge3h#EN$2Y2MC z%G0fVGol+IR!{{)D`&%Lsu^pff_hoQNtk*ROaQeHj11{&5F%J{j6@?pXTld1io~-X z=3Ix2nlM=wkbOuUV*D~vXLdU4UlW~5CPzAd%SpO!h`O~bx;uKazH{u))TgVM(zWk`JlW6Ohp9yrQn2}-aUCX;(HVw zG-LWr+YVS(A#jY*MQ!uz_iz`4RYjNt^G@U;BSq4JXJZ#yKZEry=~AAPv<^mA&9sBb zf>?ih$Iv`8=MLM_=7|q;jd;|Frbzu&;g8)-70#=EcwDxH83=^{yFtE5JtQ2CCPx7oe|nsZ$MJF`9T_o0 zQw(ETA__5h?inSl$<>N-BbobY9)`M-j40?RiTUfee7U%c|l)2E|jiBJ+T zu5KI-huzTFMJIIitauE}lMps&#d_2n^SEx@gqOaHckx-G{j>O>_lrEILyBc5AFx4l zuHq`UGJ-%8!Hpu#xV`yerCwiHPUR=)q*WI0g3%H3lwfG$_hkfH#%>3ZB8jbw#9+@- zq#&a{(Az?xxJ17jagrI=-FGLr3#HL)wQ#AXYkQnghV66+c2e7W_X$v9+FK!j9OY7c9S#bb6LtMd)<}#WT;WEhTLKGaOuwF zx<9|CL>h*6=yth} zKN*O{*?I18o6tHw6#{8fsxc#$7WiM5aho$pPoS*GN?D_u0!|3tn)|&hnT@k^f=R?5 zdwpRdPiSo&`nb;L$1)5$2p^xyN4V^A(8B%u70JB?82C(nqA=K<(mGq#e`57`8aFKoeC@DlMy5xh0g+j{CqYh}H$0F~Bpr5A znFBAa8G30&JA64NB`Mhv(40y;LyCivLr;LoYhzSE_-A_Rxzjr=5QQ@Ryx~G8#?fn0eA1f zh7_jh6vvfHT0=vdW79^uLa#*#tEzr= zJ=2tuj&?KF9;>vsvggh|u$D1d>HU|6u6|Tje7<$Mb>iBaZhBRSI?`{d?12KgFS0a( zPx^COQhkZYRD^R8=8&18?cnbZ58Vly!>@zG&j6Fk2A!nS))1r&qghLuh77U87EObE zQi-_+_`HfT`CV|c!4LglH14%~fscENQCPYf&1%L<%NjHOY0LenGTC}ZDp7Pw*|MQ%+b?9H)iGF!Kqwtq>RvyMI0v*cI_Qm5*@K% z-xkwD$ry4+&Bl^MNT>{Geg(t|(hCpfohCyp@}5r((nTtSXsMHtn5#3Q;!llN^Hq0W zQLpR;FkfvOId~&ew~Td98ddLFZA1~nH!4(=f|JN2=OgAqTWd;15y#G@;;uHTQBFiF zF*>I3!6)cX6p;ce#4u^5(ow_l8EWEw*qBHgY3&$BC4*%|eT&v4!_+s!$(RvNI`%JP zjY|T8;~a9x#Guwt_?KZ7u!56k@v|NC9j~^iKRPc839_V_Q=ggf!FH3T7ewrM>BcOs91GhO|%e(c~Mi_0M@) zwX8y;@iAzwGPBiAxzn6FG~SKXY;RB8e@Da&Z@oS0PmWb8>1a0k{Ys%z-nX+?uaCsF z-1Kul?t4l94e<0WPj<4~=9-00%{i5y^1Nzvdo+C9sYP~MD)o-aScQ$#>lYL7D?jd4bKpTbL&<=Vkf>x zp7m>>C3%XHK5w9p#H^&!fAj+&!z1s}P1jyp^b-%5zW3#^(FX4LoZiCXFw}joLp|XD zuh-Q$Jc9}bbu z64lT(eRem57F?0pgn5m-fxt4!c3;^J{cF^YTX0|WRhP=2>MLJS;CC5%$w+>?B88Sf zIrEfI0U+cVdDp6qPNuk3*q&jqxAShR>1K10g?Lo0e}hzRIB>bkX=5)Md0Rch$-K#@ zE`@q)BJu@l$J&k^?qsTUDuXh>BZknOqxb{eYLwzl1VIZ`vrr_dRMYKT?0<@JASMN8 zY^rng4%^>P@^~WQ&dt@Qe91;9f14oGQkDwn-MNC6^$$#PdIV)GJcSH9V!`Aa*pioC z;2CmNSco}nN$%saMxn+{uoNSOJesc@zH%bRTh-O-gKOLGXfK?~d&B)7Q_idA>o?p! zU%S?e9DHbPelogmwwtY=9JGx<_qIWjG=jd46x*~_1!~z5oEfg8SLjca_5+(Ez6Q)e z@JdJpW-$-)(6Bi_HfctF>J7O6)D}F9i%Dkx7%L zKv@V_^rRH8xz5&Wpc1gdvh_-VnG*cP< z{RL5xECBC;%lYrFKX2+Q3*;T1X;f$O7*RaHke&I_Jz6-I9ywJsuthCBbm$}1y(##M zX-pFNN`kLg-Ld{(IWgtr6ZvGJnz2&+d;sVN4P^1X9!NNEZgD+oMr>o4gPHj(pKzX zM;s;C7w!PGHI5WThz9!_Gy|#rK4=m44$bC{3hM{2en-2gH?=13hQ1P+uH~v;?R6w0 zu7Aged^$Z`sB2oQ?07=p40UbG)OYH`G=UbcUasb5^n-dXv=v^2Unsf>sqF!uMl9|X zmsBLC<9)zAV5fiFiM@waYKysQsMxm3U|}msR)%$6rf(q#?&m+>Hy5_;Z6=tyOq9Ea;&$^`)Hihb28p3VKQ_7_Yfkdq6vO z#l_cYuS*WMRk@+5S}_NAB<$2CVHUkuG)h`hE9xmdeZO|sDQH(q+SPhYi!W=iKZIc1 zCZL>DZP!p#u(o$?OS+Nv$l-+MGgjPktF~j8HX8BpaEgRL_~;mEk%%@vr5%~nCdY@P z9j(>TYNU)g(2nS1BW!UL2?#xqP6=f~>3C9dkOX1E5}S-g6RItC=q8>qj|Aw0JU__j zKzoq^o$-&r6i`hV#0Sc+_=JKXA7*lVOjz9LprYHN%l0!bCLX@z8tthe*GNQ7^Yo5b z%>Jyz59W8JOf#A=_W7n{{Ks;iU693P(55U+%;7)^M>d>fUw`x2a}UW|?T_208+SfX z@(7sRJ4|54wEy2muqaLOu+EwiE8{!yu1dJ@htmLq85i5Itr9xw1(Y2;?>ZWvQ*j z(?v6dY$tFTpa=;`eh`}zT~BmZd@ao&Og3+xyODIECPbfyv6n1$ z4F_;jiD(l?BwPG}CM-Nt z5=A}wLknY`MS6P}bPoJ64)6#^BYqeOck%pT3mH=ke9%&9u+M;C*~7CFEbKpMJJtJO zBJ2m=J}`ck4FJ>XU>9@e5UPRv%w+wnG*NUi@$jH!;kfT1r%^*SD_vT>Wh!5;|44SY zp1HZPSB)**0##&XJTmtiAMoR=Tkm<*Jtt3}`?KqMhui8Oi^Ze2)Q9VD&m>>HKjbX5 z&>_2}{^;2DS}2{bPhF8wmG8ykM}8!cs75E|rlL-#UtfHO6M4f=C~G0`Vf{00?yXD- z=lj^8cB|Q1F#A*D#G*WQ0A(?y$=N)3u1FST7%%8urgm}tx%KDN%0s=M--EhbzG|Vl zYr$Xthvv3z&8fzG^}W&b=wpWuKQ=n6W}p6udd~~JuYR@n)r0i*7%s}TAyaZBAXQ&5I9Ta5p`_l@jp%gY1+OHVh+p>jD zw%_UXv+B3X4Rg_)Ew6udbHX|=ct6|HqDhm=kQZEkZ#o^>8lN9cW)k^()ia~z_1_g< zJ2hV(UH@6u;hF9z03q(*AjrcQO*>e?cL2;PleJL07GT(? zbVU%x2udFHvfZs@ozQS#<0VFDb<9BXnHOcAeSx+SNK%^zggleHAu(>y1kgz;c)_?s zq9lnN;6y5f5#*Q|PA5<**a^FC_kR_Y)zfs1f{9j|lqi2x(kqwBb685Miy}(!xIDo0^B-o>`U-U8?6Gmkc#Cu5N3h zUZEY@v5e|eopjb9nn0C#BOG%pMvBk?^%qjHWVrjz-I_Z1Y#*QY_RsE9^G{BP82ndJ z7XA6QX=n0yqW>`zCkdM>3&d#QhbmNw*;?^bK__jHY&0*Cf=%mYpR7q!#gbk!qryLn z!_H9QcfJ$lJ^ZeB0cC?kzKoSA$;e+Nj1GXE^ zMD)P!Ort~VYtPz%|GEU5kpXL+ z3_;rI+aw6$ddCIfg?_~u^$4Fx{Ub2;Ke+T?vAe%S?!8n{TYz6`$j25&Uy>-BMS^U= zRzepO@3Pgm0_PyzOgdFZe5CU97$M1Vkd7&Xu`HoC3jk(WJF@?)s}=>>cl`3QoiJ{@ z4%{p0;F&8=h79lRE-1#MCz5(Jp{1|=F(=}>5qEz)zL&Bc@i+meR-=x^0LkQjwzao! zwXN9t&E;RZE=1!y`GJC-Aka&3k;2o*sg)YK=I3UGPWOO({1$Pu(Ege6@fn5;sKUB`wTl`}d(Om-GAfB}Yiq1_+B9Peq^o8YkvpqaJ!WdBP}z%yllL=bV=gg{@G)#^Wj)}Q z(}KhNR}4!nsJ;cA)fwVb6kbCn|o3CMIWtd?QVOv>ihf-hU$f0~BUyet=7;Y?)d`*aUvX<=3Z?RkD5qjr@ z9$J6Sbt2Z}q#5z7so})u;BmEbyLS~xGo&`-$-{qPzBSUydkJPWyW5IQ9=bNvT<~y@ zY2nH!%}4C>Ik!ZnDg`aLrC+o14h|PchN-)z(3HuZ*@!)E<^cZ3oX|t$8N*CZDUw%w zo3EJP{Qcise(~b#?*8D5AM4M)I2kN_`Mi4OqI37%uY2)hzIJ2P}EILWsRJ%udU5Z09Hl9bJ58HoXJ zipbzO5dA@`Vjw_uLO{sUY3K!POir|V3HFNGm902g#ss8&A1iDi{ADngu|NuLqv-`R zG)tLevFHJVp+6WYSopqKun5L90@yWU$zphFEY)9_4)^9Fq5X5iuW2nE|1*nxv-H1nKV2w>1 zMw6hF(zjCdR#zWF!#48qVa@qgq7cE^G^$6xIlc=tR_pB1!+C{PSkWR_@X(n{T zU(oe%MjY#A-G==1FT+{i1KH(9&xaollRDgjyT;H8Bl%Q18-{RXJ{e8=9uPLz`ong# zVd^)={KuSTvKp@gW*Ko~D}3FsmJVm5Blzekt)BG2!JAfv&LVyC`2f>GC1T+?(CrS) z%UxiGv)LHllxCHP5|uPBHh=ubRp#%lH`;d_A2oUl`mA;14c=W?TkH4D&#P_I`ZW4& z%xkZe%TLb*SedV?Ptey3^z;l7Cmw3hud|jNj6MR?TUP2!W@q#Kg5-`boNw;Tc)jx2 z^75G9t~`07*&avZ9B(&IWUI?3mxH7qs!Cq^yf#LT_Z0O2KCU9_{R*&_PdErG+!9M% z1~rC7>AT`JmQNY#QfU$t%2;w%wr$R2+ksBNZ)jR7C7fh)^RsN-pX)aQ$0!9@u|G4j zgo_1{O!8ZQZnhh!5}81U;6Oy}`H>@leTIyAhEh1Z`eOg{i=2?%<9ZoqW&8zZo#8ZP z&<0JCp#qz1-R{iD2+&}W=rt%VWT40{F$CFHCX2ziF`mz$uHs){Zy7<^K-!`(Lz$zu zutl%MJ1d)r?U)q~CsZJA0WS_4yn+*y+rpIvYq1#3D3fLZiiF%P-q0LUw9P_4>~+P@ z)JHbT#qbe0I(kUJ9zF#N;b_6ztPN5f@#Bj>np5nzPoUN7Zo?osGM(NMwPV1q@T8LF z11)+eTn&T4(raOl`orG@rV7r&ifgTWk%$nv;f>Kk0e7k@!E!+ycofYDoeFc)gH;tq z3yR;NN8!|$4Cm~(H8pWyyje-o>jcVWM^a%gHY)Y6imi~!W@{4YsYwAi&H96yurcQ9 zKp=~zN)#$HHU+3q;y6x{gGg!fm>J~|U>Tw9bqt@xJUMH4Il_9_b^RRrn}=sRgg+W( zEgL6pV0n@Cw_EXSf*R=wt6{NEJC5T@z*&02Dfk4-;L9a>iU=Y4RuW@533;&!QDDfh zAzAgcbU2i%5%CA`hO$IvkD+P++d^W4;$w@2-E1V{7tM4dWcW2gxcq4e7rVjs)0Xol z#Thy3no81hMsr~+3Bt)+s`XCa)(QW)EPYDf{-DxvQ(MG zY(n--Y<^T}b z=?Y(pO|@vSk(51Qn4i)YaF$WD93KN(Q$J7y(kVy}J2=2JJKJkpU#n*E8;GS41x=mDm5!jqN$ zHQTQ1UG>1qmMgw}{(NI??eXpZedU3xde?2creB#1CrZcXw%xufpH5z}{uk$^Nr_$j z95G?3-IRvu?V5n$g8M~-lKh=b%I;R8-O1un-6Sj3=kfPU>1bG_&tiGR2%}&J<4D9= zA)*_YxyA^<^RX!Ph1gc3u`mTs3>yve-b^fp-ZipydvyKJ($#8Ook>?$DTyA+CJ3GA ze$BEbZXG8rU2C?C2viay`4HUm^j*_vTrnPtdUzp`WZ`a*Yc zrkYXbGS%w(3&T|L0MJ927V9{#`3`i}MW=9d82vUJGN^)zTgSIyH&2IBlGnANuRN!o z!G-`w<`1G?G_amO2yo*lwfmXxUj6XHSAX}d=N1kgy!~J-dm^u%dGs@nJ#Sp~mi1>? za&VzuI+%(CU3Q2z`xN(A1BEsO$Ed{8$5jHuDBTXe7u1ta&RqA6U;p)FB9=UV{xz!h zj$eG}KWjt!Np1ZTH?fZp{_Q`<@BU2CJ_fanCh;XNkV{5BON*AUu)$KWS?vdEp|r)Y zj{}22zfP(t@EtlrFC!I%L@vl%)>hZ<+O_McUFUeK)1H&xJ^J$xyn0J9YpG9{(lGk)o`YFA{}H<#YMefztYmJV#=?UvtlFK) z5qA!{xLid|&sbl08DTn~uL~MLn}`$5ms;R@y7q?^wBxV3$LC{%!Ra0Z(ZjZjK4+1%GAeTSI@%?w83{ zLNK?Z8gY(b(8^d-T1L9zKqzf`5=^Uu%Z=Db24O?G0?H=*gS4Aow};9pdNi6ae$_U` z1RGF|P-h$T*Ct+#YAY7L*PzLv5L@Ek)4F71i|mqU_{QD>T~~}o+_3BneY9eyT7y`U z_`ReVOW|Ab@e)SCjVV?K>Z{0V0K-T?jF3f@ixBfC`VfzXt=}Pq(kn0!MB<4rQh*eh zXujVIg*;as2`{8>*PLj5g<=z$1K(g2h$Ki6(U4qia`J!zQsBi@$;zObm0|Fs^iQl7 zyCJa{wsMUS%9F$ywJiDt^q%o*Iz2o|F=IoNyVi{IK+sS%HIl z{Rhg+lw%!S1D?H~TG&R{LcIiKgZ$J;Do0^?r!|spf@XFPkeQ>N3XyLU91Uwvj{v3! zL91(Mceqh7oFwb>?M6RTEZvII4NnD;6U!vELJP$9QhxqB(lpVrWl>}#lvG5;}vu{V+^5Dq< zLrBIISr!Qs*D%4&)@)Z&a%p%BkM;S5-szo3Zc=-jlj;}rt_qzzUMpO;^Ys2b>V93l z?dWUXa)o-YJJlMVcGq9kn4Y;!soT%So%8ge3}Y^J(qx1YPQwL}-~k&*2AWztr_hM0 z?7^FkAFuR>fbS=&QTZ{w+iljmE0Ti#X0rt6StmxffDypzrY*VI zcye&9i2xh81=F@57NAgou^YW_N6*?Fn}Aop+ov2>_MU5EhbdhC>9Xro zsA@Sjy%#)!=<_P}&Qds3cVCTfK2wQ=#>PXDirG$k>9+ZN_amTsco^SmuIusU#TAU1JGNH*qtJr{-{3_dXK9a_ zv)p6C-h8nf4+WkCglKaHr-WLVQew!-B;qT@` z57%*5%EWMObTN?8k}%vrOr-z)O@dto`vsFDk-gx6UpI`5VZ0;w{LmDw|=I{0m{4nNWiv*3^NlSIjco=)ngT8Y&n-Bi7k^}>XsSAr&P+^X(j(Q>H;j2*pEm}d z5AHk~oB*dRz7%=!#Xu!zB?~uSEFhjf_?=HC<&x!%)h$!pmBIWeNfhNT0S!A}yf?lK zGKmHY_}(~x>@f)32J^Cf;}z^skaEEqtS*1d@Mox_J0VVCz+to_{#GF?Csf5%9{2V2 ze>z|og{OEIf_DWIa+cal$WcKl>E||oFrH!sqUzOzYaS|Pv!z26n5z23-2U#;_tgIb zH-t+@+(@7Si31Pc9BLt9NEE}3(1jpDL|2s3*Q6oM#O|yt!~iu%KRZ*Jg|YlW%Pc4&!7xHLaaE|MEf9F95ay4XVH&X^l*wpx|hE7PeT4Y{+6XW#thvr9K_&Ep~~ z2N~(W9e)5>dr2Tti6CCEvBG&Jph#=AeVw&vW)d*5q_)<%-H+}<)Ogtv|jQj?~O$6{mCR%sEov;H-sY# zCujFA%6*?-KC*1%T+w5($W3L#DBt9S$s$Qc!~8H0rE<8vrFPr#NOehkm<^+~OwQi)KJQJMajUoI#w~m9safHWS-!Bm z_pbcaIvfkBP!UcBCsuh((;lnDK!!%jnNTW{G_KCyy?6VCvPld#y>i9msYm@=4ed?N z+C95xCQ*)l9!M*ybgBUs)kL)cdN*e|DUz@0F&e_?|u8L*s(fP zRafUY-90(a^z6*+%ox<%xzG9FH`sWv24SRl}?Tfn@W^M`nA|t2! z({tzs(0qd^G(GDFgKu0%C}c)cr2rH~HE6nTYzA8&2%6169S)lJ0;z}&SGx0rQdj3E ziE`P=X1UZRt*z|rMa|vR6n2_`YVFiQV|2o6UG&Vp&7ZXXk71Roj8fLKyb7#HBhgfJ z8+?U$LuHdyH%>T=>sZ0BzMh!LG?)}LI;ngqju*kcAYClbMiLCdG9O^5BpBUam~&-r zVI#Sco@RUOcey5GS}CQ+7kZM2*?w#8f-%hXi&iSLHNu4BO5NBwe{VMN+Oa)` z-Wv9H72xH!S10so!lsFtO3xleBP%O;mLw(k)!y(hN)`bbBoXt-%N-9YcDOWyhRcWixKEnd9!N6kxq{otGdnRbVcB_8@>C>LLAUMwmMU!hj#67^^6ImW za#nZk+WM1kt$zL6Z~L7ykuwoGAAO?m4H9jwjg74@eS4Mb+8~FzL0)B)s$9XSh5TL2 zTzeipC*U!7`*)Y zz@XtnLV-HJIJ3AQnU~O7K?Wmx35fLuh~9vXq0CS~gn=ZcnA9*TW5qB5WP1An{wNcG ztd!@AbtCg%zAx}XZkoIyBaPXz2&PFEhx_q}NfU_oEKAl90v&kk2CK!Z#oi%M!LtYg zSuFd8+lI*~0<5eZ9B%Nn2LoZ6Da`f$xRl#rdQ?&5IWKufEahf9lWZPZ5O_B_PCF4{5fW4;tv8Y` zq;ii)iBQZ;(HT^;>~HE|sO({HsxUkpO!;mBf&wh6JbZ1?U!>CT@$eB;P$mXGmY$9) zin1%oWSOLm@@ZbF8*PSgmT!ieYZ^g0nM?(7Hjg`E=p{)MX=ErcjQCZVG~}Mb59bfl zfPfkb`28|uU4YWWQY4fN5Ocz&r?x0h%OnYV5(mio0Hu@W42*iXE39P`SWRgVP#}*H zv*0?q$2PNMgf#>He$aQeic|9`7lM=pG7+B~yny4Dx>$#GGuybdku@7IAaqM`4a1Z~ zOhC~q6Lx^`g3+p*zFEUI@B)1&%u@@g9N_31*8*Uh?!7KB%GczGqFeiD(dn$>A0_OO zS6SGY6rIv(f%BzlYKm@&658Nm5=a<}aqo!m5<-xBNL3MclT5go@~IjHJZcrpR1zRW zH13Wegf!u{(_lGBL5zS2r(5WM&Cbr}a!E)Ep0W(;OF{5k)+s&S@rTnBhUXa*c9}4W zZI^Q8HiQrWtfJ(7P!!w*%0c+%l06KI^W^9`BU;yTls?*swPY4zZL)?QH2U?ZG~*~sBKOz5@jC!Ojt73U3GjTUciK-KCmvd?c-UTi-HNJd`>DzXi^H*lkR4ElpgyE2q zrJLGV+GaR%nw~i%6{&Ayvdfm4>fY zv}RaMhh`UP*tDCLLElZzD?uI$%fFZ+NJRT8RCoehz42JpBhX{&quSVxTr`%R-pIfi zo7pHtb6R@U8PiD4nJVf1SbESzbKhfIfqYOA|FjhBAc#DVP*rBr>WHu zgE7uE;(^fLxi+(KlQgiIe<3Go2U0_06Hp6#ho4I-^399eg@Cg9m>syC!}Xsp@cDzq zw@ge`EHj^-ZoT=Y&6ddp)arD=CUO~6P1xs-)!R6Uuid-~R+`Y=s?4fPy-q_TA@!aFvz?^N93o`NBO zNRzP`P1?)!bmm7TmLZ}<1|dUb1KmlV+BZXhQ7$&3&a79)AO}Lej8*DHm%&AVIRYK| zBl=hU0oQm)u5dU#*Raem!MGyA@PV-*22Pl%G}K!ICy~5pxtwaW8}(wh;xnaE&u$e* z*M^!FjHj{MrRQC!YQxt|OdiQ~PacT&C)$=#PgHjmGly1h8Z9o*j6!K(6`Ep0i*5cl zeD)$)AnbCvf7^Tjp|vJ=j;qhT#LdDdMwW!N_F|uekV&D&2P!#Ww*kSDct^@1O*&TB zst?4ye5K6Inp(eLalAcCO<*fQ#}nJ0EuGpi;?~{j@b{l%*T%@I+IIdLEQOuPx)9&g zlWX?*(~B3BWBYlU_8Qp{#=oE1t`A;-2LSv3nc85gy~%KhJsDcD zp(!b1a}Y}-x+qbL?WJT*1+>U04RMKcE-4WS?}>B!=2xG(;n7q74vg(?FEOkBXpq&w;beq@g$ zeG)*Je|F>yLXykrUUJ(jqKd3R!1($*^?CgI>){0ac;riD&;Ba%KO+AY`57^*hMGl+ z9Ht8WCeH3X>i5(isXtTS;T+}GH6?T^IROza+`kZn8cGQ{b#9Y%<}+m#CcdO%*@wPm zfDUvnhtzW|;>`du*1~Q)UBRK{{aI3Cloj>FbjK@@*@cT3s#x?iSI*}10;xWd;|a+M zNR>e5q9}O|2&NqHfK)b#Hz5JuF57?`V((B z#ci1BliF+bSNS$%Z2AOyL!%$p2qn(<;agC8(e7G3BsraU=V$#D#St+lICs9DEEna? zOeF=qsIvptplb`!5_Nf$w*WTeBl~RN)${@8gypqxOHNs?4PKx4aEtg}TtyZVjbI7K z$3u3YP$a4bcN?F?cS34$$%&WYOM~($PmfQQDKi8b zR&>a=U>}MWYfx{HOXST&wR2O=&ZZs1)db)GU3C%yB`A=${-!b?qta1P$OMhHWEC+U z^jjUn8%0|qs1tUJmfPt?+MyLn5K^}NiA6g%4q>+4ND&Q*V_6F`w^`X&-%Z(pkX_)M z**QTnm2)OQdoln13XPhtVo;bcOzV0)mUe-%T4g7xW#So#jZ9meLXBgRw0`3wFm;cn z6~c35XY^m9STHJ%eM`AS1|L>jEqt8>T|g-EK^386(Ab8VO$I}buV?~F(GT`9Hiedl z&K|*{RK@-Wvyx=)x82xh2uMR9lQp!wd(tx%mQASgqk5_i_cFeRX;ww&*Z|VRG1DP) zOQ4MsL(plHvUGAy$}T?7iD9r~YV*La(&2$qqka=J8LGQSf+ejobvgb-hpwM|2aQ8C zU^;4+V93P4aN>9_2|#;g+`o8IqmsH0>ue zSiB(!)x?*0Fxv7##>o{)Kmo>5vq zF-zpJR z2Z?0dJ_0tBW0_hs#I0dsW>-M{0_sYs%v2U8Ii>r+?6^{lOlZK9WRX);@zumSHQYSh zim+-+g8u+V)^aT~C8+0HD(Lc#aQ zM}2E(SP8*|1&ud8uySfKZnMStb7ZRL6*0FnKREMI#?HscSGK4j63)79x+n>$5;GD6 zfMWoBVpd~d^^j!DVgy{U2A`7%Qcf_BZV5nDB&U@0+7J*h-KE*>lQigZC|)zX>IFGGM=DGjAI#7N|`IF#g{8%?YXVVk%M~= zd)=u9b{$XwV%NmJ%Zrpa>VcNXyYdZ&5|v{CS&M$C{;Ol61e5x*4eVFlx96 zgJN@`Q5&SVgRnS68XDFHxF0r?UUfej#T{zF=H0r!-YZw*X$(Uv8*jQJl~$=J6pny# ziy5W>jr6X6&1*_jVKo}{O0j|%0ecq(s@ZBxOBYnb2BMHGd1F6s(^t-B$p5os=l}EH`CLfO);s>QUq50K$NvJp!pFcoPQqSxRakR!uduF*Hbb>oD2iEBpBR~# zm7$I&lZgtW;srGYv}m!%QuK-pL{!aUzsFA03Q^lIFg_WiV6V}SVtB3*sgzO49Yc|! zBkj46{T8dazp*-V;ZzUa6*aT6drl4McD3M_N7M@-M4Bq36VY6e`!unRhq(2BCG^R3 zv`0V-Yfzgku@=%^b+=ooxCwJz4&=*GMks~*oE=*^&pGkj^KAQKL)8-p5{*`&&|2=T zY%+iKSPY{^8*A%9ui}*|Kbq<9Stx3WSTP$*q^D;Oj1Ipzk)BD!$5+O?`I)6X3uRUo zpa!e*>({j-kc8ovVr}Z2CEh^ z&w)D%G*%Crvw+PQ5R&f5uL@J}yh zN#~P_Z$UfF2Sf8PJ%>jFmI6Qt{g(adIYKQSOr@mXrx%IV&n--^shMy+U72U{-kdix z-LZo{MIipV@)vqD-5HKJ&GV`@voNKy>sl|Z%mvm{!bChfWTfu%Uj0NFR~t`?$mjp} zP)SN%i#|FjFAmGYN!S3c<$3GAi!VMLZA~_(u9C%5t&cx<Ap^TVFJGw>KD z<%@(*IR5zRb>t;a?PpLO6d(;y9pY87N{C>?1@NN&+_Mym&Hcyx9?8W4Bn=!#9_@n{ zqcQ|yRFg^oQIrjI{1L1`F$wT#!e@p=ow(X9P zkT5@bl9}mGQ)Vs%P%N8WxUifYFBH}H(uvAcrBv$mihgZ%w6hc~6}G;4XtdoOF8FZm zSkV4B(TPI^_3S?z%b|R{UMrqH#6E{I4u$)N>>k~OKRmWN**mwv+paPY@at zV_HkjKXg|jAvzK^PI8r>As!HuJRS8T*`Dpv>%z?rdR}A$^_h*w->NqLqOpF>HR}zv zafYzPnX9fk6X}3z+PW%oX6Q_$-&YZya-pC8Bssb#LqzRnvl3W9~J#c!DG6QMH57unXmSY22} zk}BYVun)S{q$4amEEJJQ9z~*ugysn_Qod4(o3DO2lX*BZd^D3eO7%-F4d++1mQEeK z`CuxIn{R9B98km<#V6^p17n*f;C0GG&98zY1Lu(hTr$~6p=YBV+)jYp|kjE_5+newnVciwq(-f(#)lO87v81Kcig>q%8FuH&A%8kNQ zrCdtKk*T1l=uvxG;zREI6!@ptuFx;gY@L@`wIq50JljheW_b|G6J*&vKz?Ls&5M2anv&v+}^Z`;JR0 zwVNJ$?50}fl20zvgut0BKlu1mDmwY_+xjo~d<|ed{092- zY<=~hikj&tSUxUq`y1c*yR(1liEn)48#(?%{YIyG?nOoHN9L^vILu~Vg%Knv1e2w7 z-y0tv9n)(=wPK^9kB_UxYff*X706 zLA_+;#Zwu%2E-n>KQqZEMmlqn5~rEVLeMuMk=zdZwHNH3I(eJF>&W=#UCrBWzIp2> z2UC1(>fqbnx-qm^+tojP%Pog{BT}|F()-F=-->2FbLNZcAHdNYU?uBeeZJU&5#b4^ zeP)YCp{-lfNN$SXA)QxPy8X(R_Op&o0D}5#0vU4hkAb>kB{Fl9*<8y^on9}?PF_&()hT&cKP*}ty|;c z--xxBLE47lTg;x%8+IvQp{8Pp`G$2aIl%W~7tZ7JTHUU3zz3xCg<7o?f7`^qPBuHV zf8uTNhG_+XWlaRi-n-XUdn-3p)bgcW)xB(mm!BEVC>%Qr7go3E>uM%_z+b#(0jvry zq#P{;$2Gu`@IHK=G4=bAmB_Oqk-WfOk#ma(MshO@FW!?^W-75n-iP{BtYKF0mW^1! zYI4-FG}OhIUGQ4YBT+>%%8&_zoWyl#9LY?sssHZ9)_~8#jWea|AJ_Dc>GC)07bwW6 zmfXU$>wcU+&-?fT)8p4!0d8!O;@4=Zm@Buu@!`uYT`v$;#|LJ+K$%KNFXPj2+y{$0c$8QEoN6>0 zsdg6JpDxDG*2f_M#6a*=@X~jkN(?3N0Td% zxFB$Q@LbIW!ZCqAydCmdKxMNuEM}Fwet6FawW6-XdkG1N7}!J_iO>;k3ldgAWqxFuqpm2MA$H!|YVXkeHGlnxv5 zP=bg^!v>;H!fPn0YHizkAShbCJ8I~qn_=D0Xta-vfbkRg@LFmgr2FrM;80G$2z5u~ z-pETLFONJL`7f}wzB}>(uF$foN$INrro=IOWP1872^d1oN>ezG^ezSuvsU8o48*)4 z!b^((e&Lz^#Q(o62XztRG!|!%YE~{AxE>{?WGS;=ElcJOQuZ`3`b5el*XZU-peWLA z+t(8%FN-0ob~HBsWV126dE&(8@YdgEXqox%GpC}zP)PC9>+0;91vD%l3U5B=H`mo zB{WcU>*2=N{&~IjH<{>UA|6f6_h-~0XKMMp1Jlh~t@(F}VyT$Esg|r|t&(*)u#})e z8?st)oOv!7(xyCqi^}`EHdP)ujWYwgkbEoQwa#b%~tim9y=SLc!>w1K|(@ai% z^29|;noHzhd`CPRFIZL<)Ach^6cRw@OuiVnmQ^7sb9odXN98)do~4Y$W@VRQMyKGIJAJyFx6jhHqpI%6(@eNYLtBEU@=Q~-DJ=~A;>M1wr!_j;sK>|8W zB#R`FOK24sgnN*}Mh^5%m!U^5Ez|gBj-fHI75)hS=A&FMtkLjlg+n4wd0Eo{qNud}y=|!pkqY#M!wvb^F1?cTBBs+`l<7w4-GD zsMTIN31wjSs%Uq(xWfg%I&!L#^J;F+;e#N#*25T`uMJ)gMbu`Te>^hpIFtU)TT+zZ7c) z(cbF$oDA|!IHea1av>ti)NVgoi+Onp^M$)B=7(>6xXO4N_b0qkR=Mwjo={7V zFO4i^v+E6BY<=h0OO79Z$+1uP9!=H?g=mw2v`0aP7pq2nKUR$?&o`1e<#LFaGntvr zT%(rgIIzU4^(L>rcucXs@JYp(+kZ{1&~G5c4}qC}S>!ih_4_k&aA)ww@SnugBC6I3 zSzrQ0HbZ=xW(GLR;krSSi4bACt@}7*IQV8PLBSBub3-Q#*j%3Z6DFes#fE_vvOrjU zG$25}mQON|W6Evsi}{uhBnXY)OT2|Ew6@COOF!^tMjzrl2y?h)wI`G94y zf9Jx!%)PF;{e61C4}%($K%XWL$MIQ!jRPL^Ypsh@q+#NdF`OR~KL=awrIFrz4NgE! zE5@pUUZ#VJUMW_O&<8BHUd|799cKs{0l-+oBw{`uFDewnAJq309JA$j+R+TLd-@t= zqx)Q2FQ=Tjw~rR{l(PYtDev6P%+X7&-fI(Xrd24paazt~QrV{KW@9A}oTEeEI>#O+ zX9e|lE`|>|@&mmIaZ4<1rhPc%_?pz34RtE&DkD$5FRTYS6%;~v3>lA@Y5iAK&AC;D zVy93YWR-a{4b!}NXOAS5l$X=>*3?vo1Ht8hHV@dTI!>3qxXMEu*qtUoolBJj^z>3Fm!kp7DIAFn0Vj7^@fuW{DN`>d;x7a zzB>IbIi3d-wIohVT9A^s6FG9`U(b9OJMamz%EQ>o)+R6?!NDaQxC60a6$1r#ij8JN^9k;4e6_c)`ll&aHPf zvZTjjv6o-V2BrGsVc1#4ggSq5k$zgSWP2%PL<49OE_-F2#!soOcU9F}<3)dUM}PFT zSUX_>SRykLZkwG!y~4Tovon7|Z@PED>=ocq4@=j1rV8@8+tWdJbQUQx@7V#?p&7tv zjDKzPdoH4H5wb${M!GQmk9M?e-l7wo&CsUBiGTWoSR^tV0V$I#+u| zVU`2F4sQvbiPi40)c{u^B3^wA1YeMU;#LZh7906nOT)v%4|0lhj5J@7tLHlTibeO1 zp~CBKg~j5-ns0E&C_yla-P)(j^#^>E4)cjFhLhwxC+3pfdWNTs=Ee zEiU$p)znDb!K5N8Z`6G@d(?_iFhNBoPM(~vj%+*|N3VzMU__A4&A9}a?^r7f=%y+} zO$(AD+I~&zjKx4*E4_6FEpcX5Cbr~Ne=Y8K)emexvq(L0b^}GvocG2us_HrMU#ohV z?dK&H@m-$r$8G80eb)as7u&6YOCf~=_#9H6hRzv8!xqoJrBs_4!0>r`fyih^yzC{| ziNW(AV4&V%#9oAFsyjw(HAAW_nt}^U=f=SNp$d?K^hj7%pv>^!x5N$^N$Vj8Jx7Y_F?gzF5=gKAM z!VJ!KVyB^nLnP1HS8C%)Xb=|U;Uh?*D1ZZ_GNe)a61xv2JZO$|rs;pSamlS)w4Drn>@!co@%$uK|1x;0!`m5Sj965xT(<~F9jtKoh&o7oeNOmAE;jJ6cgAya0gw*NjrRsa*lau59*Y9jz zur`b~C}a~xu9PU$%~~#(7+ycvTzlOk*Zj(_eCFlzbkS7hqL=Z;#?>8;r&?`qwb`b3 zkMF!@@K#+~IehWq%1|NIsL=5MTAFe@1Bh#1y||psLIPB+rwUsizTyQ}T(NUuWW0|Q zPbKK@bC>Jnb6KB5HN)o=3_&^n>4hR2&Yh3M$Ii}2=%27k2))pMYfggH1Yx&l)f-M7 z$a~J?@mBox$1l6|U_O_+b9-)V{m z@wx&caf4n|V@RVpJnt2$8IraMrZk?C925^{pKW>H_oc}JJr!6g&pZ$Tr0Q2_Yy(>s zs1u6^E<6*NZ1s;fvh)_II!lFExoQN~lCS5hp1)dA#j(0sZjMCTWB#P`qWy*b#8_o6 zzjNVmcK7BnK%BjCv(lsEM^jLDdKtC$)d!28ea?%Fg=D|gTOk;J)bx$e40u4{bqSqCq;(#vI)IZC4z(v;gTDISsqq4>f2U@q4*q3VXEf&<92-S64z zGjJbF5Snl@B_nf!%v&m!E}5BmgdchX%>G1ef!D+ zR9$uu;J-bUHdEEInLZB6D7B+;oUAj~(>U`j^=WJv>9H5-10M)US{OWnfa}ssVE}ZY ztO}t0kAsZ^bw5m* ze@H=z_GcDtYpgff_SI4QQY&rmv1~Y$-#2f|;#a6K%hpva?pv1Cv#fWq+uLn3 zoZDx@yoeoXkn3CHK1fD{QB2^u&1J`>*GOFjivjkbRDD^Zo1xt1e8tN|bA;MjLM*&i zuNsD`=Bu`Ck))xM8z5|JCx0?a6tF`{y`ncEj>zLs(XFeP1zDVT1 zat{bv3RD(kI`%G{6d}S9>JIQyn%G_N&qxrMN3pM9bCHzejI0w3%OV}91||vCiIiPX zwz0qv6a$&@B_mHZmHk4rCBb2)d#GkbLx!7(Q6qWIZVQK57GVGonBwE)S+e14#G2;h zI_9NdWST`_QlU5y0jX=V^nwE(NO=Ks2PX_{75#^$LQ6$+u3aSbOH~xGIs7ZG1wIy4 z5F{+C;*V)mSl|*cQbVvNZXRpKI#sWlCPJsHtf%3_fv_h)joA}+>JwKGClK?IKr2I# zy#@Fvhhan0b&G2YR%M45D`;A6JejFxQt07uI|a`MxmEJ205UNg^`Bgr%we$;0i#e1 z7$7F3)Uyje3eYkwRw~9C8Qn>$7$i3ZEjk(}%^RbFN?>~OuLCFpHO6@i0ga<0ji9R& zp!_lR4E7V)=g?R*KLMMHd{3P&(4@h|B)QsR{j+6 z_Y^%dRIv&3D^CRJ_-<4n(Bzwjz=pJE8(VZ7q_tP1B_(py3d@xfcw6 zrxY*xXl)v~pv%=$MzOba7*{=~dAqM(qAoc`@2?|#mWKdZm!HMyC|&yC%d z^o?$n`WZL?jY?(w!adzW(%4YF@=!%PV5TPL_5``b_zQEl-QPWMeK&XM3zn{pWjl#t zSjHtiC(lIo5)-->tD}Z4z7U4KYl(y2LtmEn5YzuO^MFxJj1%ucjO?1~ctNP1iEnn> zjQu{dR8aq61P^mB4El<57aFc4`UKS=rhJHGpg}IYOJKP0BzYlp6~A;5%UO-f7^aHZ zmCOnly>JqDTn54{Cqap0DwB$)^?TE~nwuGFMSIB%WrtG6w}aP|?x&NEliWd<@ffs} zkQhYQqw`Tm2O6dW6^{0zD^k+LxM%e7qdw7CteXcG#@d+~YrK`2sZ8%Bq;cTt1Z{6K1k+ObX|&N9;xdRjC%m9OyC+(`X*QmSeirY<{{D?w z%~P)i({T0H@3%4+Ovi8eEk?ZghKA|czh-97wMzzU55M3U=a6air3H9?4#)FO)y}KO z32-`F56R=}@7!;h7v6lKdBWaf^LK&<#b?*47dhV6>ylS2&F|YcPbi5Eyroco3(w!D zm^)$<3kIbJyILSY*d8{n`qX#6dGuuupSq%T^zvf@KRJE+>l4pCc<_zO3;x0s3LmDT zZ606A7fQV4=WS|XqT7X!b3Uks0ebBE$gPoQOXjsV*OR6{j9COJl}-J@BcrOn{SUtn zUuN)Lo*}&;9nxb6+WgD2a3Sj5UemJm?WJb;uJ`no+aKZo`@Pd&|NKoid5-%E%eYRr z?$ERwZ#wr!r7ZmxrC)b;*&oCF*>Q43-f#V;kyMvN%lW&u9noq>zon-PrGX2hm|h8AduWTKLF%^OD~a zBZg9Lg8SH!(iv9jxJA!N{0LW_%46pM6ECkZ^-6 z0ra}CfKe+($8dTKJla{QQ>kn=bLp=TO6!*0>{rq;2xPK_ev4vYiV^9~fer&;46Z*O zXIWH=0^t473J`5-_SU0WH&@H)8hhq44(yG|Ov~dVV|)^y3%rOKI*7qPN&ZA|RWBw2 z|LVw_BkziQkYFh|5}Zi642v@~rniIMfB0|PTsFEP|FVBTiv$f&C%^O`bu%0E?fftM zN8yN>6JUo8vaZ|qnpBVmVO(Va8o6g)pSwIjtiv^cn3j%XWB}NVbC4Hc3)+}WgO@XH zv8elk<>RBsl7~rPf(5W8L9C4FBpwMvLx5Y%57dTNG;4XqiuG4w$(hBZGd$^J z$}NPuo2BS4oAHv_bgP_BPYyfD#hGMmWhrK@E?e5tlA1ek(_=T^{Mbdu?mu?y{ugm4 zsnd*)Y(LY0Cw%`UgmKKIWvxb4C>+PODHano1XA|^4xVnS)MPkuaEo;FV&GCFjzyv3 z^a#++H6xKUZ>8wUCZGhNGNxMY9khaw8rUK{&M-^V%ei!`kxq1m5;((Fw+oo7XU5x| zgi~*&VQbELZs0+l1CoTyzUyY<*=(%{sYEXoUz(3ccdgsT{(ZVS?>uAQe#2hh6^+j? zC6Ya`XT@5UvTzqq4Je@FH05+}`g=(^9e+M2!R)|XG~$_aUW<9Oy*zPTPu=PI_J(aj zEeWTy;id$N2?L)-R}Ypj2{SMP8ri5p{g38XXtO64B8F26b4d|br^+W|rJ~JKxxBqe zT9_7 zvQF!bhV5%X(6dBl6%jWag4s2blCuSh$v9gk@8AW;^sFxK7ncZE3(+iBU_0B#6T%ny zERuQPbygLz73n4*3)H8Yl>}%J-_6sF(QX~+a7id=p+1{Hu-q$uC~yU=ptR}Q4MXku z%Nm7vP_b+0j~m&Ek?HgZws<@{TqUHur@leYq--jm%_SeE)iS74V=S+RJ0O0H(JAbh zDa+nFZ&}M1B;pk(yPhw&xXeFz)pfL{#t-#WNM;`J>erUlnxA`4n{& zqy*^J1qwLEOtNe&vU$rd)f=rwEqVKNxukse#~0pSu<0M-V$hlkGM^l z0pW&Uy4o?w@#O1Tp1C+_Ft*w)sKMDu=B6BF?v`|5m?0Y7vYO(J;)W5Ppg4Mna(kp| zGlrA5Bq}zpZf0X+DdGRzhN^>Z=U#^!_VMx9@(;od;~pD^4qV5~*vQ!q?4G99jLQ+( zFqVy-Oh@fIflmQ56H1VSwBykalBSfBe&bGv&>@Fli`@Tm?GuC5thavY-f~Tir(<~; zDtM0IQxP{N_dST^#1r`wL^n4Sg~6 zHQU(z{CjWj4PV$Fs++SZ-TNvLIGgdufYmK804ahx!VAYUAt=gl%~FU5)j&LD)pAzu z)Y+PjiuZxd6VsCqUG3*DZL7V#;f2LRIXyLPHoE<1Q^E){3LbOZ(Bp0n)_IOEHGPyU zr*ob!%o+GIww_|Cs{LH$(1Ta++L)QVde>OzNFFSCnD6+@P_9prV|W0mM`+O|=V|xS zs*T4ouH5sd8P6iX9ddY*5`v9VXeOk-%RfV26}>`WXyO&pVLCHor&mIS909+sL7 z^<0pjy|>Y{61PQ*@SDrc*ERi>wI?rAD(}J}nll zBz?VF)%|2SYQy0}5jdX~jg@Jj0!^PEehDuT+vI~`ESqX(o#7OgRwg|k3|sPX`Ppo` z-OW2AX|FfrWahn|H9tu~wo9QlRgEavFkEs zZ@T5e!p#lIyM%rv7!~^Xxc$Vi?Af_~D7AwA6f+xrGkEWYSzs~q-W)crEAHKKtNWMT zA*J^0uCE^svTi<4x%~-DCr!TZWSo~vpRnGe$U)$8Jeq)h3S-=cHLGX zPP6q7go-}-`!Tiu?++~IYK?X_Q&20#i8=-o!JB`0>>850j7i>XeZ6+$&Kf)P?fEzTk#c8FEy#DsfCR`YR zVzc|*kxZ)rYzb8}W9-VVVj`B|ZL@Q&BliuboZ7}|6|cnVS_-QZ@Cw&JBFs-lw(vWW z!s*_Zey!Zh6_sr-WRcJkNqO@X8z&E5wsE9i?am&pKJ@voc5ga#=~m?P%2`?g8MhtF zn&cQnwt@wHE^y~`|A9M~j+~f({>Ho2==Z3gd*?gx5`P+5C8o58Y}ORE+iofVZ=y2| z%*5i|^rc#Ux{YHD(whU5rTo|}#* z)1VA6Rx5X$?^d&&>S(Rj_VRh}v|nEAR*OcNSx4_pl?u`jrrOCnC>1eiUNTo7QtkRe zte{%(D9}YC*VNo}5u7g6F^F8UBd|hL@;fj)2)(v5Rkt#<`QAfb$9sR^b|JVfcquPY z^sfzy0S$;~>It&pKtXx2N1yYYHTT9eqhob0nAFXDdSb5Q}p zC6Tx@EJNoS70p>jE+baPG`2B5V^_B6wpkF`6>xcoaRDu(gwhDBG2AheWoG-ZL=br! zTv0X>K((aZ1G!giU#x+?nyeNIhr9q1pxk$m_6^Y@>M5_XwUMN(CKZdXxhflniVJ>} ztm&gv={7;s)d9z3y{hQV`PG!4JP!VdHmWWyN=n@rP>`5+W4@q@`6114@b>HswKJ;U zKhE5zV}kYP=Ts}cLeap2y8A0)el|hHvjNLX&K*vJk<7(5y~4v&37W?^LhbBiK^2v0 z^pwq9fAPiFTTTX3AGuJZ#_2X}@C?^=GD6IWu~{L6FIS3d!)SEHR7OGL)K2O$dL^!* zXN9$n2tUzK)-C>F3+{jn-B%SgSjHpuc>Ef=C=#+|I6cIoM^Zd_QMx^D&}+AL3Q?y6opkxhGBpHutv*&XHc`jaP8xdXlZ z&%Z%1muG&;+SP$LdMUMz?~i;w^7Y71$)L22!ck|* zGqnT1q;$aQQwAMI^5jkrggz%7Rt+pLQ&FA>%YHcI!sMjnVEe>!Bql5IStLK3cf$}Z zHG&e`8GJr@aw%TmQ5NvEupjw0Y{R0#RxD(RpgwrAAw_PrZK)k147bpqUg+E5uIHZ1 zFYe36HiQ=fN}i@fy)9knrU`IJnCjfmAUqfJd%YT&(i7?9_@!pOx*pkPKleG5YFR6Z}c&o2% zAK@UipvV#qjpI#Kuu_}ha?7>g2@`HR0Rc{Y)F%u`uRy->G5EnUK7 z0*gu)xT)wG*+3M2xO7qydr|EL$8UUVHgoxrlvysB$&RJEx)2~BwY0b79 zL`@3&V>Gbp`QvR4>!joQi@Oky0X9gF1i?501{Y*{$WC}=&J7BuL}HC|qH<+CpC5JL zK8cO4ClmQPXJ=NynC+eb+k3>ciWP$1R&OzCdw-uy7kpETKb+4eQONp%yz5a%;Jcoe zcj=_4d9J^YsGUi=a#ucY>cq9K4J!>UwC9HC%x zG`zr}87hpD)43cFl)wuZLS3K~+ z*it656cgx&U<#?CL@h4q*ghwLnT-<&DlArgC)}y)~F*vXt)A}vzMo%Dh5P&K*;wwYG`Tu zUSE!>{qK%Rn2Wg^M3!?2}4M-h#7s$AAx(x$|zXx<0J2E?taKWmJzVvF(^*9Rdt z8cPSMN?8c-gniM7ZM_9oKkkfyp^=>!r?OLcq=P>ki;-k(!V1rXMSK#bk5TX*q$D zuVuC2%>C92_AUUyVN9SPq8Ihm<^uly_OdM%3H>X8Z!vq!{ z9P?@xNh$Icp8ejm#g*sSn4_!?bsg`;5t?1oC3!PL>@6Sq?tE!A`L83`n9_FIOq;b% z8%m-AU0f}Xb_jx=NMn*>Z+lSYIyCarwF<;j|VrW$+-Y(kE&ymfb7`j!ClC^ukA zYQ4^B`sye_Fgew2DXAhp~ve(@JFQL`5ZW85HA5!HwJYV^UwLo3lwD?8ba;XYua4)#h!T=RL!c!AO0b`!Infm{R8AT_>y)?kBQ>=3t<6o2Lmg7&D6)B&?;sr{T=MeM__ZI8d={Y{%KV^b zW1b6;L%Iq|&RxJAU~1vG)j}HuJee2^1i1*DG6^&p^m$s+3IU&FFh!^%k-b*wG<~{MPbKdo~_u%>Iqx8gsUg!n$b5cGcDk%_eSZb8|Q2gR=*gXKibG=*L zc{jVZw&wP(Udv8QWZm@d=SFULc|2FB6mpat!ed=YO4Dh2635~`O`W0;#K!!1%(;Sw z4|e=;ycj3LjbiUIsW9C6O+>pav8-ym5IYPq)s7$gC#81nVm87wTFB+r*T_VuK;PI< z>Yt8%k@#26d*|rrLl0d)w)zlN&8JV_bI(1=WGDO%Zcr%zIT5%>EZNDsbS?x7<>qsP z7kd;ucQy=M#33}Q(@eeK*wVxh_B$FPFwAV|&}cqsI|ZO$vIXr)xA(zbF$lgh(qZS; zN+S=vE#kJTc`ytDy_3B$Kq*w*K4e|Z ztpVqfMsB#6$YJ&+^diMbj_O!8aQQAC$bhwVl7^iX`ZU!5(91S>q?}TqotDWTFVY5# z;5~30dUHazUCB6}q5I3pyk+L{l1hdN+r|^qV0{w-m!u39_B-f9Eh#F<7+D%el9!WI zm1a3*KM^fvs3C;`5&INULXdvKRLrj8vZRS7%3(CLy5#imY3Y-NtA{|8GJI}%mO%8h z11O$RfirB^3_qZHE(IN13hq*>rx3$JfG%1sf%0hld%zAw%699-|PE{vK6hY>UZGO58A(b&-CY5E&wXFhuG& z3lJG;_#Km_LLBO;XgQfG6nwsLOlRUibSNc}u>C>xB6vYJ)lh`)5W*Zr8m=r#_9A8! zNZ1X*f2b0fkYIj-^QP8iA&-L8wdJX}WFKw5haIg?>~x~ZVMz60XSGx((9NXhVg*Wi z$+AeUBxhif&^vI43*RC34pD?U@)^^I_Kna)YexR|enyX1f|8XV*I!Sb9!G&6qLEp= z@bF~|{RKud;l(o4g%ESq!9abQnCqm>lkI3JV+QP-7$yr@F)l-^(VrD=K1@f9hPh;dro{3ixADE2z-;`20Kh{Ij(Qk$yWn7i4*me?;o$?ChzzH_g3q_9bfbf~^bGN96xs{w1s> zk6eJ9A7Ni%5fc2>I@LXIiM)e|RisayD8{wb>koy-a(EI*dBIe{!wM$Fm)>3E!DY%L z_JO!iO2!nnX~PoS|-aWYJacqC0Cbv%LJ!@-eJ#aIyf>>7(4pi488AHrDX-`CH{3 zLl-3NIqPHXU8$1m*UG>`n?Bfq_;R-7NS2($$w+>qi*z3x?I+bqU_-h5ck}s^3+=0W- zePx?=3uI#GBgvj&(1QDI&ygblF(Jc<#XT%{?(Trek!&i$oU!$;Rx6eS9pHJThmu7< zHQv2ueCv!~@-r2;7?gr6FPr6pM`^cqiMm2>@Uoh3xSe{fb##)bwsTi;s9nmW)p$<> zUk5Cdg4O8V<7%qgU5^3(XjujC(b0Tt#LnFG=9QuN&Rq2?1l^EMtrz3GUp}j?ve7) zv6WbAdEb!p!dFx}sa&Uu-+y=U*cBVv>P0VkVm@%v6&J?N`QFi`@tu2%d55f!6H(gT z>V9O}mBgw(68W-7h$DVmAPCx^2AR(oee*q+MykSE8m_ct zmeT;R%_a-e5pZ0V=zB$a$Rbg~v1QV+!X>XHq=XkQ8=>u>Z{E|)(uMfUvIVc4J zgR@!a&V-8xX$8V#@C;&VP%yBf0hio9u@<3+RTV7b)P(^yIUhO9LRU&T0Ttx&9xM9J z%|>Gwa+gB2eN9ucTqwV>9i48uB+6bvZB!iTwJs|gk6T*2P7@hQP^zsm63L(xR>vsi zC2BG2!`kgO0WL<1g}nL_m7ws-qmI~63MEme;~7MjMTLt8j3!RI5Yx3Simll=wU9S+C`ilxfR3JIT2ME)ogw zx>qfFRTJV?bv{n6seKi?H(v1ZDQ#qQl=DhywInU$3nC7O`4Vv-3;S6@4n@3Mx-;an zsT{_UoR4b)OTyAHxi}o1Ftnjg>v3QPKvE4T+{9$TNK*IbqFReFiY&$wsZsBYVZ$l& z^;pC&jcK?D+H2g#2Ia3s}r(C7n@kVX)~5_@C*SLxopV zwa(3MeuaBKB(VyNIXrRfWE`bDMz_~vO$I% zX~(Qqz^qh@dm3%uCrpzg^U$bI4aw(D;`7HrwK5kNS8fjHj6n{aIKz)PNt*c1kB6A0 z1aG`VHXSdKIZEjnDwY|zq-dZ=#Lj-@If>SB`U4#+5a>u6GGQrN;49JgXZDm>J)GU8 z5K5@@DqRMRXHl|aVW6`@J7!wIKKcGSbAu@yLSL~-;E~X6Yz~ga1WjD}yUb<|vr6{c zEM$BrzQvE=3(FLNF5>U$AI;``Gs`K080Ffl>0&~{{J1OWB9X;t6~mqnVgkxMT%)| zd3YKkzlhok!mRj$OLUs_xq6`3ZWp#b&SSCssf(Yzwt4T-Cr_%!+r_O<$Qrf3*uLVS zhwiy&>xU0McsIuf3eNGLWDY-5?fV}-LwKHrPD!|_>$ik3MtE&MFb!}dCfJX zvAt(T!)VkhCHL2=cUMXl4VTY-^W(+E?>)0&#e8K7Xmr z2pPC(Pn`vb0Ab?RhPrXY<0j}gqDiB^E=rQ*k0$O_j zMHh3Dg2|SnqtlTZ@ckkiLdHZ>%!M{;e!yUXj3efX8!oCIWy(fgn7hIu$YV=}rsUZV zkeC56)k%r?d-=0n-G8c&==`0m5G{?XLXh&{IMHO87LM5`NFtjNz z%vopy!i5hrVHw~TCr-OwI{}eJro~`)om8~oRO>-Hu1%1-vGurKOx&`5<+^(eV!!HG zKK6e4IaRWqxz4|G@N5Bl3^)&d+AX{i#_o04-Cjs#LL@S%2bibhr*xJMP3<+qUZfV% zfPBJUhV#k>szXo&njTX1AZ}>UQ=lw^p;C+}g><$!P%%2&WXphgV06FDPm&K}Jp`b~IyaIlzb#O;)ovjQR` zN3VRrl~=yt)`>$`9h#UpG#QK!HT>kSnM8Xky}A14Y|o9 zj}9e_H9(w;kLdtGH1wHA<6a=dvSjecS_4h+ zk7qum9zhr6!2ax{0_~QzdUV zDQv0eHO!{x6@5Oo+}r=~?Ce;n*i0C9qNbT6Uz#LW@?BH&=NFyy-hD%w`r|rKckNA= z%zML+P;zzb;1E&^H5`j?{)!(13u446E90;UtC-H0@*L%Z4qC z06hjJL0KZP8l=)hco6mi97IA%yI0&=?R3y0`=vX!SsY4Ly>6xFjwctaF=zSzVeZYt zB)Q5n--KXhD)efY?`I1lV9~ z0p87Gn`>jhT>F9=4V`b^5K1q9o=#pM|wlR(wVmvfN#Tzh0^<(4(r=+xX&*ar1bJ1Q48 zcWQG!l55-93d&vh)%L8qi#cjv1K()Wo87Yyf70J9*RFi<#-8iUrk7Kp;ba=mx-OR; zyANjmS$D@@9h2zHU6JUPT+!rOVk@|CHOzj3X#nVl*6_lOx%>R#GyAvp?>9GUFLxgP z;8@##hyCD{t&Q1+dwKQXbToHx-$41eOqNNPpVRBXZ&gUgdupw-55KWw7KRJ8=9LfT zn@ztryef6e1J4+L7d@iJOnz2BpEYGKq|ZLpoePuI`N&5;GCA+FC+S<~=1$MfCOYk8 zo?7^+uy`-9@o;mgP8Ex!+{I4drW(iOM^O)aa#OjFlY44f6P}*$^pC6;ls|XNg&^B& zFBS)JNx#hZhu8dzVm)+r?b_;z_Tk0Ww6~nOz~1v8t4mGgmV7J9&)3t{H8AMie(^>_e-w?X}uWP8X_T(N%ZXw2VaqJADnr=Cj5_dYnu)nR@;zdMc!1;^zcM8H=w? zo{(g`I>PXk`8QeBvZAkKw0`R7(e+WsA#UN{v`XyAuXgs~GWKdukKWE!vc6@ZEBQCP z;e4E>kk)1A7tM6Roqxljpg8k={hZ%r8hTbgpLO5Jp%5Gd4ghkUsi85B@YQzLf*xEMlP3MpY!KrY{fRauveA9y#46jT6p-$0NnLk0t#mr69rylRpYzt{dk0O(-!=?@u|mW~^`?;n%tWZQ#;lw0!l=>GaiCP?cFx zuD!@{F13T2rP{^YNSHoy1Ckw}{OQ|L-?x|COw8a%-(_~^3&X@fpt&+8#mTee<+BDy zs=YAEJd9+Jw&B~@eQB+t&?Z43yk87`5f%3bMlRetENJK~rj(O4l+Z9j{1Uw2At1nN z4d*;9kL!+Cx;Z%pS0uq!BVZKOqqZI70(tmhL-&Pf9+J!Gtn2RIaNT2W=`P47-3*c;Z`ldVQ?1LiYdfM(>%0FIm|z z6t5-e7i0jy=m4cLJkG(>a^tF9ts-aIw)r~49syHO({CW?^tze!`22^@pZ{T3_G;Vw zsC;3B1*Lw>#Yv`*?!P0gP9OT2#24~(f2XHbut|RO-8Na!yEu?-@;#E}f|rvJyF^2N z;D+6|K`_P%XD5Fyo>xbd-tKQXxy<|z>*r==ZfUeMSDE~&U0d{S$5Xy|SH`;*I9W2b zQ~S_44fb3b6e!2}t6Q#S&mh*?hVzQNg$9 zyR2U0b<~W+%v&Axh23XTg8`)_iDcrTK{39m^PFC7%McXrOueO;)77}BwA5B{&{$Mc zhYF>cw%XATE$7_+c;}-Fwc0}U-%7b93qVtK=XPagj%>?vJwH1$JJXx%C<_a?DLy@V zAmts*x{gbBsiBt|)WQng_n$6J_Z+*8e? z%?<{3PzeKg@wks3d|SVPzsbU$x@GE3Q*WP|>g^h>f_=noE6!;2!%kq5Wg&PqAtk^i zgZV>L7nOW8<``li6K_mJxHd8)!axuUaUl6u!eoLr3MOZb)M(My@ZN})7LeK?k7Ace z3J_C5-kLn;(Y#Mn1881heGfE@q0q4S53(sp= zHs?}<`ha@rM%h2peabADc;S@bSMhpiw@7}nriH)U)nkw8 z_vx$Lr=LY9xrN!gi;yhhwE;G$gSo=|7o>a=a0ll?kgK6vo0>ozv3R*b1l;lC%l0se znDcGr7aDcgbRJ1ASDV`anHe4AMn_)I(su|xmt;NqSX?B?RF2N+kA$FK} zcm6ODB12s&!C$mkiJ+^>99u2T8)=d*^JH{mJ&HHCj~C4|{6=k|Ij+~!W_7XZQ>+=j z><-@*Ge#f5MfiGP~Z+TyH-H9WLG_M&7l`g zxBJgI_l6HVeA#PH?JOMG^ZTmhlj#Eu8DzrLWzvs6aLe_t{0qCB@tqX;&xY${(*dbs zQO5o1nTzhb#L>66)8{X~{Jyp84#fv@Uy^xIFO>QuyNg8Dy2lP4I&~wCC%6htD2W-E zM?;N#F|mQZk@%H0_Whg1t0UD7`h%{ZXntSH&B%pwWf0im1WdS}rf2qWQ8 zj61GtQ(0l-FABst*@r|@CcaO;7FwNnoy`e@H1XG(887nlVY94mk^?^ptC3g3(}tce zkO?AOVrDk--Cl)4*@oIE06d@w7vLH#=Ylf69a^MZKdF>>pOr1Q^;#|@5=XEMKc1!E zgCj#6ZVDN7Oa)0~UZj+1#(XpACPZ$k8dZ}D@RZ9;`EJWAEGS2mZjNL}8I#{3KjItM z*IoR^cj5_`89u;%MTE%xl!6s(9=t^|C~U>p5JC}1j8%&A;t&?s0t6=0MwLbtjrk{om?r* z-vzFhB30v1S2I6r>p6ISTeFq^Gvu;Glp?}M`JAWit-`M4x?q1@cn#t$X8+jz#Zh1l zp82~rj@ZMW@r#W;#&_`aZ_$b;*4a-y`PgGmKK2tIeZ?z2`ifV7_>o6G{D|@v(>t-1 z3&y>uTq81^(W{lHHwtsxCp`1y8C5-d>gfZjItLiFa?~3Kxvdi!>(p72XfGIY@2Ba< zo>cbH_A5TR^Pgz^kq^fgj`hq_XLlRh&j0AMojJ+bwsYaA8&)P~les2!)JZ7EN)gto z2u((~SfOUc1Gi6w+HJi_czJz%ORw9#VrHf|9NymT^{xnp!(c|K&Ck#M*dH$}{PB|; z8=bkic}<=?`NsrP~ z*Zr0b{@B7|JXFuMt%T%VdcRpRH zS!LTUTeU(*c~#l&6lyzP?=S9I_;z7o&!SZIc>IgXDNgXZPu<6d>7rInj-9*EbTfP5 zT)O?NbFD4xNw?~$LV?y^DBapa7yFmW={ki1=TCfxl2`jz>Y5~@aDHed;!sj0FUIu%KG(NsUK^{{tVVp5$S9d zGHEy$D1W2j%C_7z2#bOwx8V09zXSH8{cvupGy}#N<_fovF42sN;=^eZ)Y zcIEVfky}|97gf93Tp1qjY8mEi8J6F*6Uc@tn6zI)(|C#U4srrMtNbZJMC2hb=}o{{ zA~Z#P1UNqdav^C4-3cBFA04Ol^B?*Uu`{5sm8K=Jh~zTaAuw+ydqwgnt3K(*q$`3v!BxuyTaAcMF&^w{d;mZ4-%7+reg*G@sUd(3 zmKN|1*=gi`dBE|3P9`vN*v^tL>EvqyZNlM^d|%p;3>UsFIy{MJ9-whKf;VUj+9fd4 zJOg?{+BL0VGw>QbHTofVYB`C}*nn*zEt0R%ApqXlv$Ni$UqO)-&K6qYH%ZOm z4|scG&dHmW)sW_dQI_Upc#bC@B*Ww+(O^ZnS|DM%=Ie(kP(Ra79nX@u@UL?X$`u*q z@*t0Huavv}{Z=6~iI&wUxdN097G2$l0M*+~1AZOqyg+RQl*VCagM*f9SK`%Pb_(4G@adA9@sP2h53ik~~ zTqNjiK(QvHmHd;@lCDFT%U{M;rcMTAeR4B6gnO5aHfRU#ru<;|VC`@?vcyV<7zOZf z@ShZ2sZb*c0^~WGo(81{Eoo zJh)2IcCqE?eeeNXu$Cj#sF&!ZX_Drf0nU$)kAdb2-HW{HJOV%~2OF2Z-PjXTx4WF5 zZ}rnwnSwOP8V4@c&r;XnV{mHyu~JoTQg2ezTyJlJ)+KorJ}jKvz2OzYTVwu+P}QV#zce$j-LL&Nu=z%617NP7(>9+HXh?7_F{M zBGR;%{K$64&oj5lq$?qyQ{b{-w13DCXA>0P?oX#153CVhuiYjkV^KD)$t4K{kpH1T z`UL@43_5tPcfZ@ks+{b_yQh&+kZ;OHCfom0KRCSE*yX}@|F1pc!em#Uw74ui`GG5v zQwyq*2$5XP+EcB-%PkFR@+cw;1`tk5U-=E?DkeXxfD_qWCqKiHIGK`$phg3rc%hHG zUD*8+%Cr1TE|$Gmc31gH-o9}CvPm~21ql!%9NvP46e*LuAus@bcF#H%4sj8%ldejC zpK652z@%%!ABx@?EO(Jv$l^{?2&UWVuB}kcT<+y?4R-)qD3Q*!74GmT953TLMJTO` z>Ps2)a3rCUAwW&5!!K7CFk9)LGz*G{`Ww_{MJjbxa2c#wBSCt~?Rx6xsd+7u9@+I& zKjjv2znA&Lm59S{)SrB9`C0cGJv+vU|I|fIE^ad7av$WC#-KDHV}isTZW%Qd$V9g4jT0xLFuxNo`$^*yDG1CcFnQofR5$nH2gh zE;mUo^(c}>sl2>Ly&%iN!=G&WWDo`ELJ26fYXAT+nbt}Q5K&5-ZkGz|qHHN55k(%j zPNrCimngYu=x%U;yt7fIZ{9zexyH$D7-ynQRSWgTWTBxO;{IY zODFU~4MZPFAWc(y0Tv#fA{9(KTh!0V_oU^7_#MH`XR>9<+wza+?2i+;l$7*BzJRPG zWrWfgj+qxacJ@`q3(Fy+P!FI9`()e>2cnP3i*O`mH?Jbuzya>e+= zZKcPQ3{wStin@^eqp-G=I-_6wp*6l_$Y4%B{e;{nm7E#Zp@cUu6TC^#@ba$f7`B(Q z5P(4B5T_T0hVLPE9uOgp97NrqI4DKZr~-s=q>IA&2mUnWrn1y=FhP#dH~I2M<&4OG zr_gN1WJEeX-gJaUPH^xHMq00xSGysE#&Rzv-$_Ir>6T31^!x$v?`aWv=pAl>J1bg- ze39axrcwz8WlJYTj(<4CfW4B-uMxmA7dA;0%EEG-NvmC87Ew-8hed!VJyHm6N4m@? zv%vf1cn)E_C!FM^6nF-7c zVGQW*)1XH7;td_jr ztp<>cr2*{EF0Hf|)I0$huR(GM#kVTU?Is$nEF}~2Hb7<#(d6J z;Aug5#YNm*X62AVg^abOc@yID@T zrn@jZ0-LM%$^{B2o7p8l>rMBTHvQt-!trj+?qoMA^DE7@o0ge0Hl(oVhr53IHh3$b z0(nyDiL>BhkdPR79r-ciKYp&yv1`ZBT{HCW_Y+K-_#5l+Ca!}E zJ`PX*^~4nJg}MC4f6y1)CtNT=BEKE1bX)S0mJ(yMH4bl21K->e&S+ znDOjy<1nLMWEIH}UuW`yZe)^N$rO$kJ@yrc- zN~cRtK6xXg(N3{sP@DvMr!0axZ9XAY2cd`e(r5B{2WOBG6@Qttz2uyf^0uQ|Cn^!I({vwP~TYmQQi*9}T0&?0xLhJX3L;a~nq@@RjSy4xR``i-fN zQK#e6Q=gmqB0*Ry<|m-!Hc_c~6tm1ClcDA(`Gn<%A8RnS_>&ZFfWuCFlejdSYD~iN zm?&yeh$O<5LfI{Ga!wAAWsG931~Fj{ToPs=r;3xtM=@Tc*o-6>SGU&Xez1U9|Dxsb zKO75L)QGiC{t$15QSwycExS)SJm3h-OP31A!asmi*W7Fap;T^Kje=2w*Uzswa1WvA zRCG6tqkOEYQK1OaAU-$yT4O_do|T`^)%2j%4zyZszF?i#H}q>M9I7D#iiMJ*#`$bL zAEtFmO{o>U!qp;B@~Tm2n9WK$Tdc!;#HNcZ2kK%T$-!nw`6hB(rc1~zt)iFVKxk)T z<9|rH`n(73UMU;4i?!xf^qOkKUMUE&X{({;BFa~44a!3`Gt<*r(6DhQcdWJ*)Qm0> z)I!BEiv3ivuPQA+TL-}J>vnsqwt8E9}bE16WkXgHMuS^QIZ+QPk2NdspT}Bdet1eJy;L37$=-vdAZu@4=PW1y^~mME1vG zq)OBQLv~T2b^{Ah+@)e*Z73@Nl}i&xDw{g_iICPTQ||)AXabK&Qq{$}Gq4T1Qu`?@ z{0syiB*I9PLE`%(!Jxv(zB0+f5aF;cH3XOB9QbarYl%?~!%LO06^3xsr-_B-@&w53 zl3lJBypuDE{-R!U3op)x|z4v}_F%19NFqRVK9YKa^^rr|!g9S>2W56MTqLtf>#fJDs zp7-VMpg3{QIvj@20S0SB>RGLD2p=`HqL_qmQLjR4jgu6PT{a!aHE3~HCm0zK>H?0( z@#-=LrE~)|A?ASOCoyk1IX$G9L^nnZ2;+TnVsS0tx)e=;pGCzQ#FD}^fTThN5{rj^ zxUFKfuC37h70l)}LSG%+AyCFDm*Bc%vdb2u8ha>Kidre8ZzlGRn!~Gv)}&eD@v^sI z9wl=QSUP&2_7${BwggeJaX=*&jHqzvVR{J-9W`ndd`M=vb!2lL zyP5fY9eiy{%kgvi&hH0#-IyO}q!P5YDTv%a1sEwn1nAxTw%SdeIVjEsT22N@D7rD_C?XxdM!@T7YB zI~2>GhK%12u_B1whzeN$yy-}mu7#0F{;cSq;vhvI6-yDt5S7%-;XNaSPc+BAm}9Bj zisd8@Rmf)7Sj_dBhkku z7&HUjCr-GH0xMQ8V20LX`Jo6GKU5$B_PYD{C!Kb?b0mp~DsR}kS*dL9tyJ2bN~P1T z{6nX6u-#VD6BXfqUcL94%}ad$lFe)OUR^96XtxjSKC+H=Fgc%6?nACyWTdoV;-6w2 zCi|;-VIPFzHvzM>U+>^j{jRq^cOf|4>TPvT zKL6CIpWq{?KRC<eL;o-&6;KQT(PapMB8_PTzCS=GocfS00=7 z+r`Dk+^-fIc|ME7FRKrqz5GFQuy5bAdEM6R@mc;yYx9{(={nABqjUZ%c@Q1JN>kIl zyPTXH44f=^9G<*J<7-g=oe;-ZwgEG7EiD>w9Lug2D(|RW)mH9&{P8=dZ)r$LH0Aae zhxKv4b0Ac1gA@*1roO-Ew{itb85GLLjuTL=afA9K!t46wCt7Rmb}m!SE817x^wJz= z0n9ltn-x->jI~Ahkb8w+_n&F!&2S}5a))5TlDkE!34n(Yb#&wzqqh5&j31s}4kG-U zkQ9M>;I7F#d<}@N5En_6rr4oV@4usJ-!Zj$$KEd$_w9x=Jb@Cc+ zEQAW^+TA~6@HF@rM*}c;kR+M*yrt*?bjpUe^IlL_CtA9XhgFv z1>5R}@HnMfe^II_wQ{>!9nI$RvoOV{Qs=28WBz%Nx+j&ohm^oScMAFq=Ni4<9bLUu zSI%`hSLE_-hz$mQd%C^8Ug-9E+)D`x%bXA{inBaTvR)8s$-I)ewP=Y`ov9T@60S%1 zY{OJCNdl!>L2|ozOy=b-*buT}Jh;mH2P-K`3S_Oyp{;Ux>kx3lbOt|%t_=H)1!yv> zBn(v|(_Y+T6a1}P^U6YMBekH+TXn#d2fS9>jg{$Cr{yUxTk2(6r~GKnKROy6siH&` zp^QU`Xf1T>l_rj?TCv`}zoff4yZ+`kv|4X?b3LEVgumYJyy!)pzU(h-Y2a25vb$_h z<>Owi6@M#ISTrxO1=Q6c208N*QH^t$3zK0;4xn690xTEOLJS0Mx(HfuBbypvsYDUO zYA{7&Tk=U4D-Ss?l6))xYx3;8jKq_y6cH**y-#oX2LtMlnOYV%8<}fq7!J~D7^aMa zeoODx%y1?&Yd>kaKs3e4fqQ}~EVveb@ocZ)&YISoV^>qo*|XieJ7<~mU>*?4n<(Vq zs*nv4qEA)2enI^X!Nq(jy|p3%$dzrklt-|AM=kiTxu+aO<$E4=-WgI6K~>&`^H5@Z z2tNrNnd;e>=-Jd(Dzz;YonG>YnRE7~Qv2iuyMXvbCi23P?C=XHC_|Jb83HsSG?Jt; z=;|oh{~uRguj1$gvyFl0Qa=OueCG7I)4PbIH$%F{=kbvA9nO$4SN82o=4XUl`0vPt zysSC}3OqJs?$7{ty&y}lh?#wBgH6DBXwn@*M*=w}g20^gjFk}Rk5xn=2OT|FA3}X< z;?Oshucdi(N9ykMGPqd4yH3RZOQ$4LDJLrD@ zzYOkK-QSkB)j`APFHxpWReQMFn4GHiz^$fPT|l8&j#XJ|;4eJvh*hqK zQA775g%pf_l3+ba(hl;H_aNk$$}$eEL#FZkraiRz|7xr;T*z-8;p%&=aULAtw{3gp zi?1~u^>vB-{x|g%@Si_$Hl}DwwwY>#( z-_~bu*xuSwUQ_jVzMQ?*dS1JY|Dt5+qhv^aZ0E2Ua&P zgoWc_UdfN41{il0f4t87z9M8^^l%f^TA-M6!v-?y+gdvgl2e@h)RM zLLX+?%1S277(YvLbkXXIvYr56Odu_I1>$WqZ&HzxDac|+0T>FGJTiI-2r#pXH>FZg zBKFup12V5Z_~MmWJG%9F+NfQp!GCUM?Hc+d|3>6on?Ym0bx=>Q(J9@npnnp#g_z1V zt}*nXs(i_BKx8G*PyF5=L}4V#>(8i0?r)UTi&Mocs-wYEtKju$BN|D{qXec%Iy!c7>!@I5P{J;Uce|Aya4n7;`9`C>)U7Wq^g1JspQ$Tr^_iWo7TT@jQ&~84KLxk^r$7|Ged;|* zS9v}=&$pBvy!d4`R#((3)#s^i#-?Ozk1VXMgdG9#I9SJ{v^H8H=MmI@G|u!$yo78prkL}}-@>Oz5qHOW z<&8HSJV1S}sn9al=)s+pxw>$degw3JzgxF2y0 zNo6BUU`5UgiZ=Nggg02w?153MP(jcUaAAeBXn|4c1Uw?Lwx^{euq=5)-}xroI239m zb=xAENf3+ZABJSwm)r5;U=fOuo5P~pJk*?|u;&8Z=G8q@k8tP?A(rsv| zMz!Zf%T@+kW>1E!5g)_GC=}|oco1bHH>>8|Lb2h`W7DU@S2&qeI^$K9QZ8TcX29!E z-ND1rCwhkKEaeNWQ2@z?nw~E~@JU@nvim0;_FGPd?hq`N@Ryr)LN#@KzCydH1`L92 zlh7AoLD9^7hZqo{?^(B-Z$4Rk#$IC*NI59-j6 zS-_ppQ-e~tP=y8&+EBzrQ3A`5WNT!p2yGM7h_ITI%~RNuxN7*u4Bm@$N>626z_@8Q zm9ybjf*Hvo^a{}@(Psk2`Bpj=&}Wh>tyX*1sODWl_edLw!%9SxD5h2+01T9q!XbjU zqU(hwiMuKem=NDzy@vHBOFaz;9o91AbOQ1Ub^#< zbfIWh^;8xI2=Qw-ihzx!%sdfX!_RF-+!PDu7&8DB23RVe@lycAtPqMNy95+>1~Okg z_0>!pVk)9(I+QepoJ{Ge*PG8l5Le1#k}F1-)k`^L9(ookR1uLSG%D1H03(Q<%LVbz zwyL!-H@9Tc2{8dkmjvM@XaiPzt%?sfr&;M*YuP!*@K9&dAkj*yW@XF=4X5v>v$^FV zbjTT!rt}~UHi3l%M>(Emp<-i7tdb~XYAg7spw4FO2`u0&lIo6ftGTI>e?<{dzV73Z~?Nl$qRTJbw(QOgYj03&_cPkdrT*dOeySyMDVPTOd#= zBA^K-hrJlh8(9PB63{4{<2Df&f}et(5%qo8Mtn*tHT!T<4nrg$BxWG+*8u>L**n=U zgHUi&vYm$mNjBmZypk1&1z%{6-a%3MSDQvBHVT}8`5eqbGxbiCQSci2(r>h zXtPa*!gi-|j~4YN)l=(CGBf`?l2)MB&46&I6(x>sSgc-cr(rN{#c=AvI55u@EjeO*$R5H#Edkxgd2yoh50 zV#pP4j>Mi5KARYIfKg9uC}eZd0%Qke$cCWkT9>-D+=o~Q1XoD$=6;C6EX$5y5y}v13nRtZ0vAL<+;bWHx#81w4x|3F=$8XDXfjj5^t!qBAsK5 zU_}&~Q4^s1y2+3Vcn^K9!)ZnXQx02OM^9sDW7GP}uOMoiJMU7paBB~fNOS+lbZ))1 zX=~|>(rS@5z6kLnXfL7xz``Uq6;*^BI;51S4{VjjGKvhvYpJV*{6}npc2(3*OwR~j zOpCj@>qH>v&slrx)hTsA=IU(Bb~bl2fezVwQ&o0H(9s-{%TV5dlEu7_Fiwf*gO-do zM>$2@9JHtSA2(Aj1+KBbh=`OsDN4CeFh`E%-`pZZKX(x5l4yt)A|=rj9O@7tuQdQr ziD*T%2ZkeZ3M(#I)+EW!B`7gzgDqX)Hi$lP(W{V^@(Mx2xrr|DrWVpGPAEtY^-lTQ zmfmnYLxLSE*9BUO{adzZ#pv)f(d;y=YEMgN|4;eHg%j4m_@tX3Q?9+^Dg-%bCRcOSyQ%mH zNEx_+P^&v>)FNHG64w$-{wsp|`_xMqq?f)78M(2kWV)@IIK}_6O8n~qt+;0lV8}4O zxo6BNE@OD8#*e^WoIIia$Lct=Mgt2p8r=kbWOmt`v6eFC&W(dMI{@3Zc)cc+!A0#R#*Ym}F0H3rz+P>W;NR9X@9}PNY7za`ExrIMsNQW6i3jC-LM3}*t3!6jq*hs_52F4LMXXd_afJ{~}gs_WXI>7tV z<2|ehEMJ19iS{U)CR31UjgHQ;Mrr4z!~C&hH{-%9Pa8~btSw?S8OQj?YI|>q;>cBd zX=&%Xph%rOadWYcnUJyyn0;(dgjs<*CBj4!b?C_ik;vplNT7R*`7KU+j2XCh(Ov;_ zVv$ui{Vd1RAVN+Sr9=UC{wjqjVL+_trOA~e2C~PCE@H|{HZ-N$Xb$lsN6PX6%xEij zY0^D#v_FX9n*$u z;8JNF>yLauLn(k5HN?JVx1eYTwjZ3-GRSNzJaD{xC%JODJeIo*j)&V3jy$#~HV_WC zOZu=HuBi4YTfNk5w~X6K6LJzfqn=`Oq~9dr(1YYMfUagl+m_oRJe5SnITJ7F9D4=* z<$fSp<;E}_{xQ96Q~4sUnTAkRCd%Y~69a9JL)8bu2@U-Q<0)LN-!d zx`A`j4zj_fTL^-J8`SH;sT%(r(AEAyYcQ~`Bteby`Yt*C)PLXQ~WlDe=IL62b!^N)qhK zGou4OiYT6Kalfv!!Jvvh<0tp+Z+}~PFD=5pM}tO0jDU@PV2(ws6yd&w9~IqECn_tlBwtTF~SF$N(X zL$+~oDwCMV6jsE}G4iJ@Fnl zOy#tu=gyNIm(>ajtG%IbW%MA6tK>`2MPz#IsFX>~8}}RYjRk6ax%CC}@mz6Uf0;So zhyl-dIvMNWRn%7~FD2$vgAed(ScslG_4w5Lr~aJPI7=x0O63;i<;uI2-%_4b{#LzO zy+OTA{W0~`>YLSffZIz3lOpxZc_boYk#`bQJIjqKbGVq21j5$+s-+Z~|Um<54CLMV$_A)feA zL?D93FqYvx;H*3)q{6!3q%fqdvnmKyu|P28#G=GF>spZv3ef>^#b6%;D_kEi4&j(u ze5S0sF^=sdK*0qhmw_;Y?S-So2o%>fh{e$gao;*TCqmk}vMygkyaO2`P#dE!g?JpS zFdAdz6Eza0V_OYzT+OqN`-p#K|7+10e~ z1|!DZC-OY_e_c0~azo_?{i3;BuNDRP+UG28AdQq?kZ_vn*uxjRALp=057t^PO--?o zl#FyOJbuxUN9L8Xc9fa(Rr87;>zU|&&)O+3aL zCD1Z1_Krzq03`h)O`+2h@Jvhru`l2Li6ZX*d;o^6#RDsC421B{-FawN3yy|?ZxL$z}1cR z8syK^8+k8%Goyxb2A(iqAtOSs;!gxOL6hxXEnVvwvkk4IQsLat3IU-LXc*M3Ohh?k zr4TmydinY*4;@^%WG>pdM=F%Q$)%dQTRrgn<3pf`^C#~=xVfs>-dM?ku9RDmGE@7x zb=ZYEMJ2gC=bT*DcAAGht30ilr>QT0v1`D_X@8Uw^HmbPfA;<}_*Dc!Zl!Xyl#(u% zscsO=I94{Q75!Rs<>HIZ-sP!Id3Dg(`5K9I8MA0v+FNX+S~o9#9p_-^nR}x6piCqq z&pQMYa2w#1mLz4s*Qj9}2x&2DhuTm9EovH`bM6-7Eq6gQVTLd<5kXF(6rw4id2utoFH2V$tm|2dW|20jo4KoJST5n5$}o^zC6+y^!-F9y*ArtzKH8aPd6+A zF1XME*r${m@FAla$}(b-a6Ag43_(sbN}^g^B*+h2on;KL4~VuAcEiDr(Zir*k&MhD z%nfVlU{&dF9SE4G^(~Zk)?Rv!Ih@P%oLQsJHJ38Y)ZTQVO8U6vml$9cW20TRZ<@=H z{&3jXD-%^9C8j>6^5&eWS1CPy+R6EW6H`fase2nem>%RwX@8`-y0y1Tp|$DL`=?3S zRJ;!moXDn13dvx0qq|Q2#)8p+0+ZrelOu3*@(H!jl7oh${u?EN? zaVzwt45*&efDuhajCjf~NNb)|q8CfB@AoeXSe zOI@SpQ(al%wgkI(Kn*7ILdNFbRIF*@rK-6hyS8A`&08tf#6Qe6lGM~w1Top|fvC8Y zaH%0^$hVXnWX;Qy18cBXvMCX~7AuLpfekk87+3Obf~df>t?!Uall=D)RJjvy$!D`} z5G=B9AWm@cA$GDkgPJ}*biKmPZ_q=`KvkWci>k1z7ON2X zEnZVEDT5&FFGn>pru+);EnGvs#T6mjZMxUUQ;q>oF|@X==>p7XTzEEBF1MP6JdR5v zTbORiMo!wSv9_UKw1HoYZHj%f*URUmMoqq0E|Wb@ZltDV^Sz$wuqRvAJpjaoAyO@> zX}L`HA4{!0^^E(#h!LvAS)D{`bA-}%5Bruy%}g2E-M=H9Qt;EBM-GbFLl}siTo6gH zDPD0+3M3c(5_OX?x%QGDhWnG)*j~&fIQREzv-MR?8;R6c^>x|5_sdxoHgvVRPuiXK z)_Uo5&n;K+)&gdRHVG-=T5i4Vr3n7>Mr6B<;ck~+kzZb3%3VKKJ9boe_U%VpC{7hR z+d{>|jp3cz*4(gHY^-KE8UXB*6-X3G(AqjvdT{ZUL|KQHAmUA;pYOgP$=?fpBW z_==SA^y~MOV%?1VDnV|az%Yb$Au2;12acPf#GY1ZO0=?8)t!J*G^m(6x3G{a7D)jJ zY`s!POg5?>BYoL*SCuPwuZW`1$!IGk8$JK_$%9BdwR-#R zjS!qaG_K0uQ2$uP<|YvnWS*ksu`%MVviX6XuYKT~=T9^)n-o8m0?2RqCs6V7J@tFv zK=FGYHdhu5?bNtU(035`1*j;y5pF+K*}He=uQF9r`GdSav-7=cc)ImB4_tg_-aS`s zU95a6=l;v*XiEiW^+^`t?~$`Z@p7zt3%{Qf8E3Bp2JQ<~8sxFDt-NcsQP-Vc2UYS? zJ$sv()4z?4{%elm#6dIdIOVU5etNI%+5cuMUjqx-;A!E+?6Kj+o5wkiBC;*Pj)ElZ zdjN6SRPiBVx>0(3EKw>ceZwAxTNhWs(@(Jh19S3jVh%RZ93N%MrfyF8X2bOF!pp6g zcM(DlmhnVo*U*a8YJ)`BIdE9+bd}XIhU&oHC=m0_-+7CnXD9%ndiUO$Vj~5&ryDmy z7IO$ETdK7QXu*CZD>!}n7&xBjVabl#t`eilY+4z^lHxxS`vGCX z&6DSw&GQe^2w(q|)B>%hPLez`UP!qMrBlu8v)3=4YVp|I`AqB5i)r!p%UajvuD_mV zcel>N1>Yt+CKj_YIM zGlP@M>WdS8$Rd)cHln=GD_Guu@+EJvWW_jDDF|u}M=LzYY!Sq4@>jlV4N=HYLdO3P z?hSYT^eX+#uO1G&x6ceo`GQvN==SvX&Po0HFH&DVcu|nEyam#xbo~pbjPLS=pEs^M ze*C1PpZV@a{MPA>u(Ibh;Z6CB_4Y=iU=|u|3fs0k8rI6;LDt97S73IhgrTJ#%>{ZIOH$JqfybQHQ0YpM7^G{F*_*5xU z&Mgz2+xOC>hCvc9Y9%AQ4}aVl@yPXI$Yn6QiIUbW;wS}U0JuU*P)J(#FpxUgIC54a zQbFVmR*f-Sgc@-Yz1j7vN^&>@KBjmDHMHZ2m(?1oCr10D@GDRrmpbZIU3WIKe_dT! zxo2~n9BWmKn(s8$ zR_pcQP{uzb*XQfX?=k)^yM77OdT(l$YV{*#+(Gi;FC&-Y4O4HK`iUeP`ZrShZ>nStvH|42;ggt&QdtHeAE*mdOjk9Y)83wJf6Dwt9>>f}Hm7I>*e>mNn9 zk`n(NuW?u7i4%B5;@@W4;tK-@G8Aw&a9jzMjnQs@#Mz2yqVx@cuuhL&{fSp8ue$LA z5m_5=)WGFwKxR6BgCAFL&8QYID0D4r-PAjVxngKNO<&YBBBYC|x@N9uV{MPwfrMx8 ze%(B#{yPBeY{#C9sCVx|_o2*f&*n0jTw62S5y2ufB6IPwgO2S`fY79MrH*oBI3vU8 z8pg7r_cUWs*QT}Ak)=>kO{G=keXsuQSGB?q-cWJfJrojc>*mPsKQ1I5=u}mRXwYbT zfWLLx&?z6&OG+I~L%nF)_?$r^?IB)7OV$DR37*;%SLQSQ8wX5Dw?V@wB7dKUZCPX1 zOetL=4w4l%oIK#GoK)75KwjrUqG535>cy+8-7dcvgsa^Tb|H%2IJ&xebX5tyH^uvW zd~`LMj^vOMhsbB_hZp3d$i^n|-KD7m%+DmF>6wuZs7%Ea!Ljh4fg| zg7L0-r@TIDx1*g8%i~`+8kZ#hPc$0JA-ok&OQrnI+vM?e)har>n|@-D zEfB*2G+@h&W-e7&UoX!T z>sc@KQ3Xi2a}nh`e_ikND@vuGX%t^l*h}&3RIQlLEr))tKuknw9>7j%DGPqS5G+wl zd#|C*F9^h8b$Q_`rQ9sDW9OBt7M54BSEi#%zhCicf@|=;x5GI86nTp0ryiJkWa`PO zFM|(k5e*rk)?A`otvpA$hw%_D3mFobAfjZ8;*&g#aD||Liz+n0!;t)paB}m@BmR3> z^B9mQiN^_~oiGE*~)#(>-aycxT{ z%c1aU@XCrp0wHGd4e@ozFOCdT-i!uJh4BbrhD?j3pE5F*DRg-QnwJaUjPcs!>;8NN>{ml290lVEV||dhX^$Ffo!1ps7rmXTCVk!Uaefq^kFdbwIHi|1uqM}EeljB z1*sZNaMK5_L&Z0V_G7Q%aWF&bAd`^}93LJr@LnFZJAA<&u*p-!9m|}bu*6#HN9nnx zjlNl5?3p#>ikh3*I$Y86A#w%l9ebt7_lvp%-5N!3S;geTGmBL#YeJ%HfoUUc0RFC& zP83rG4d0`lU8z)8dXE{9ahh&a)5?dpX0lqAPyq~Z#1V8%boLJL!PNTAlXy=a05JsI zEaa3Ew{~&&;@{x=<>JOVuy_`2?_z|jjZ!$ss3tU*Xlhja0s9WTCZNSgVjDY4_bK#ZqRhf z5xfwvJ;3-TglD*W@ZCa))ohZ?kobMsI1B>iHstc)aXABpfcPqCFg5BBTj<9(MN4y; z($cKbOqCl}yV#|$B}O0m0RP4PiyDmd#Xll02@nqoPzSZ$MT5PyFq*@Nrs69KRq$$}O4p(wiI6O_br!hn+W zB(i(#5%v;aqHK}MRc8YH^11L}+741q(~cl+n;jj0sWG10eBpjQXmtF0-+%J#(fOZO zmW&&y0z!@FVzzTaw=4BZz7ZO==FCN9u+c`k?9W`gckgtCeo*V%K@NTgzk`P7Aw>m) zJUNFIDvC|PKi%y|MDjFD+a`vWA9a@$*fj_OEGe~9=Jm>}tk?a_Oa9{e%v-iD_pbZK z8?L#|RCfNW`ij>py~odiV!iuSkGe;enzmdI1ZVfjAn`0!g>BKS-v^NLvZQ0Ue1R#|iq&^|~ z0C?v6+$~xm87WuF zm2mwnI{iUmGzwC)11Ia3QY0{A1`;-biN?-WlrYmPLLFmPdf~8|hLECiqIY;W%0POmng%3BW`?r4bd^TMh~pf}_)Uzt`Pf4q?l{XS-IB;#ly=6>=ma z3t>bmR~N^D=#Uq3nuYUAc*(Zm#(yfixkr>+M1dt!K3N$u?%c41pbTMB+?5oF5s5p; z$P*z{M<|JaKt%3lLJvu%M{g#RARJ7DZRQn>_uIakufjzep_qzSbCyQ02YY{%IC#> zjRfG42WzRJdp^csHMCxtsq||b@wLsh+8X(@pK0{Zb?{uLD37%BkIIvWcdk|522_w- zAXE42%H^*;_CqBk19fZ->M+ z3vLx+@EejPcn}0a*hEmY3etv|MS$ap7r0iqK zlYH_2NoaF+ewWm3Ussn}m1=H*;1zyBmDEW`C7Q1LL8-F>_=6HZR)!Kms*#4pNO35L zqD^V|W_}mh=&)14sVzgsKEn_)`PlQx!|gC1<=%yS#*m99&mx8ISYSlR2@FSJy^=e* z+R(qzx6@tfD>&ANDQVHT5;oi%X{R=9u>eYxyB%+dR_9X=bz3@tT5BA<-LVUMSH>4z zG+xFs;wXZGeg zi--Eh-|PG8>gnz6)2prj-0!J={m7NfONagVQ|Kh0hI#Q~JWW^QWBR43UxV%MaI8Wsgk4ED0RAKMXMoKc@q|bD88J!4G~r=e#3HYlZfwuvkXN)NU*ef$ zEQh2Wcd>i;5(7t)ix?JWkj#IP&C(d}@uk&m(u~w@=r5|4%;*sZ3c`9~NRX*Lh?L(8 z8q^edNwXi9P#y^ZwbpbiM*&q-17{HGSC@6Kzef#opejmk&JSztwV65QW~$PL!|8mx zkG8&~{&p^eb~u&&gNmv(;eUGLO0TLOevVo!?%iVrU@bBwC*!UiSZ;R=kF6CAKUMU# zTG%;!fJ7D#Iv1C;0wz9T!=`Wd^jw`(KWMLd$1FS0s3TVQ(C^K;-r!?j%hXDN)i>rU z$H+?4YfFL7&8^#dHrE{NU7aT@&`}3CzDgSvPz7Oin@VOa%j>P=XVaOxRk)_>^|`w$ zL=+3f^v^4~g6|jX-)*hW#Bl+d0^KcUBT`~1epMy-ciGLVdE&BqQ}tTRk&TVHLVS|4 zbJ<0}M@d8!7gx$xJd=z-r_k^tgQSTr<6>gNU5QO=VjM#k|xqMBbA){q!|k4 zm6;#oq2-n$&eYEMnO>nbJNFtmJkSM;+MJT#&_-xeBxCnn-!U$~99AH8^^!|+J_#D0 z@%j1Y&Yv{r=bOrGbN=}&*mSv?|8jpZs-}VF=^wmd5dywe(j%ef<1C zT|~z0G&#!|>HvX?fQwJoa+-dB1V3v)ROcdkjSGqZh43N~iAb1|_#5N9|AF_T{)NNj z!&v#yXF*ea*0PrV|Ju$x%#pLc^Lk(1XX{p&sgAdP@9+2h-S`6^V)zj6i$VVUl zqfNY&SGxZSzSsG@6)7uTcbgDwKI8SC5u*1>q$<`2@jvk=D(kTFNbOtP)RYqsN?064 z9Ndx`tHfwfon@K8$yFYLD!(qCNe>OB-}%nH`wk7I4~@*66qnw9=(5Aw!-v|Z#eGBR zy|1N*4vB*&Po{_Vp22YPi{iEub zubUY4XJ7zID3dIJP+41DR-S{U1*#KZV+ycL)nZ>;Kj!0cOPHWahXe0`l2?ifUI(TU zK?V5N-CVmBq+eGQD3M4`v;xI~ELbA5qmyHl-~c$}BAF4~y%eL4QfD21aW)njU(3?u zj##-!AwdCUm2yCFo#0TehcQTqFm|-AF<=z9=czt0KU8MJbAV*XP z6EQ4RDg2HMGGK-{7QgYJP0DJYfN%}J*zq^&?^XO z!Z!r4Oa>Peke(yGgj$6`*4{OxcNs5FE{~^j#j*LZ1o*X(U{K7Z#+Tm_1gBw7PF|S` zPve)5Mvw3P%OwdS8>7?V)RhCdH1+EJZc%yQW4JhV!wpkJc0~e&ka($25Q_|?DP14V zAI^u!XQ8;@*v-Zy+@`6v`wg)AONZo|1q<%nZfjL&TJkL=jKNLi1@lG^ysogtvtk=Mxy_ z_!k&Ogi~|m$Ity0IZ2O0%>!_(tB7QNBi@uxd;Ws@m3grwuETC-I zQA#AO5ZOwnon?Y{kB}9tmZ{oF_1FhVIcw->*|uq zc;Ynbd<1S4j?<`#i@vah+pO5Ny?{6Rrp`2 z5@FI#>5nYe0+}qZA&{ZU!oh)H??hOZ*k@5C;iQr(@RA=SV~GBsSo5cIK3VKtK`_M? zOb?3TKUNMjs{KSr3Gk+X+jw_}uec>aB+EcyA|aBLdl_23_*klVVx=^eox&&;A_pXp zj7(j2igG~MD}BWG0<;ojseqF)kBK0?d)dfUgjESqqBz)N62?W=%*zR8p-6usp;DOG z$xBoU6>V11?!i>7FltZef@2*MA&y2}V*-MCBLah;vAVHd5+9vT$71{+BezHp^(;nX zDsPAI=vB@?$BbbVB(V!cRvbvCnGh9eufi)Dx-{Aa2UFqbgmFB?G(snl*p?`26e8N0Sd_v+jo{QiVe56BGo! zCSCMiPDPDqXnSDbjS~yG>HNEn-*mh%Svb&p%Vu|Di)0W7c0;k$LTYj1@%#)zS6yES zd&NIfFEZ%4?|Cc54A>tt^+AwQaA|?KG-a6fE3m9;6rp{wz`(DJz!yq+lgh@#qf%U* zuY)8Q;?-8lm#S*Bnn$%~E0zupal#K1!lItdzq?oS^EG%ihqBmb!UIINB?1HSLUtsc z3}mQjL7cWwS+e-VXv^VC4;7bvUV`9gn-EE+d7hDUDn>RNwJD;>a8eY~a}%k8q6!Lr zL=YCF;wHxkgAS_xBh;?w*NS5WgyIwjp^|(uJH`TuLE!O&L!6R^1vn+?c!3o#mL#j3 zMA_lN{3t$hBGRKN-^B9Kqaa+u0WmBgaRs?F>vPj83>-Bw5lj=7OKrM{FQ1sA)Fff> zF?xXzE2ZL}Bz$_-?UZ{XHKJVaQl4p}&ZBouNk#PgzLurXgyOa+J8s4-Wchg-o>DSm?5uFeRRe zK=y{Oc*W8UuX1v;Me>hUPai(nBC?V8R=LSb(krpZHH25h)9)D`8agvHJp2yI0ZH%l z(9jfF>^PMVl#cx1q*WX$k7Y$}OdK4_FUQBSLn{O2Yv=CX`*FB9pI^B7#LV@_qX&y6 zD$1t9c@axKi3MjOPCgW`^B~<&3hTrW(Ur0M0YQ~Tg}=w1drtg~xF4HRkvM@{!GlUO zQz4=VBxRlCznZH#BD~b?f)&(;;3VP;{q&faQS$8ws287Q?-w|Z;P&jUzF1P7dDhm& zM-HA2h3+B+nzFB@T=%G0sllx#zTHpQCMYCrr!iZ97ufWiv!ry zy?g&~aF!&p07>;DC8~oz2Y&y3=mP}odNEy?PBu}2#fBw}$F2+?t9(>zY)<`G!EWNI zQ0UokA^iDJ=%E!q<#f>UM{$v+7o&^W%-HZsu$Vn^0DHn$+3DF(=<~e2o+gihdxO*1 z1Hv=ma|^@cne0M%C0h(0oezbu>+SoFmBoCSilvuBv!3;=s|Bso4>_Y?@B$V)qN|U? ztz7Y}C{Td{!cs`pQR+`GClJXFB*vGl+{xe1XEOOO?wx!roJ~bBRY0D@K-pX|9?lMp zi%F_4#a}Zzd~_m1!QI^Q_|hB3mx^D{9sKm(N%72co+ztOmAOfzQ$rc7P|Ed{z%Nk< zr%q*u9$t)QZ+MtL$`%`=7Vh`pZEJXPZ-9<8PYWx#XNG7SR-8*B$_~y4xDjePPKcgg zLK!mvJL+awo2uL&3^5PjcW;V(>Ju2(6W%-~0@fpLMvlnuUBfQOKo#zSz4aWOX#C=v zLz!3I?L7FPbN8#VLz&jCXFJPxT{$;hEDs(n4_y1wWbtrebs;^PiQHJPVN04G%N!b< zNIg0r&}LV0}Qs2Cp}oER(@7mr3Vqv>Vub>9q4&rAkJQWJxx?(~h!;1|md zjt#tJG&D688YQrAdW2}Q>Cs0<{9{-r1E;2ENZ%);xh7q|yX!!x1ibl%F2nLDPY?)dFnzI0P)ZZJ2M6_@T7M=q{X zyeOK%oUQK7b2s-kL0stweEJeB+NaTBRQ#12vqjj}49Qv)a+7FvtE&7QRHLftOJyUF zZkKbny82vB;ow!ME4Y?d{;l9tF12`Yz8qih4VOp#hsBgHUK+}Z%;;z&va~4aoEI64 z#{Qm)-ENcfg`+ZDmNP$JX z*6&cQ9ObNcQVqo1{GniKYH;yxS9X82sQUdSqaR$pG=FsZM&Ua=5gi;(zUMbx6Y1B# z{N?{1Q{Vs7iG#xnr7yd9$mMY#f}5}sh1+EfJrCxs)+8)ii9*F$VAOyhMMvvU5q}v# zB_JjkdV$)6iiog6Dq9;MU3u}r!AcoN2Sg4i^O}+x%2!W(oq{5i*nWl>_rYkqI1mi{ zAwjoAO2P#wzNXZ^kaAn+qZH-nf2CS-5%UiS^ux?gIuu{L zI2VrGa9Q#6p_@BDq1TQ=*4kcXLE|?@4h>Rf46Y#35`k^mW zI=}^q_3g>28&r(Dck$*+riOzP~_N#bK=?SI4>!{Vt&H|Ae<>B7*XU+Pj^EtJnEs2|RxVu_AtbGi8p&AwE_FN`3W`CKj=cUuH7 z{iy*$-UmYSOPDe8OWbda`~Ba<`*_F?@?`v&V3Cg8Fh!qMmz2t;;+XnGv#cWb;Ku64 zxgmE)ssbr?t+o*1r=l&^3QQ6yD#{M5{!=KE{BU|6OinQwZl^_Pu}6g$3!3`5zl8=1 ztBS%N5O)oR#K0Tw(BG<5DQx$Yu#1!F7#$rahmG`+AdQ6!fy_9GmO_>=j`=Ww9=-3zuDy^k+s?#Tr-Z;}t2R z=9;rb#l$H(2dMf3fvk2wOwR_VnP%+hC3yav%kxzf=R92hQexw7Qs`{Ia&rN%Umqb* zn9@ZG%B2VCA^(Z|o+5JGEeD{&R#EJgOrWZ{N_`DXO{CmXQtX_7;5DT1}Hl(C$ap9 zAT>|iw#L8(%z<;_txCtlm`2@l>N=7nI)CSDUwh{zo5vj)xMmOqd+?foJeCVcdHlGP zf!wV`+=a>4k-pfhH?jNi-StCa8l-2=@ly4P{}G~ z>kRb8CgM|xO{|l-tO}I4&|RREd-bb6yH!DA3g4d)-%boA#t)B=znw-Z>2UIWNviNa zKYZ`0rKM9#AH=0buBgA29UaZ?J+5Ar`9m%p%HEkhqS9=&xxnOYEyLixX4qbpPENxf+=O& zn_F96Ig*-5p9cBtCMv8%4F7Zl?bh)6oeeaQR@yRh!^zFU$ zs2EmtAQMwlOKz#M@zBT^?xSo`Mz0VPhjRYV{BXqQ`<9Wn zZuiG|PtZ4qU7LJxF7+t?dt-q$?Z*CCVkh{u{@9Bq@@#+X^BfZ^{juM39lb_+zYTbf zQi8NMj(BdDH}%I+&w~6$e+rUxFb-r}9*>Xyk@94JK z9!UL67OIKK0t7eyO(l9N`rX@qaykd39m=$b#DIXUxj~{|z4DLYCc2S%O2T=kT-7wawI7Orqgl z%`x`A@K)wMo+C$=7nYVTDjj&iU2W@@ZJ4cNrQY4J-~K6H_=s0u@TAH=Irr+A{U^TK zdral?kK-1oNauPK#Z-u}QV+VxRoX5kwHdX~Kk^dSUv=-G#P_IGM6VljX zA4lau-BQ;}J9=f#E;UWtDYebEqKlGIHCrWHcS=x8$uZ5wmf^6sB4vkD*(|$7{%*LZ zaiFvdNqNCNJt5+C$7$Qg78f_TGp$F<3ssXZ_UEUBIqAY*zW8zfbo%+Xpxd8*rsr46 zzuh&%&6IzDKhOQS%Bg+vAMy_oY#y*bJ{uJNmty$3Oq>*#iz~#H;*_{bTrFNAt`XN#cS9b@kuuDeYbE-x5Yj5 z?Xqba-4WYjN9^JvKPT=buJG03HPm{zPuwqFi<0%Ac%68?c! zTt6b-CLR@U7w-`7#J&F*br;`F<)-(F_lft54~P$n$Hj-lFNt3k9~QqNJ|aFUJ|;dc zewEscPlzY!)Aj4(H^e8!r^Kg;o%k)fP=7`|C7u?a1!4RSkx{>^swHEU{R8o=_`LXn z`1j&Jh(8p6B>q@@QT#{oCGjWXKZ!4kKNbI3d_{a!{F(R~!LMHz-w@vve=hz)d`tYL z_$%>k@n677e0r{!aY8_y=-V{!x4vB>g?{eenbFL;Nv6 zLcjeN@ni9?Voy9L6#<~m;RGR|Tg7=(YKMyYlp%;qRpB-%Q!*_xWZ36qo^<*_dQA+= z5jiTysH!q9OL9U^$|*T5XXLD$lLzEHl@qmlw&4 z=_-9(ULr50gyLoLq`X{SK|seTd6m4HO3~NIYvoJj%j9+PdU=Co2HE)98>P8bc@lr3q>w!BAL(w2_w z$ZfeJcd4~?PTnhDC0{LHBmbSePu?$ID<6;#%Gb%)%ZKC}St8CxX(JXynL#yf))7Vj@lC_2cPjnO!Tzs8`@08vUx(X9i-YO5lRQ7_|0KO>nwt&1+-`L2{-*ea zn>M5Srz#t*NdIhF!wEFC-D<7nuRG1gs;_F>N4yoM8tj@Kr&G~=yGCm>s$TZ5tY(?* zin*Jx9j&@mHj!Q%4Ra@KwAip|Y0bb!!>DdqUai&&R}81pscz{`!09wA4J+Pz+dtHf z(W;p{b`U9HnMTde1$K5rHOr_}Dh=IlZ*|*xWW(%Oy}1iGmfmQXAa?{+;H+sj8(OfUbD32=(SJXGp^mOKW43zTpE0{_I>tsfQq#8$4XJ23 z5!>vvYSo6>srk0K_h8km>D*E1ti55>YD}Z0SM)|eZ?+9fSI@nFEHA@oovGjgTl$V2 zcXY0y<&@nimG|5eV8DZq*bSqm+jWRHps2E1_jUC~JHP|r7WPSOM{6{6Cs?hPw+&m1 z^WN0Ba;vIYwJ4uzhFw*(qV~|5`fix_-VxNub}S|;(BLN<&3Nx^|CSANw+^-ls|VR= z7`79sbh%^U9Up5p$*f5;B-CM(6v^(QwgXa*xi_2H!R0C zT9r;?D{33fcB5PA7!4-`t!WM?jy}Jv+M%7g=Ga=h9b~errqK#CS*e|7tXl?#RE(D9 zm{#0uJ4UPAG@*ap3MpdZo}HM!+h%7+ui17Zz%8DwRua6i(t&&BYNJz0RxKSu-qzL8 z)bHiyW_WXRpS0AJXf1Mq5)}Nw~-O}1H1ShdSC~qpt8aV&4$7UkWuhl3MW;Lz~Cgd4*>UvW*Y}Wn-%#~YY;q-4Y01ILb`tRwOmX_GCboGF1ylBmK$k7|R;*y~X zOiyj`@oC$qJ9gJ+G;5V0pY(>+4Jm@Ha>A(H;bFDS9ms~;KmVwjy6$w^$qP?hhO-Q4 z)hbNQfPa?N(+TmpW1AaJ2wLJ0S|g!tY#0qgb9DDsf?8wK8XOj@BrfRhUK4 z)(or`QROD2#Ycwu!wk&N+Zy~YRNe@Gf zHTX%*R&?1Ll{aI38s#dtFCbX0iwrf|iW~ShEf%2b{~rbw)8GABf14R04eZj zt@s7w@>USVQPI9nNpN4IyWs?z$n%zC24TApoE_@1-*VH>z;eP(qiUIow!QF>K+`za zX&C_+scxA;mb&7{LA4UgXKin*-SoEXi=wR#@;*futH7FuEYU6rQOgI z`^*T;$g=SvI4yq%;o(G?Nef2HDZ4@FgW0czI=0UMvm#%K>}9l1)OrDp=~Oe z!g&#J@V@}Js%>lP4cQs$y_Idh(`}naB0W*4u1O)A=RbuM54H48Gq7b^oI4EPYATYo z5`FyKpNue|7}N|H^3{#D?QPfF;d?r)l&S{qQR}v;`_6I!7Xn4dBeTzm_c21b(tYu^ zRyF^k{aV$s-ubDU&$fTc5o#~q)~*#^bksInwBfw?u$ImeeDNV{ZRkaZun~Gt>t^w1 ze&ebzFFuD0L31zq)j5{CPp@d*ARMNm*?LGJPqU_0Vj1?X1P~CVx3D%5dtn8r-L*4t zfq3t6*G{7fa7=A#tTI+lnU&kA0P6?NZ-HTRjL2E=RSy?Lo6s^$-$L-k%m!<}|C{7K z(@?Mi1*he+kylCR43-4T0fH%t&+M3u4M>YsUC|um5Zgj^(`r?WMkCzRj7GH%P=)zz zYHh?ppk*jk%4_M{A-it2cL3(m9&)iciXL>udesEJ>y<{=yJPJ7z!GMp$KEmEZUwfq zO;klD%GyE!Yx!C_GW}fLsCNRKUoAZj!j06*UEMK;v?{d9#dBpgi?wQkN4R*s76-C1 zO~_8mgrD?9Y`LO$%~s6|D$i~xEyeDZp?_@CII&<3ay^~PJPH$v=%`~owohp-#q)%{>>Hnot;iooLxfZp7&?=0w&$%kJx^lr@J zr!ICuwPW4ZSfc?O&|R%7Ma-&#hIHL$cYqC@3Y@d!?X+!Q$L6K0S*?I(Sd~^KqMkGt z3HT0U4B&1otsSUzHa5TlJ*=*F`Q0HXPa&yb80Wo`=f&Tp!8dbfY!Mw%bHkHCxH^mVy0UU=>`R zqFq#3Wtn{LIpry56yv{2a#f$a;&*t*LA-C>?D=xOu{HA}wt2q!frp7Va^DyK54>La A-~a#s literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff new file mode 100644 index 0000000000000000000000000000000000000000..2a89d521e3fadb6ad3bc1738830a2c039bc57272 GIT binary patch literal 89824 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

3G|%UO|%AX>y_DrlqHm5G}x0qqtuK)rJSvbj0n zdBQ5I=hk@c1cq~3-6XA7u}C~E!7O-j5(w-Qk}F6_fE%-@?$nuGP^u&QI+!Mi->M4q zB$)-LdV%y&lMwZYUGPoia%&$qr*m^t>dW34V);*X=OK)mH!Wr-A>f0#ab}c=n59Oh z8}PM|Ee3$y<{<4I0?IZG|1!nUs+ipk*${{hQs`oVq}zOP z5tKEsx}5Dri}N{}MZE~K1we^zueu^w^;N_xk7v_!(BY-$iY7E| z8BpLNv=~ZPsGaOCq)$P~cv67`tvu500CY-4U*$w;rL>9MA|%Pl+yeA5ax!;3W0T<- zD3I{wu%gM%2^5jw18k*D<H z%L5KUP70?y`Rw#Gv>WM#S$82jJ`Uv)7?u)L3Yp9#Eq+PJ)}zGgZh+J*nEZUkr!*to zgfdXYvd`vo$8q9Syt&Q#dhkG|TQM^J3S8QEoKxcO7Sf9)n_UBZfOSz70kspeHY`I! zX7hEV*s|2!Dd>i%lj_6~T{3nhdd_}@Vo>ZNDEf_Qhr3ni#J;R%!Tt!C2&a&>Uj*%E zZaNGgBE5;tl_?!aWc?+Dkae)M#Pga&%GIm~#!0iIb)(6D#7*fESo6>Qi9r(GUsk$+R(@E&}7s=XEuGnW8lf zp@i^l2@5P-ebTKShkhPf^IRTP%-QUTEO2CD(j?|wJrP(f+0~(nnujWGyv55X@p8MIHKD9PST1|xIMeMlZ4K7&5)3kzvbG)G@!i)heEPt2CUANyfwGlA#9FF49YpvMe&clyC3167b7i{2ET z%ubWSv0G7R3~GKxo6O#P^QquuAw5N`G7TkZd*ozcao$IVZm`)vQt1V+v-;>oK1C<+ zVK4#{&Ka>ugm0j{1nvR}2b*A*v-4E82~Bq4cn-i3QI(t%pk7i4L6?RHtI2_1T_OCI zE`bo(Z?pLxQV7kFf&n}aba}W1QczIyh+7!EDF~S}87MOdDizF!)@`j(H!P*y?oI<; zO{D$Ki7ihQJq&MV8` zPpTEg$*FavG%c{u^3H${QtInWL7xF+mKMh~{AT8~8%f}lvk*;zGKj6B-l`IP&8B(U zTp9{`k2s+Vm78Ekm0BoXb;593p=;Rtxs<=qE&7W#uAfOtDinx0mIDR%r zr2-ZTty1V}iUtr2ba)FT$oK!Ri?hj6+lZoY;zkk})ZlJ=2{|Vdo%nc={S-(b8T1pC zprb8h5!qKV&muB%zWYk*#+g8sx)7zR_jBL9MfZ|rI!YVVu)y7DB*ngLS;n_giE$)V zj8<$k)t)44HHd|b)F>!b3f0YBwxO59OZT$7&Am+~9u$7)$EDu2T|^LL@3Na~R^LJRw>jY1j&&WmRbppq*=ljWKP01Ym7t}VyQ7YE42@z z+a%AxG~~rjjGDeuZXimGv`C~^%WnuvFFev^Jhja_hz>-kyr4Z2F^#K0~Jfo0uWwc zAbR)o9R-1X{Z8C8y)rYnG^>`P(9+`-VMR5cZkC)lcuATBUe?}4dk53J_zEO@{0jO| z;rRkPeDTrQLHx#Np-?9tIhVgd{IF=KP-nQprtMvcs)J<|@7nrN0+%~F$+DHNqfW%r z4j%#Iu%b}L!so0u%9dWTav{S`^`d<>yc_+-Dc{}xHDJSxzG`|f7iP;m1 zu%8dMbt;U=+FsxwI0|2#JFx5uGQyo&VkKtI#GzpOs>)7mXnd4XV03y1+3!V^&^;)*h?CWkWDSqg0beJHs__`@NfYi98O0kq{`J|_m`@?RhF_7r2X z80(`_r;&)3fs-5?Eg)4ap&k515}8uo_WlBV;POV!%f`Y*R;NL) z?Z;FvdQy!4RIK(@TRLsYVt;Aj5dg1tGy!=aeeH1M;EWYDxyAN^^DD6U1Aq4gV<-is14h4r$ABInJE-h5Gt-9TNp+|YkbG1DiQ2fX zu%srbuXuJltk9(@i`(gIOJ2L=;RKTFmB$gu+C%a9+EUR}CO#0ROJCJyONl>oBN^wp z;w=qtaPU3;75j^-_Xl4S%V8|d@EDb-Q@duVck=2B!5vqNg9R(`T{sy=T*UQIYMB|K z5#S1SL@79t6l;`xvN?(q61-N{7vfPGa=HG5M6w9*8@TZt4l~kHiR}BfWp_}qw*SiVqDO?VMgmfpp#PZ7!ztdC9*G{ zEelFz(5$C2`t6#5wXM9(CdA~x)Sv#>51%p=A4VzO-+ky-Q8wbZSXL(M0*LFF26Lph zHkic2F(#b|rVx8LQvx&%H|GRI4MQBbyOni(>pD&Z7|Cm&=ERJy%*=oG<@5HnKn{1I zK1O2*?Wg-(5-vf^F$s7mUc;5d+J)CI8NPkv7Y{$u(*KUv{~=ZH;o%?N`7eR + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.ttf b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5b979039ab28aaae305074541fe39258753ba624 GIT binary patch literal 202616 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%0Dsnd=jSXVj{o3VuMbQarZKj?n9Yp? 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“gU2Ebm7^-Pz)Op! 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
    Bi7ET==w0ED@om}nElpl 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 D#gX8w literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff2 b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..978a681a10ff0478581436eaca5c5695c97445d4 GIT binary patch literal 79444 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 + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    11. Collectors & Extractors

    +
    +

    11.1. miplearn.classifiers.minprob

    +
    +
    +class miplearn.classifiers.minprob.MinProbabilityClassifier(base_clf: ~typing.Any, thresholds: ~typing.List[float], clone_fn: ~typing.Callable[[~typing.Any], ~typing.Any] = <function clone>)
    +

    Bases: BaseEstimator

    +

    Meta-classifier that returns NaN for predictions made by a base classifier that +have probability below a given threshold. More specifically, this meta-classifier +calls base_clf.predict_proba and compares the result against the provided +thresholds. If the probability for one of the classes is above its threshold, +the meta-classifier returns that prediction. Otherwise, it returns NaN.

    +
    +
    +fit(x: ndarray, y: ndarray) None
    +
    + +
    +
    +predict(x: ndarray) ndarray
    +
    + +
    +
    +set_fit_request(*, x: bool | None | str = '$UNCHANGED$') MinProbabilityClassifier
    +

    Request metadata passed to the fit method.

    +

    Note that this method is only relevant if +enable_metadata_routing=True (see sklearn.set_config()). +Please see User Guide on how the routing +mechanism works.

    +

    The options for each parameter are:

    +
      +
    • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

    • +
    • False: metadata is not requested and the meta-estimator will not pass it to fit.

    • +
    • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

    • +
    • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

    • +
    +

    The default (sklearn.utils.metadata_routing.UNCHANGED) retains the +existing request. This allows you to change the request for some +parameters and not others.

    +
    +

    New in version 1.3.

    +
    +
    +

    Note

    +

    This method is only relevant if this estimator is used as a +sub-estimator of a meta-estimator, e.g. used inside a +Pipeline. Otherwise it has no effect.

    +
    +
    +
    Parameters:
    +

    x (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for x parameter in fit.

    +
    +
    Returns:
    +

    self – The updated object.

    +
    +
    Return type:
    +

    object

    +
    +
    +
    + +
    +
    +set_predict_request(*, x: bool | None | str = '$UNCHANGED$') MinProbabilityClassifier
    +

    Request metadata passed to the predict method.

    +

    Note that this method is only relevant if +enable_metadata_routing=True (see sklearn.set_config()). +Please see User Guide on how the routing +mechanism works.

    +

    The options for each parameter are:

    +
      +
    • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

    • +
    • False: metadata is not requested and the meta-estimator will not pass it to predict.

    • +
    • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

    • +
    • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

    • +
    +

    The default (sklearn.utils.metadata_routing.UNCHANGED) retains the +existing request. This allows you to change the request for some +parameters and not others.

    +
    +

    New in version 1.3.

    +
    +
    +

    Note

    +

    This method is only relevant if this estimator is used as a +sub-estimator of a meta-estimator, e.g. used inside a +Pipeline. Otherwise it has no effect.

    +
    +
    +
    Parameters:
    +

    x (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for x parameter in predict.

    +
    +
    Returns:
    +

    self – The updated object.

    +
    +
    Return type:
    +

    object

    +
    +
    +
    + +
    + +
    +
    +

    11.2. miplearn.classifiers.singleclass

    +
    +
    +class miplearn.classifiers.singleclass.SingleClassFix(base_clf: ~sklearn.base.BaseEstimator, clone_fn: ~typing.Callable = <function clone>)
    +

    Bases: BaseEstimator

    +

    Some sklearn classifiers, such as logistic regression, have issues with datasets +that contain a single class. This meta-classifier fixes the issue. If the +training data contains a single class, this meta-classifier always returns that +class as a prediction. Otherwise, it fits the provided base classifier, +and returns its predictions instead.

    +
    +
    +fit(x: ndarray, y: ndarray) None
    +
    + +
    +
    +predict(x: ndarray) ndarray
    +
    + +
    +
    +set_fit_request(*, x: bool | None | str = '$UNCHANGED$') SingleClassFix
    +

    Request metadata passed to the fit method.

    +

    Note that this method is only relevant if +enable_metadata_routing=True (see sklearn.set_config()). +Please see User Guide on how the routing +mechanism works.

    +

    The options for each parameter are:

    +
      +
    • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

    • +
    • False: metadata is not requested and the meta-estimator will not pass it to fit.

    • +
    • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

    • +
    • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

    • +
    +

    The default (sklearn.utils.metadata_routing.UNCHANGED) retains the +existing request. This allows you to change the request for some +parameters and not others.

    +
    +

    New in version 1.3.

    +
    +
    +

    Note

    +

    This method is only relevant if this estimator is used as a +sub-estimator of a meta-estimator, e.g. used inside a +Pipeline. Otherwise it has no effect.

    +
    +
    +
    Parameters:
    +

    x (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for x parameter in fit.

    +
    +
    Returns:
    +

    self – The updated object.

    +
    +
    Return type:
    +

    object

    +
    +
    +
    + +
    +
    +set_predict_request(*, x: bool | None | str = '$UNCHANGED$') SingleClassFix
    +

    Request metadata passed to the predict method.

    +

    Note that this method is only relevant if +enable_metadata_routing=True (see sklearn.set_config()). +Please see User Guide on how the routing +mechanism works.

    +

    The options for each parameter are:

    +
      +
    • True: metadata is requested, and passed to predict if provided. The request is ignored if metadata is not provided.

    • +
    • False: metadata is not requested and the meta-estimator will not pass it to predict.

    • +
    • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

    • +
    • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

    • +
    +

    The default (sklearn.utils.metadata_routing.UNCHANGED) retains the +existing request. This allows you to change the request for some +parameters and not others.

    +
    +

    New in version 1.3.

    +
    +
    +

    Note

    +

    This method is only relevant if this estimator is used as a +sub-estimator of a meta-estimator, e.g. used inside a +Pipeline. Otherwise it has no effect.

    +
    +
    +
    Parameters:
    +

    x (str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED) – Metadata routing for x parameter in predict.

    +
    +
    Returns:
    +

    self – The updated object.

    +
    +
    Return type:
    +

    object

    +
    +
    +
    + +
    + +
    +
    +

    11.3. miplearn.collectors.basic

    +
    +
    +class miplearn.collectors.basic.BasicCollector(skip_lp: bool = False, write_mps: bool = True)
    +

    Bases: object

    +
    +
    +collect(filenames: List[str], build_model: Callable, n_jobs: int = 1, progress: bool = False, verbose: bool = False) None
    +
    + +
    + +
    +
    +

    11.4. miplearn.extractors.fields

    +
    +
    +class miplearn.extractors.fields.H5FieldsExtractor(instance_fields: List[str] | None = None, var_fields: List[str] | None = None, constr_fields: List[str] | None = None)
    +

    Bases: FeaturesExtractor

    +
    +
    +get_constr_features(h5: H5File) ndarray
    +
    + +
    +
    +get_instance_features(h5: H5File) ndarray
    +
    + +
    +
    +get_var_features(h5: H5File) ndarray
    +
    + +
    + +
    +
    +

    11.5. miplearn.extractors.AlvLouWeh2017

    +
    +
    +class miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor(with_m1: bool = True, with_m2: bool = True, with_m3: bool = True)
    +

    Bases: FeaturesExtractor

    +
    +
    +get_constr_features(h5: H5File) ndarray
    +
    + +
    +
    +get_instance_features(h5: H5File) ndarray
    +
    + +
    +
    +get_var_features(h5: H5File) ndarray
    +
    +
    Computes static variable features described in:

    Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based +approximation of strong branching. INFORMS Journal on Computing, 29(1), +185-195.

    +
    +
    +
    + +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/api/components/index.html b/0.4/api/components/index.html new file mode 100644 index 0000000..ae0645f --- /dev/null +++ b/0.4/api/components/index.html @@ -0,0 +1,728 @@ + + + + + + + + 12. Components — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + +
    + +
    + Contents +
    + +
    +
    +
    +
    +
    + +
    + +
    +

    12. Components

    +
    +

    12.1. miplearn.components.primal.actions

    +
    +
    +class miplearn.components.primal.actions.EnforceProximity(tol: float)
    +

    Bases: PrimalComponentAction

    +
    +
    +perform(model: AbstractModel, var_names: ndarray, var_values: ndarray, stats: Dict | None) None
    +
    + +
    + +
    +
    +class miplearn.components.primal.actions.FixVariables
    +

    Bases: PrimalComponentAction

    +
    +
    +perform(model: AbstractModel, var_names: ndarray, var_values: ndarray, stats: Dict | None) None
    +
    + +
    + +
    +
    +class miplearn.components.primal.actions.PrimalComponentAction
    +

    Bases: ABC

    +
    +
    +abstract perform(model: AbstractModel, var_names: ndarray, var_values: ndarray, stats: Dict | None) None
    +
    + +
    + +
    +
    +class miplearn.components.primal.actions.SetWarmStart
    +

    Bases: PrimalComponentAction

    +
    +
    +perform(model: AbstractModel, var_names: ndarray, var_values: ndarray, stats: Dict | None) None
    +
    + +
    + +
    +
    +

    12.2. miplearn.components.primal.expert

    +
    +
    +class miplearn.components.primal.expert.ExpertPrimalComponent(action: PrimalComponentAction)
    +

    Bases: object

    +
    +
    +before_mip(test_h5: str, model: AbstractModel, stats: Dict[str, Any]) None
    +
    + +
    +
    +fit(train_h5: List[str]) None
    +
    + +
    + +
    +
    +

    12.3. miplearn.components.primal.indep

    +
    +
    +class miplearn.components.primal.indep.IndependentVarsPrimalComponent(base_clf: ~typing.Any, extractor: ~miplearn.extractors.abstract.FeaturesExtractor, action: ~miplearn.components.primal.actions.PrimalComponentAction, clone_fn: ~typing.Callable[[~typing.Any], ~typing.Any] = <function clone>)
    +

    Bases: object

    +
    +
    +before_mip(test_h5: str, model: AbstractModel, stats: Dict[str, Any]) None
    +
    + +
    +
    +fit(train_h5: List[str]) None
    +
    + +
    + +
    +
    +

    12.4. miplearn.components.primal.joint

    +
    +
    +class miplearn.components.primal.joint.JointVarsPrimalComponent(clf: Any, extractor: FeaturesExtractor, action: PrimalComponentAction)
    +

    Bases: object

    +
    +
    +before_mip(test_h5: str, model: AbstractModel, stats: Dict[str, Any]) None
    +
    + +
    +
    +fit(train_h5: List[str]) None
    +
    + +
    + +
    +
    +

    12.5. miplearn.components.primal.mem

    +
    +
    +class miplearn.components.primal.mem.MemorizingPrimalComponent(clf: Any, extractor: FeaturesExtractor, constructor: SolutionConstructor, action: PrimalComponentAction)
    +

    Bases: object

    +

    Component that memorizes all solutions seen during training, then fits a +single classifier to predict which of the memorized solutions should be +provided to the solver. Optionally combines multiple memorized solutions +into a single, partial one.

    +
    +
    +before_mip(test_h5: str, model: AbstractModel, stats: Dict[str, Any]) None
    +
    + +
    +
    +fit(train_h5: List[str]) None
    +
    + +
    + +
    +
    +class miplearn.components.primal.mem.MergeTopSolutions(k: int, thresholds: List[float])
    +

    Bases: SolutionConstructor

    +

    Warm start construction strategy that first selects the top k solutions, +then merges them into a single solution.

    +

    To merge the solutions, the strategy first computes the mean optimal value of each +decision variable, then: (i) sets the variable to zero if the mean is below +thresholds[0]; (ii) sets the variable to one if the mean is above thresholds[1]; +(iii) leaves the variable free otherwise.

    +
    +
    +construct(y_proba: ndarray, solutions: ndarray) ndarray
    +
    + +
    + +
    +
    +class miplearn.components.primal.mem.SelectTopSolutions(k: int)
    +

    Bases: SolutionConstructor

    +

    Warm start construction strategy that selects and returns the top k solutions.

    +
    +
    +construct(y_proba: ndarray, solutions: ndarray) ndarray
    +
    + +
    + +
    +
    +class miplearn.components.primal.mem.SolutionConstructor
    +

    Bases: ABC

    +
    +
    +abstract construct(y_proba: ndarray, solutions: ndarray) ndarray
    +
    + +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/api/helpers/index.html b/0.4/api/helpers/index.html new file mode 100644 index 0000000..3592874 --- /dev/null +++ b/0.4/api/helpers/index.html @@ -0,0 +1,473 @@ + + + + + + + + 14. Helpers — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    14. Helpers

    +
    +

    14.1. miplearn.io

    +
    +
    +miplearn.io.gzip(filename: str) None
    +
    + +
    +
    +miplearn.io.read_pkl_gz(filename: str) Any
    +
    + +
    +
    +miplearn.io.write_pkl_gz(objs: List[Any], dirname: str, prefix: str = '', n_jobs: int = 1, progress: bool = False) List[str]
    +
    + +
    +
    +

    14.2. miplearn.h5

    +
    +
    +class miplearn.h5.H5File(filename: str, mode: str = 'r+')
    +

    Bases: object

    +
    +
    +close() None
    +
    + +
    +
    +get_array(key: str) ndarray | None
    +
    + +
    +
    +get_bytes(key: str) bytes | bytearray | None
    +
    + +
    +
    +get_scalar(key: str) Any | None
    +
    + +
    +
    +get_sparse(key: str) coo_matrix | None
    +
    + +
    +
    +put_array(key: str, value: ndarray | None) None
    +
    + +
    +
    +put_bytes(key: str, value: bytes | bytearray) None
    +
    + +
    +
    +put_scalar(key: str, value: Any) None
    +
    + +
    +
    +put_sparse(key: str, value: coo_matrix) None
    +
    + +
    + +
    +
    + + +
    + + +
    + + 13. Solvers + +
    + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/api/problems/index.html b/0.4/api/problems/index.html new file mode 100644 index 0000000..439b9cb --- /dev/null +++ b/0.4/api/problems/index.html @@ -0,0 +1,783 @@ + + + + + + + + 10. Benchmark Problems — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    10. Benchmark Problems

    +
    +

    10.1. miplearn.problems.binpack

    +
    +
    +class miplearn.problems.binpack.BinPackData(sizes: ndarray, capacity: int)
    +

    Data for the bin packing problem.

    +
    +
    Parameters:
    +
      +
    • sizes (numpy.ndarray) – Sizes of the items

    • +
    • capacity (int) – Capacity of the bin

    • +
    +
    +
    +
    + +
    +
    +class miplearn.problems.binpack.BinPackGenerator(n: rv_frozen, sizes: rv_frozen, capacity: rv_frozen, sizes_jitter: rv_frozen, capacity_jitter: rv_frozen, fix_items: bool)
    +

    Random instance generator for the bin packing problem.

    +

    If fix_items=False, the class samples the user-provided probability distributions +n, sizes and capacity to decide, respectively, the number of items, the sizes of +the items and capacity of the bin. All values are sampled independently.

    +

    If fix_items=True, the class creates a reference instance, using the method +previously described, then generates additional instances by perturbing its item +sizes and bin capacity. More specifically, the sizes of the items are set to s_i +* gamma_i where s_i is the size of the i-th item in the reference instance and +gamma_i is sampled from sizes_jitter. Similarly, the bin capacity is set to B * +beta, where B is the reference bin capacity and beta is sampled from +capacity_jitter. The number of items remains the same across all generated +instances.

    +
    +
    Parameters:
    +
      +
    • n – Probability distribution for the number of items.

    • +
    • sizes – Probability distribution for the item sizes.

    • +
    • capacity – Probability distribution for the bin capacity.

    • +
    • sizes_jitter – Probability distribution for the item size randomization.

    • +
    • capacity_jitter – Probability distribution for the bin capacity.

    • +
    • fix_items – If True, generates a reference instance, then applies some perturbation to it. +If False, generates completely different instances.

    • +
    +
    +
    +
    +
    +generate(n_samples: int) List[BinPackData]
    +

    Generates random instances.

    +
    +
    Parameters:
    +

    n_samples – Number of samples to generate.

    +
    +
    +
    + +
    + +
    +
    +miplearn.problems.binpack.build_binpack_model_gurobipy(data: str | BinPackData) GurobiModel
    +

    Converts bin packing problem data into a concrete Gurobipy model.

    +
    + +
    +
    +

    10.2. miplearn.problems.multiknapsack

    +
    +
    +class miplearn.problems.multiknapsack.MultiKnapsackData(prices: ndarray, capacities: ndarray, weights: ndarray)
    +

    Data for the multi-dimensional knapsack problem

    +
    +
    Parameters:
    +
      +
    • prices (numpy.ndarray) – Item prices.

    • +
    • capacities (numpy.ndarray) – Knapsack capacities.

    • +
    • weights (numpy.ndarray) – Matrix of item weights.

    • +
    +
    +
    +
    + +
    +
    +class miplearn.problems.multiknapsack.MultiKnapsackGenerator(n: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, m: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, w: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, K: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, u: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, alpha: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, fix_w: bool = False, w_jitter: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, p_jitter: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, round: bool = True)
    +

    Random instance generator for the multi-dimensional knapsack problem.

    +

    Instances have a random number of items (or variables) and a random number of +knapsacks (or constraints), as specified by the provided probability +distributions n and m, respectively. The weight of each item i on knapsack +j is sampled independently from the provided distribution w. The capacity of +knapsack j is set to alpha_j * sum(w[i,j] for i in range(n)), +where alpha_j, the tightness ratio, is sampled from the provided probability +distribution alpha.

    +

    To make the instances more challenging, the costs of the items are linearly +correlated to their average weights. More specifically, the weight of each item +i is set to sum(w[i,j]/m for j in range(m)) + K * u_i, where K, +the correlation coefficient, and u_i, the correlation multiplier, are sampled +from the provided probability distributions. Note that K is only sample once +for the entire instance.

    +

    If fix_w=True, then weights[i,j] are kept the same in all generated +instances. This also implies that n and m are kept fixed. Although the prices and +capacities are derived from weights[i,j], as long as u and K are not +constants, the generated instances will still not be completely identical.

    +

    If a probability distribution w_jitter is provided, then item weights will be +set to w[i,j] * gamma[i,j] where gamma[i,j] is sampled from w_jitter. +When combined with fix_w=True, this argument may be used to generate instances +where the weight of each item is roughly the same, but not exactly identical, +across all instances. The prices of the items and the capacities of the knapsacks +will be calculated as above, but using these perturbed weights instead.

    +

    By default, all generated prices, weights and capacities are rounded to the +nearest integer number. If round=False is provided, this rounding will be +disabled.

    +
    +
    Parameters:
    +
      +
    • n (rv_discrete) – Probability distribution for the number of items (or variables).

    • +
    • m (rv_discrete) – Probability distribution for the number of knapsacks (or constraints).

    • +
    • w (rv_continuous) – Probability distribution for the item weights.

    • +
    • K (rv_continuous) – Probability distribution for the profit correlation coefficient.

    • +
    • u (rv_continuous) – Probability distribution for the profit multiplier.

    • +
    • alpha (rv_continuous) – Probability distribution for the tightness ratio.

    • +
    • fix_w (boolean) – If true, weights are kept the same (minus the noise from w_jitter) in all +instances.

    • +
    • w_jitter (rv_continuous) – Probability distribution for random noise added to the weights.

    • +
    • round (boolean) – If true, all prices, weights and capacities are rounded to the nearest +integer.

    • +
    +
    +
    +
    + +
    +
    +miplearn.problems.multiknapsack.build_multiknapsack_model_gurobipy(data: str | MultiKnapsackData) GurobiModel
    +

    Converts multi-knapsack problem data into a concrete Gurobipy model.

    +
    + +
    +
    +

    10.3. miplearn.problems.pmedian

    +
    +
    +class miplearn.problems.pmedian.PMedianData(distances: ndarray, demands: ndarray, p: int, capacities: ndarray)
    +

    Data for the capacitated p-median problem

    +
    +
    Parameters:
    +
      +
    • distances (numpy.ndarray) – Matrix of distances between customer i and facility j.

    • +
    • demands (numpy.ndarray) – Customer demands.

    • +
    • p (int) – Number of medians that need to be chosen.

    • +
    • capacities (numpy.ndarray) – Facility capacities.

    • +
    +
    +
    +
    + +
    +
    +class miplearn.problems.pmedian.PMedianGenerator(x: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, y: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, n: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, p: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, demands: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, capacities: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, distances_jitter: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, demands_jitter: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, capacities_jitter: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, fixed: bool = True)
    +

    Random generator for the capacitated p-median problem.

    +

    This class first decides the number of customers and the parameter p by +sampling the provided n and p distributions, respectively. Then, for each +customer i, the class builds its geographical location (xi, yi) by sampling +the provided x and y distributions. For each i, the demand for customer i +and the capacity of facility i are decided by sampling the distributions +demands and capacities, respectively. Finally, the costs w[i,j] are set to +the Euclidean distance between the locations of customers i and j.

    +

    If fixed=True, then the number of customers, their locations, the parameter +p, the demands and the capacities are only sampled from their respective +distributions exactly once, to build a reference instance which is then +perturbed. Specifically, for each perturbation, the distances, demands and +capacities are multiplied by factors sampled from the distributions +distances_jitter, demands_jitter and capacities_jitter, respectively. The +result is a list of instances that have the same set of customers, but slightly +different demands, capacities and distances.

    +
    +
    Parameters:
    +
      +
    • x – Probability distribution for the x-coordinate of the points.

    • +
    • y – Probability distribution for the y-coordinate of the points.

    • +
    • n – Probability distribution for the number of customer.

    • +
    • p – Probability distribution for the number of medians.

    • +
    • demands – Probability distribution for the customer demands.

    • +
    • capacities – Probability distribution for the facility capacities.

    • +
    • distances_jitter – Probability distribution for the random scaling factor applied to distances.

    • +
    • demands_jitter – Probability distribution for the random scaling factor applied to demands.

    • +
    • capacities_jitter – Probability distribution for the random scaling factor applied to capacities.

    • +
    • fixed – If True, then customer are kept the same across instances.

    • +
    +
    +
    +
    + +
    +
    +miplearn.problems.pmedian.build_pmedian_model_gurobipy(data: str | PMedianData) GurobiModel
    +

    Converts capacitated p-median data into a concrete Gurobipy model.

    +
    + +
    +
    +

    10.4. miplearn.problems.setcover

    +
    +
    +class miplearn.problems.setcover.SetCoverData(costs: numpy.ndarray, incidence_matrix: numpy.ndarray)
    +
    + +
    +
    +

    10.5. miplearn.problems.setpack

    +
    +
    +class miplearn.problems.setpack.SetPackData(costs: numpy.ndarray, incidence_matrix: numpy.ndarray)
    +
    + +
    +
    +

    10.6. miplearn.problems.stab

    +
    +
    +class miplearn.problems.stab.MaxWeightStableSetData(graph: networkx.classes.graph.Graph, weights: numpy.ndarray)
    +
    + +
    +
    +class miplearn.problems.stab.MaxWeightStableSetGenerator(w: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, n: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, p: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, fix_graph: bool = True)
    +

    Random instance generator for the Maximum-Weight Stable Set Problem.

    +

    The generator has two modes of operation. When fix_graph=True is provided, +one random Erdős-Rényi graph $G_{n,p}$ is generated in the constructor, where $n$ +and $p$ are sampled from user-provided probability distributions n and p. To +generate each instance, the generator independently samples each $w_v$ from the +user-provided probability distribution w.

    +

    When fix_graph=False, a new random graph is generated for each instance; the +remaining parameters are sampled in the same way.

    +
    + +
    +
    +

    10.7. miplearn.problems.tsp

    +
    +
    +class miplearn.problems.tsp.TravelingSalesmanData(n_cities: int, distances: numpy.ndarray)
    +
    + +
    +
    +class miplearn.problems.tsp.TravelingSalesmanGenerator(x: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, y: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, n: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_discrete_frozen object>, gamma: ~scipy.stats._distn_infrastructure.rv_frozen = <scipy.stats._distn_infrastructure.rv_continuous_frozen object>, fix_cities: bool = True, round: bool = True)
    +

    Random generator for the Traveling Salesman Problem.

    +
    + +
    +
    +

    10.8. miplearn.problems.uc

    +
    +
    +class miplearn.problems.uc.UnitCommitmentData(demand: numpy.ndarray, min_power: numpy.ndarray, max_power: numpy.ndarray, min_uptime: numpy.ndarray, min_downtime: numpy.ndarray, cost_startup: numpy.ndarray, cost_prod: numpy.ndarray, cost_fixed: numpy.ndarray)
    +
    + +
    +
    +miplearn.problems.uc.build_uc_model_gurobipy(data: str | UnitCommitmentData) GurobiModel
    +

    Models the unit commitment problem according to equations (1)-(5) of:

    +
    +

    Bendotti, P., Fouilhoux, P. & Rottner, C. The min-up/min-down unit +commitment polytope. J Comb Optim 36, 1024-1058 (2018). +https://doi.org/10.1007/s10878-018-0273-y

    +
    +
    + +
    +
    +

    10.9. miplearn.problems.vertexcover

    +
    +
    +class miplearn.problems.vertexcover.MinWeightVertexCoverData(graph: networkx.classes.graph.Graph, weights: numpy.ndarray)
    +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/api/solvers/index.html b/0.4/api/solvers/index.html new file mode 100644 index 0000000..8d8efa1 --- /dev/null +++ b/0.4/api/solvers/index.html @@ -0,0 +1,736 @@ + + + + + + + + 13. Solvers — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    13. Solvers

    +
    +

    13.1. miplearn.solvers.abstract

    +
    +
    +class miplearn.solvers.abstract.AbstractModel
    +

    Bases: ABC

    +
    +
    +WHERE_CUTS = 'cuts'
    +
    + +
    +
    +WHERE_DEFAULT = 'default'
    +
    + +
    +
    +WHERE_LAZY = 'lazy'
    +
    + +
    +
    +abstract add_constrs(var_names: ndarray, constrs_lhs: ndarray, constrs_sense: ndarray, constrs_rhs: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +abstract extract_after_load(h5: H5File) None
    +
    + +
    +
    +abstract extract_after_lp(h5: H5File) None
    +
    + +
    +
    +abstract extract_after_mip(h5: H5File) None
    +
    + +
    +
    +abstract fix_variables(var_names: ndarray, var_values: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +lazy_enforce(violations: List[Any]) None
    +
    + +
    +
    +abstract optimize() None
    +
    + +
    +
    +abstract relax() AbstractModel
    +
    + +
    +
    +set_cuts(cuts: List) None
    +
    + +
    +
    +abstract set_warm_starts(var_names: ndarray, var_values: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +abstract write(filename: str) None
    +
    + +
    + +
    +
    +

    13.2. miplearn.solvers.gurobi

    +
    +
    +class miplearn.solvers.gurobi.GurobiModel(inner: Model, lazy_separate: Callable | None = None, lazy_enforce: Callable | None = None, cuts_separate: Callable | None = None, cuts_enforce: Callable | None = None)
    +

    Bases: AbstractModel

    +
    +
    +add_constr(constr: Any) None
    +
    + +
    +
    +add_constrs(var_names: ndarray, constrs_lhs: ndarray, constrs_sense: ndarray, constrs_rhs: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +extract_after_load(h5: H5File) None
    +

    Given a model that has just been loaded, extracts static problem +features, such as variable names and types, objective coefficients, etc.

    +
    + +
    +
    +extract_after_lp(h5: H5File) None
    +

    Given a linear programming model that has just been solved, extracts +dynamic problem features, such as optimal LP solution, basis status, +etc.

    +
    + +
    +
    +extract_after_mip(h5: H5File) None
    +

    Given a mixed-integer linear programming model that has just been +solved, extracts dynamic problem features, such as optimal MIP solution.

    +
    + +
    +
    +fix_variables(var_names: ndarray, var_values: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +optimize() None
    +
    + +
    +
    +relax() GurobiModel
    +
    + +
    +
    +set_time_limit(time_limit_sec: float) None
    +
    + +
    +
    +set_warm_starts(var_names: ndarray, var_values: ndarray, stats: Dict | None = None) None
    +
    + +
    +
    +write(filename: str) None
    +
    + +
    + +
    +
    +

    13.3. miplearn.solvers.learning

    +
    +
    +class miplearn.solvers.learning.LearningSolver(components: List[Any], skip_lp: bool = False)
    +

    Bases: object

    +
    +
    +fit(data_filenames: List[str]) None
    +
    + +
    +
    +optimize(model: str | AbstractModel, build_model: Callable | None = None) Dict[str, Any]
    +
    + +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/genindex/index.html b/0.4/genindex/index.html new file mode 100644 index 0000000..5b2d940 --- /dev/null +++ b/0.4/genindex/index.html @@ -0,0 +1,872 @@ + + + + + + + Index — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
    +
    + +
    + + +

    Index

    + +
    + A + | B + | C + | E + | F + | G + | H + | I + | J + | L + | M + | O + | P + | R + | S + | T + | U + | W + +
    +

    A

    + + + +
    + +

    B

    + + + +
    + +

    C

    + + + +
    + +

    E

    + + + +
    + +

    F

    + + + +
    + +

    G

    + + + +
    + +

    H

    + + + +
    + +

    I

    + + +
    + +

    J

    + + +
    + +

    L

    + + + +
    + +

    M

    + + + +
    + +

    O

    + + +
    + +

    P

    + + + +
    + +

    R

    + + + +
    + +

    S

    + + + +
    + +

    T

    + + + +
    + +

    U

    + + +
    + +

    W

    + + + +
    + + + +
    + + +
    + + +
    + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/guide/collectors.ipynb b/0.4/guide/collectors.ipynb new file mode 100644 index 0000000..443802e --- /dev/null +++ b/0.4/guide/collectors.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "505cea0b-5f5d-478a-9107-42bb5515937d", + "metadata": {}, + "source": [ + "# Training Data Collectors\n", + "The first step in solving mixed-integer optimization problems with the assistance of supervised machine learning methods is solving a large set of training instances and collecting the raw training data. In this section, we describe the various training data collectors included in MIPLearn. Additionally, the framework follows the convention of storing all training data in files with a specific data format (namely, HDF5). In this section, we briefly describe this format and the rationale for choosing it.\n", + "\n", + "## Overview\n", + "\n", + "In MIPLearn, a **collector** is a class that solves or analyzes the problem and collects raw data which may be later useful for machine learning methods. Collectors, by convention, take as input: (i) a list of problem data filenames, in gzipped pickle format, ending with `.pkl.gz`; (ii) a function that builds the optimization model, such as `build_tsp_model`. After processing is done, collectors store the training data in a HDF5 file located alongside with the problem data. For example, if the problem data is stored in file `problem.pkl.gz`, then the collector writes to `problem.h5`. Collectors are, in general, very time consuming, as they may need to solve the problem to optimality, potentially multiple times.\n", + "\n", + "## HDF5 Format\n", + "\n", + "MIPLearn stores all training data in [HDF5](HDF5) (Hierarchical Data Format, Version 5) files. The HDF format was originally developed by the [National Center for Supercomputing Applications][NCSA] (NCSA) for storing and organizing large amounts of data, and supports a variety of data types, including integers, floating-point numbers, strings, and arrays. Compared to other formats, such as CSV, JSON or SQLite, the HDF5 format provides several advantages for MIPLearn, including:\n", + "\n", + "- *Storage of multiple scalars, vectors and matrices in a single file* --- This allows MIPLearn to store all training data related to a given problem instance in a single file, which makes training data easier to store, organize and transfer.\n", + "- *High-performance partial I/O* --- Partial I/O allows MIPLearn to read a single element from the training data (e.g. value of the optimal solution) without loading the entire file to memory or reading it from beginning to end, which dramatically improves performance and reduces memory requirements. This is especially important when processing a large number of training data files.\n", + "- *On-the-fly compression* --- HDF5 files can be transparently compressed, using the gzip method, which reduces storage requirements and accelerates network transfers.\n", + "- *Stable, portable and well-supported data format* --- Training data files are typically expensive to generate. Having a stable and well supported data format ensures that these files remain usable in the future, potentially even by other non-Python MIP/ML frameworks.\n", + "\n", + "MIPLearn currently uses HDF5 as simple key-value storage for numerical data; more advanced features of the format, such as metadata, are not currently used. Although files generated by MIPLearn can be read with any HDF5 library, such as [h5py][h5py], some convenience functions are provided to make the access more simple and less error-prone. Specifically, the class [H5File][H5File], which is built on top of h5py, provides the methods [put_scalar][put_scalar], [put_array][put_array], [put_sparse][put_sparse], [put_bytes][put_bytes] to store, respectively, scalar values, dense multi-dimensional arrays, sparse multi-dimensional arrays and arbitrary binary data. The corresponding *get* methods are also provided. Compared to pure h5py methods, these methods automatically perform type-checking and gzip compression. The example below shows their usage.\n", + "\n", + "[HDF5]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format\n", + "[NCSA]: https://en.wikipedia.org/wiki/National_Center_for_Supercomputing_Applications\n", + "[h5py]: https://www.h5py.org/\n", + "[H5File]: ../../api/helpers/#miplearn.h5.H5File\n", + "[put_scalar]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_array]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_sparse]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "[put_bytes]: ../../api/helpers/#miplearn.h5.H5File.put_scalar\n", + "\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f906fe9c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-30T22:19:30.826123021Z", + "start_time": "2024-01-30T22:19:30.766066926Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "x1 = 1\n", + "x2 = hello world\n", + "x3 = [1 2 3]\n", + "x4 = [[0.37454012 0.9507143 0.7319939 ]\n", + " [0.5986585 0.15601864 0.15599452]\n", + " [0.05808361 0.8661761 0.601115 ]]\n", + "x5 = (3, 2)\t0.6803075671195984\n", + " (2, 3)\t0.4504992663860321\n", + " (0, 4)\t0.013264961540699005\n", + " (2, 0)\t0.9422017335891724\n", + " (2, 4)\t0.5632882118225098\n", + " (1, 2)\t0.38541650772094727\n", + " (1, 1)\t0.015966251492500305\n", + " (0, 3)\t0.2308938205242157\n", + " (4, 4)\t0.24102546274662018\n", + " (3, 1)\t0.6832635402679443\n", + " (1, 3)\t0.6099966764450073\n", + " (3, 0)\t0.83319491147995\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import scipy.sparse\n", + "\n", + "from miplearn.h5 import H5File\n", + "\n", + "# Set random seed to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Create a new empty HDF5 file\n", + "with H5File(\"test.h5\", \"w\") as h5:\n", + " # Store a scalar\n", + " h5.put_scalar(\"x1\", 1)\n", + " h5.put_scalar(\"x2\", \"hello world\")\n", + "\n", + " # Store a dense array and a dense matrix\n", + " h5.put_array(\"x3\", np.array([1, 2, 3]))\n", + " h5.put_array(\"x4\", np.random.rand(3, 3))\n", + "\n", + " # Store a sparse matrix\n", + " h5.put_sparse(\"x5\", scipy.sparse.random(5, 5, 0.5))\n", + "\n", + "# Re-open the file we just created and print\n", + "# previously-stored data\n", + "with H5File(\"test.h5\", \"r\") as h5:\n", + " print(\"x1 =\", h5.get_scalar(\"x1\"))\n", + " print(\"x2 =\", h5.get_scalar(\"x2\"))\n", + " print(\"x3 =\", h5.get_array(\"x3\"))\n", + " print(\"x4 =\", h5.get_array(\"x4\"))\n", + " print(\"x5 =\", h5.get_sparse(\"x5\"))" + ] + }, + { + "cell_type": "markdown", + "id": "50441907", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "d0000c8d", + "metadata": {}, + "source": [ + "## Basic collector\n", + "\n", + "[BasicCollector][BasicCollector] is the most fundamental collector, and performs the following steps:\n", + "\n", + "1. Extracts all model data, such as objective function and constraint right-hand sides into numpy arrays, which can later be easily and efficiently accessed without rebuilding the model or invoking the solver;\n", + "2. Solves the linear relaxation of the problem and stores its optimal solution, basis status and sensitivity information, among other information;\n", + "3. Solves the original mixed-integer optimization problem to optimality and stores its optimal solution, along with solve statistics, such as number of explored nodes and wallclock time.\n", + "\n", + "Data extracted in Phases 1, 2 and 3 above are prefixed, respectively as `static_`, `lp_` and `mip_`. The entire set of fields is shown in the table below.\n", + "\n", + "[BasicCollector]: ../../api/collectors/#miplearn.collectors.basic.BasicCollector\n" + ] + }, + { + "cell_type": "markdown", + "id": "6529f667", + "metadata": {}, + "source": [ + "### Data fields\n", + "\n", + "| Field | Type | Description |\n", + "|-----------------------------------|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------|\n", + "| `static_constr_lhs` | `(nconstrs, nvars)` | Constraint left-hand sides, in sparse matrix format |\n", + "| `static_constr_names` | `(nconstrs,)` | Constraint names |\n", + "| `static_constr_rhs` | `(nconstrs,)` | Constraint right-hand sides |\n", + "| `static_constr_sense` | `(nconstrs,)` | Constraint senses (`\"<\"`, `\">\"` or `\"=\"`) |\n", + "| `static_obj_offset` | `float` | Constant value added to the objective function |\n", + "| `static_sense` | `str` | `\"min\"` if minimization problem or `\"max\"` otherwise |\n", + "| `static_var_lower_bounds` | `(nvars,)` | Variable lower bounds |\n", + "| `static_var_names` | `(nvars,)` | Variable names |\n", + "| `static_var_obj_coeffs` | `(nvars,)` | Objective coefficients |\n", + "| `static_var_types` | `(nvars,)` | Types of the decision variables (`\"C\"`, `\"B\"` and `\"I\"` for continuous, binary and integer, respectively) |\n", + "| `static_var_upper_bounds` | `(nvars,)` | Variable upper bounds |\n", + "| `lp_constr_basis_status` | `(nconstr,)` | Constraint basis status (`0` for basic, `-1` for non-basic) |\n", + "| `lp_constr_dual_values` | `(nconstr,)` | Constraint dual value (or shadow price) |\n", + "| `lp_constr_sa_rhs_{up,down}` | `(nconstr,)` | Sensitivity information for the constraint RHS |\n", + "| `lp_constr_slacks` | `(nconstr,)` | Constraint slack in the solution to the LP relaxation |\n", + "| `lp_obj_value` | `float` | Optimal value of the LP relaxation |\n", + "| `lp_var_basis_status` | `(nvars,)` | Variable basis status (`0`, `-1`, `-2` or `-3` for basic, non-basic at lower bound, non-basic at upper bound, and superbasic, respectively) |\n", + "| `lp_var_reduced_costs` | `(nvars,)` | Variable reduced costs |\n", + "| `lp_var_sa_{obj,ub,lb}_{up,down}` | `(nvars,)` | Sensitivity information for the variable objective coefficient, lower and upper bound. |\n", + "| `lp_var_values` | `(nvars,)` | Optimal solution to the LP relaxation |\n", + "| `lp_wallclock_time` | `float` | Time taken to solve the LP relaxation (in seconds) |\n", + "| `mip_constr_slacks` | `(nconstrs,)` | Constraint slacks in the best MIP solution |\n", + "| `mip_gap` | `float` | Relative MIP optimality gap |\n", + "| `mip_node_count` | `float` | Number of explored branch-and-bound nodes |\n", + "| `mip_obj_bound` | `float` | Dual bound |\n", + "| `mip_obj_value` | `float` | Value of the best MIP solution |\n", + "| `mip_var_values` | `(nvars,)` | Best MIP solution |\n", + "| `mip_wallclock_time` | `float` | Time taken to solve the MIP (in seconds) |" + ] + }, + { + "cell_type": "markdown", + "id": "f2894594", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "The example below shows how to generate a few random instances of the traveling salesman problem, store its problem data, run the collector and print some of the training data to screen." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ac6f8c6f", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-30T22:19:30.826707866Z", + "start_time": "2024-01-30T22:19:30.825940503Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "lp_obj_value = 2909.0\n", + "mip_obj_value = 2921.0\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from glob import glob\n", + "\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.h5 import H5File\n", + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "# Set random seed to make example reproducible.\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a few instances of the traveling salesman problem.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(10)\n", + "\n", + "# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...\n", + "write_pkl_gz(data, \"data/tsp\")\n", + "\n", + "# Solve all instances and collect basic solution information.\n", + "# Process at most four instances in parallel.\n", + "bc = BasicCollector()\n", + "bc.collect(glob(\"data/tsp/*.pkl.gz\"), build_tsp_model_gurobipy, n_jobs=4)\n", + "\n", + "# Read and print some training data for the first instance.\n", + "with H5File(\"data/tsp/00000.h5\", \"r\") as h5:\n", + " print(\"lp_obj_value = \", h5.get_scalar(\"lp_obj_value\"))\n", + " print(\"mip_obj_value = \", h5.get_scalar(\"mip_obj_value\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78f0b07a", + "metadata": { + "ExecuteTime": { + "start_time": "2024-01-30T22:19:30.826179789Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/guide/collectors/index.html b/0.4/guide/collectors/index.html new file mode 100644 index 0000000..443f6f5 --- /dev/null +++ b/0.4/guide/collectors/index.html @@ -0,0 +1,595 @@ + + + + + + + + 6. Training Data Collectors — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    6. Training Data Collectors

    +

    The first step in solving mixed-integer optimization problems with the assistance of supervised machine learning methods is solving a large set of training instances and collecting the raw training data. In this section, we describe the various training data collectors included in MIPLearn. Additionally, the framework follows the convention of storing all training data in files with a specific data format (namely, HDF5). In this section, we briefly describe this format and the rationale for +choosing it.

    +
    +

    6.1. Overview

    +

    In MIPLearn, a collector is a class that solves or analyzes the problem and collects raw data which may be later useful for machine learning methods. Collectors, by convention, take as input: (i) a list of problem data filenames, in gzipped pickle format, ending with .pkl.gz; (ii) a function that builds the optimization model, such as build_tsp_model. After processing is done, collectors store the training data in a HDF5 file located alongside with the problem data. For example, if +the problem data is stored in file problem.pkl.gz, then the collector writes to problem.h5. Collectors are, in general, very time consuming, as they may need to solve the problem to optimality, potentially multiple times.

    +
    +
    +

    6.2. HDF5 Format

    +

    MIPLearn stores all training data in HDF5 (Hierarchical Data Format, Version 5) files. The HDF format was originally developed by the National Center for Supercomputing Applications (NCSA) for storing and organizing large amounts of data, and supports a variety of data types, including integers, floating-point numbers, strings, and arrays. Compared to other formats, such as CSV, JSON or SQLite, the +HDF5 format provides several advantages for MIPLearn, including:

    +
      +
    • Storage of multiple scalars, vectors and matrices in a single file — This allows MIPLearn to store all training data related to a given problem instance in a single file, which makes training data easier to store, organize and transfer.

    • +
    • High-performance partial I/O — Partial I/O allows MIPLearn to read a single element from the training data (e.g. value of the optimal solution) without loading the entire file to memory or reading it from beginning to end, which dramatically improves performance and reduces memory requirements. This is especially important when processing a large number of training data files.

    • +
    • On-the-fly compression — HDF5 files can be transparently compressed, using the gzip method, which reduces storage requirements and accelerates network transfers.

    • +
    • Stable, portable and well-supported data format — Training data files are typically expensive to generate. Having a stable and well supported data format ensures that these files remain usable in the future, potentially even by other non-Python MIP/ML frameworks.

    • +
    +

    MIPLearn currently uses HDF5 as simple key-value storage for numerical data; more advanced features of the format, such as metadata, are not currently used. Although files generated by MIPLearn can be read with any HDF5 library, such as h5py, some convenience functions are provided to make the access more simple and less error-prone. Specifically, the class H5File, which is built on top of h5py, provides the methods +put_scalar, put_array, put_sparse, put_bytes to store, respectively, scalar values, dense multi-dimensional arrays, sparse multi-dimensional arrays and arbitrary binary data. The corresponding get methods are also provided. Compared to pure h5py methods, these methods +automatically perform type-checking and gzip compression. The example below shows their usage.

    +
    +

    Example

    +
    +
    [1]:
    +
    +
    +
    import numpy as np
    +import scipy.sparse
    +
    +from miplearn.h5 import H5File
    +
    +# Set random seed to make example reproducible
    +np.random.seed(42)
    +
    +# Create a new empty HDF5 file
    +with H5File("test.h5", "w") as h5:
    +    # Store a scalar
    +    h5.put_scalar("x1", 1)
    +    h5.put_scalar("x2", "hello world")
    +
    +    # Store a dense array and a dense matrix
    +    h5.put_array("x3", np.array([1, 2, 3]))
    +    h5.put_array("x4", np.random.rand(3, 3))
    +
    +    # Store a sparse matrix
    +    h5.put_sparse("x5", scipy.sparse.random(5, 5, 0.5))
    +
    +# Re-open the file we just created and print
    +# previously-stored data
    +with H5File("test.h5", "r") as h5:
    +    print("x1 =", h5.get_scalar("x1"))
    +    print("x2 =", h5.get_scalar("x2"))
    +    print("x3 =", h5.get_array("x3"))
    +    print("x4 =", h5.get_array("x4"))
    +    print("x5 =", h5.get_sparse("x5"))
    +
    +
    +
    +
    +
    +
    +
    +
    +x1 = 1
    +x2 = hello world
    +x3 = [1 2 3]
    +x4 = [[0.37454012 0.9507143  0.7319939 ]
    + [0.5986585  0.15601864 0.15599452]
    + [0.05808361 0.8661761  0.601115  ]]
    +x5 =   (3, 2)   0.6803075671195984
    +  (2, 3)        0.4504992663860321
    +  (0, 4)        0.013264961540699005
    +  (2, 0)        0.9422017335891724
    +  (2, 4)        0.5632882118225098
    +  (1, 2)        0.38541650772094727
    +  (1, 1)        0.015966251492500305
    +  (0, 3)        0.2308938205242157
    +  (4, 4)        0.24102546274662018
    +  (3, 1)        0.6832635402679443
    +  (1, 3)        0.6099966764450073
    +  (3, 0)        0.83319491147995
    +
    +
    +
    +
    +
    +

    6.3. Basic collector

    +

    BasicCollector is the most fundamental collector, and performs the following steps:

    +
      +
    1. Extracts all model data, such as objective function and constraint right-hand sides into numpy arrays, which can later be easily and efficiently accessed without rebuilding the model or invoking the solver;

    2. +
    3. Solves the linear relaxation of the problem and stores its optimal solution, basis status and sensitivity information, among other information;

    4. +
    5. Solves the original mixed-integer optimization problem to optimality and stores its optimal solution, along with solve statistics, such as number of explored nodes and wallclock time.

    6. +
    +

    Data extracted in Phases 1, 2 and 3 above are prefixed, respectively as static_, lp_ and mip_. The entire set of fields is shown in the table below.

    +
    +

    Data fields

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Field

    Type

    Description

    static_constr_lhs

    (nconstrs, nvars)

    Constraint left-hand sides, in sparse matrix format

    static_constr_names

    (nconstrs,)

    Constraint names

    static_constr_rhs

    (nconstrs,)

    Constraint right-hand sides

    static_constr_sense

    (nconstrs,)

    Constraint senses ("<", ">" or "=")

    static_obj_offset

    float

    Constant value added to the objective function

    static_sense

    str

    "min" if minimization problem or "max" otherwise

    static_var_lower_bounds

    (nvars,)

    Variable lower bounds

    static_var_names

    (nvars,)

    Variable names

    static_var_obj_coeffs

    (nvars,)

    Objective coefficients

    static_var_types

    (nvars,)

    Types of the decision variables ("C", "B" and "I" for continuous, binary and integer, respectively)

    static_var_upper_bounds

    (nvars,)

    Variable upper bounds

    lp_constr_basis_status

    (nconstr,)

    Constraint basis status (0 for basic, -1 for non-basic)

    lp_constr_dual_values

    (nconstr,)

    Constraint dual value (or shadow price)

    lp_constr_sa_rhs_{up,down}

    (nconstr,)

    Sensitivity information for the constraint RHS

    lp_constr_slacks

    (nconstr,)

    Constraint slack in the solution to the LP relaxation

    lp_obj_value

    float

    Optimal value of the LP relaxation

    lp_var_basis_status

    (nvars,)

    Variable basis status (0, -1, -2 or -3 for basic, non-basic at lower bound, non-basic at upper bound, and superbasic, respectively)

    lp_var_reduced_costs

    (nvars,)

    Variable reduced costs

    lp_var_sa_{obj,ub,lb}_{up,down}

    (nvars,)

    Sensitivity information for the variable objective coefficient, lower and upper bound.

    lp_var_values

    (nvars,)

    Optimal solution to the LP relaxation

    lp_wallclock_time

    float

    Time taken to solve the LP relaxation (in seconds)

    mip_constr_slacks

    (nconstrs,)

    Constraint slacks in the best MIP solution

    mip_gap

    float

    Relative MIP optimality gap

    mip_node_count

    float

    Number of explored branch-and-bound nodes

    mip_obj_bound

    float

    Dual bound

    mip_obj_value

    float

    Value of the best MIP solution

    mip_var_values

    (nvars,)

    Best MIP solution

    mip_wallclock_time

    float

    Time taken to solve the MIP (in seconds)

    +
    +
    +

    Example

    +

    The example below shows how to generate a few random instances of the traveling salesman problem, store its problem data, run the collector and print some of the training data to screen.

    +
    +
    [2]:
    +
    +
    +
    import random
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from glob import glob
    +
    +from miplearn.problems.tsp import (
    +    TravelingSalesmanGenerator,
    +    build_tsp_model_gurobipy,
    +)
    +from miplearn.io import write_pkl_gz
    +from miplearn.h5 import H5File
    +from miplearn.collectors.basic import BasicCollector
    +
    +# Set random seed to make example reproducible.
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate a few instances of the traveling salesman problem.
    +data = TravelingSalesmanGenerator(
    +    n=randint(low=10, high=11),
    +    x=uniform(loc=0.0, scale=1000.0),
    +    y=uniform(loc=0.0, scale=1000.0),
    +    gamma=uniform(loc=0.90, scale=0.20),
    +    fix_cities=True,
    +    round=True,
    +).generate(10)
    +
    +# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...
    +write_pkl_gz(data, "data/tsp")
    +
    +# Solve all instances and collect basic solution information.
    +# Process at most four instances in parallel.
    +bc = BasicCollector()
    +bc.collect(glob("data/tsp/*.pkl.gz"), build_tsp_model_gurobipy, n_jobs=4)
    +
    +# Read and print some training data for the first instance.
    +with H5File("data/tsp/00000.h5", "r") as h5:
    +    print("lp_obj_value = ", h5.get_scalar("lp_obj_value"))
    +    print("mip_obj_value = ", h5.get_scalar("mip_obj_value"))
    +
    +
    +
    +
    +
    +
    +
    +
    +lp_obj_value =  2909.0
    +mip_obj_value =  2921.0
    +
    +
    +
    +
    [ ]:
    +
    +
    +
    
    +
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/guide/features.ipynb b/0.4/guide/features.ipynb new file mode 100644 index 0000000..495e8ea --- /dev/null +++ b/0.4/guide/features.ipynb @@ -0,0 +1,334 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cdc6ebe9-d1d4-4de1-9b5a-4fc8ef57b11b", + "metadata": {}, + "source": [ + "# Feature Extractors\n", + "\n", + "In the previous page, we introduced *training data collectors*, which solve the optimization problem and collect raw training data, such as the optimal solution. In this page, we introduce **feature extractors**, which take the raw training data, stored in HDF5 files, and extract relevant information in order to train a machine learning model." + ] + }, + { + "cell_type": "markdown", + "id": "b4026de5", + "metadata": {}, + "source": [ + "\n", + "## Overview\n", + "\n", + "Feature extraction is an important step of the process of building a machine learning model because it helps to reduce the complexity of the data and convert it into a format that is more easily processed. Previous research has proposed converting absolute variable coefficients, for example, into relative values which are invariant to various transformations, such as problem scaling, making them more amenable to learning. Various other transformations have also been described.\n", + "\n", + "In the framework, we treat data collection and feature extraction as two separate steps to accelerate the model development cycle. Specifically, collectors are typically time-consuming, as they often need to solve the problem to optimality, and therefore focus on collecting and storing all data that may or may not be relevant, in its raw format. Feature extractors, on the other hand, focus entirely on filtering the data and improving its representation, and are therefore much faster to run. Experimenting with new data representations, therefore, can be done without resolving the instances.\n", + "\n", + "In MIPLearn, extractors implement the abstract class [FeatureExtractor][FeatureExtractor], which has methods that take as input an [H5File][H5File] and produce either: (i) instance features, which describe the entire instances; (ii) variable features, which describe a particular decision variables; or (iii) constraint features, which describe a particular constraint. The extractor is free to implement only a subset of these methods, if it is known that it will not be used with a machine learning component that requires the other types of features.\n", + "\n", + "[FeatureExtractor]: ../../api/collectors/#miplearn.features.fields.FeaturesExtractor\n", + "[H5File]: ../../api/helpers/#miplearn.h5.H5File" + ] + }, + { + "cell_type": "markdown", + "id": "b2d9736c", + "metadata": {}, + "source": [ + "\n", + "## H5FieldsExtractor\n", + "\n", + "[H5FieldsExtractor][H5FieldsExtractor], the most simple extractor in MIPLearn, simple extracts data that is already available in the HDF5 file, assembles it into a matrix and returns it as-is. The fields used to build instance, variable and constraint features are user-specified. The class also performs checks to ensure that the shapes of the returned matrices make sense." + ] + }, + { + "cell_type": "markdown", + "id": "e8184dff", + "metadata": {}, + "source": [ + "### Example\n", + "\n", + "The example below demonstrates the usage of H5FieldsExtractor in a randomly generated instance of the multi-dimensional knapsack problem." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ed9a18c8", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "instance features (11,) \n", + " [-1531.24308771 -350. -692. -454.\n", + " -709. -605. -543. -321.\n", + " -674. -571. -341. ]\n", + "variable features (10, 4) \n", + " [[-1.53124309e+03 -3.50000000e+02 0.00000000e+00 9.43468018e+01]\n", + " [-1.53124309e+03 -6.92000000e+02 2.51703322e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -4.54000000e+02 0.00000000e+00 8.25504150e+01]\n", + " [-1.53124309e+03 -7.09000000e+02 1.11373022e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -6.05000000e+02 1.00000000e+00 -1.26055283e+02]\n", + " [-1.53124309e+03 -5.43000000e+02 0.00000000e+00 1.68693771e+02]\n", + " [-1.53124309e+03 -3.21000000e+02 1.07488781e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -6.74000000e+02 8.82293701e-01 0.00000000e+00]\n", + " [-1.53124309e+03 -5.71000000e+02 0.00000000e+00 1.41129074e+02]\n", + " [-1.53124309e+03 -3.41000000e+02 1.28830120e-01 0.00000000e+00]]\n", + "constraint features (5, 3) \n", + " [[ 1.3100000e+03 -1.5978307e-01 0.0000000e+00]\n", + " [ 9.8800000e+02 -3.2881632e-01 0.0000000e+00]\n", + " [ 1.0040000e+03 -4.0601316e-01 0.0000000e+00]\n", + " [ 1.2690000e+03 -1.3659772e-01 0.0000000e+00]\n", + " [ 1.0070000e+03 -2.8800571e-01 0.0000000e+00]]\n" + ] + } + ], + "source": [ + "from glob import glob\n", + "from shutil import rmtree\n", + "\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from miplearn.h5 import H5File\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.problems.multiknapsack import (\n", + " MultiKnapsackGenerator,\n", + " build_multiknapsack_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate some random multiknapsack instances\n", + "rmtree(\"data/multiknapsack/\", ignore_errors=True)\n", + "write_pkl_gz(\n", + " MultiKnapsackGenerator(\n", + " n=randint(low=10, high=11),\n", + " m=randint(low=5, high=6),\n", + " w=uniform(loc=0, scale=1000),\n", + " K=uniform(loc=100, scale=0),\n", + " u=uniform(loc=1, scale=0),\n", + " alpha=uniform(loc=0.25, scale=0),\n", + " w_jitter=uniform(loc=0.95, scale=0.1),\n", + " p_jitter=uniform(loc=0.75, scale=0.5),\n", + " fix_w=True,\n", + " ).generate(10),\n", + " \"data/multiknapsack\",\n", + ")\n", + "\n", + "# Run the basic collector\n", + "BasicCollector().collect(\n", + " glob(\"data/multiknapsack/*\"),\n", + " build_multiknapsack_model_gurobipy,\n", + " n_jobs=4,\n", + ")\n", + "\n", + "ext = H5FieldsExtractor(\n", + " # Use as instance features the value of the LP relaxation and the\n", + " # vector of objective coefficients.\n", + " instance_fields=[\n", + " \"lp_obj_value\",\n", + " \"static_var_obj_coeffs\",\n", + " ],\n", + " # For each variable, use as features the optimal value of the LP\n", + " # relaxation, the variable objective coefficient, the variable's\n", + " # value its reduced cost.\n", + " var_fields=[\n", + " \"lp_obj_value\",\n", + " \"static_var_obj_coeffs\",\n", + " \"lp_var_values\",\n", + " \"lp_var_reduced_costs\",\n", + " ],\n", + " # For each constraint, use as features the RHS, dual value and slack.\n", + " constr_fields=[\n", + " \"static_constr_rhs\",\n", + " \"lp_constr_dual_values\",\n", + " \"lp_constr_slacks\",\n", + " ],\n", + ")\n", + "\n", + "with H5File(\"data/multiknapsack/00000.h5\") as h5:\n", + " # Extract and print instance features\n", + " x1 = ext.get_instance_features(h5)\n", + " print(\"instance features\", x1.shape, \"\\n\", x1)\n", + "\n", + " # Extract and print variable features\n", + " x2 = ext.get_var_features(h5)\n", + " print(\"variable features\", x2.shape, \"\\n\", x2)\n", + "\n", + " # Extract and print constraint features\n", + " x3 = ext.get_constr_features(h5)\n", + " print(\"constraint features\", x3.shape, \"\\n\", x3)" + ] + }, + { + "cell_type": "markdown", + "id": "2da2e74e", + "metadata": {}, + "source": [ + "\n", + "[H5FieldsExtractor]: ../../api/collectors/#miplearn.features.fields.H5FieldsExtractor" + ] + }, + { + "cell_type": "markdown", + "id": "d879c0d3", + "metadata": {}, + "source": [ + "
    \n", + "Warning\n", + "\n", + "You should ensure that the number of features remains the same for all relevant HDF5 files. In the previous example, to illustrate this issue, we used variable objective coefficients as instance features. While this is allowed, note that this requires all problem instances to have the same number of variables; otherwise the number of features would vary from instance to instance and MIPLearn would be unable to concatenate the matrices.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "cd0ba071", + "metadata": {}, + "source": [ + "## AlvLouWeh2017Extractor\n", + "\n", + "Alvarez, Louveaux and Wehenkel (2017) proposed a set features to describe a particular decision variable in a given node of the branch-and-bound tree, and applied it to the problem of mimicking strong branching decisions. The class [AlvLouWeh2017Extractor][] implements a subset of these features (40 out of 64), which are available outside of the branch-and-bound tree. Some features are derived from the static defintion of the problem (i.e. from objective function and constraint data), while some features are derived from the solution to the LP relaxation. The features have been designed to be: (i) independent of the size of the problem; (ii) invariant with respect to irrelevant problem transformations, such as row and column permutation; and (iii) independent of the scale of the problem. We refer to the paper for a more complete description.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a1bc38fe", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "x1 (10, 40) \n", + " [[-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 6.00e-01 1.00e+00 1.75e+01 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 1.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 7.00e-01 1.00e+00 5.10e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 3.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 9.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 5.00e-01 1.00e+00 1.30e+01 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 2.00e-01 1.00e+00 9.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 3.40e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 1.00e-01 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 7.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 6.00e-01 1.00e+00 3.80e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 8.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 7.00e-01 1.00e+00 3.30e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 3.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 1.00e+00 1.00e+00 5.70e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 1.00e-01 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 6.80e+00 1.00e+00 2.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 4.00e-01 1.00e+00 6.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 8.00e-01 1.00e+00 1.40e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 1.00e-01 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 -1.00e+00 0.00e+00 1.00e+20]\n", + " [-1.00e+00 1.00e+20 1.00e-01 1.00e+00 0.00e+00 1.00e+00 5.00e-01\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 1.00e+00 5.00e-01 1.00e+00 7.60e+00 1.00e+00 1.00e-01\n", + " 1.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00 0.00e+00\n", + " 1.00e-01 -1.00e+00 -1.00e+00 0.00e+00 0.00e+00]]\n" + ] + } + ], + "source": [ + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.h5 import H5File\n", + "\n", + "# Build the extractor\n", + "ext = AlvLouWeh2017Extractor()\n", + "\n", + "# Open previously-created multiknapsack training data\n", + "with H5File(\"data/multiknapsack/00000.h5\") as h5:\n", + " # Extract and print variable features\n", + " x1 = ext.get_var_features(h5)\n", + " print(\"x1\", x1.shape, \"\\n\", x1.round(1))" + ] + }, + { + "cell_type": "markdown", + "id": "286c9927", + "metadata": {}, + "source": [ + "
    \n", + "References\n", + "\n", + "* **Alvarez, Alejandro Marcos.** *Computational and theoretical synergies between linear optimization and supervised machine learning.* (2016). University of Liège.\n", + "* **Alvarez, Alejandro Marcos, Quentin Louveaux, and Louis Wehenkel.** *A machine learning-based approximation of strong branching.* INFORMS Journal on Computing 29.1 (2017): 185-195.\n", + "\n", + "
    " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/guide/features/index.html b/0.4/guide/features/index.html new file mode 100644 index 0000000..34e5422 --- /dev/null +++ b/0.4/guide/features/index.html @@ -0,0 +1,538 @@ + + + + + + + + 7. Feature Extractors — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    7. Feature Extractors

    +

    In the previous page, we introduced training data collectors, which solve the optimization problem and collect raw training data, such as the optimal solution. In this page, we introduce feature extractors, which take the raw training data, stored in HDF5 files, and extract relevant information in order to train a machine learning model.

    +
    +

    7.1. Overview

    +

    Feature extraction is an important step of the process of building a machine learning model because it helps to reduce the complexity of the data and convert it into a format that is more easily processed. Previous research has proposed converting absolute variable coefficients, for example, into relative values which are invariant to various transformations, such as problem scaling, making them more amenable to learning. Various other transformations have also been described.

    +

    In the framework, we treat data collection and feature extraction as two separate steps to accelerate the model development cycle. Specifically, collectors are typically time-consuming, as they often need to solve the problem to optimality, and therefore focus on collecting and storing all data that may or may not be relevant, in its raw format. Feature extractors, on the other hand, focus entirely on filtering the data and improving its representation, and are therefore much faster to run. +Experimenting with new data representations, therefore, can be done without resolving the instances.

    +

    In MIPLearn, extractors implement the abstract class FeatureExtractor, which has methods that take as input an H5File and produce either: (i) instance features, which describe the entire instances; (ii) variable features, which describe a particular decision variables; or (iii) constraint features, which describe a particular constraint. The extractor is free to implement only a +subset of these methods, if it is known that it will not be used with a machine learning component that requires the other types of features.

    +
    +
    +

    7.2. H5FieldsExtractor

    +

    H5FieldsExtractor, the most simple extractor in MIPLearn, simple extracts data that is already available in the HDF5 file, assembles it into a matrix and returns it as-is. The fields used to build instance, variable and constraint features are user-specified. The class also performs checks to ensure that the shapes of the returned matrices make sense.

    +
    +

    Example

    +

    The example below demonstrates the usage of H5FieldsExtractor in a randomly generated instance of the multi-dimensional knapsack problem.

    +
    +
    [1]:
    +
    +
    +
    from glob import glob
    +from shutil import rmtree
    +
    +import numpy as np
    +from scipy.stats import uniform, randint
    +
    +from miplearn.collectors.basic import BasicCollector
    +from miplearn.extractors.fields import H5FieldsExtractor
    +from miplearn.h5 import H5File
    +from miplearn.io import write_pkl_gz
    +from miplearn.problems.multiknapsack import (
    +    MultiKnapsackGenerator,
    +    build_multiknapsack_model_gurobipy,
    +)
    +
    +# Set random seed to make example reproducible
    +np.random.seed(42)
    +
    +# Generate some random multiknapsack instances
    +rmtree("data/multiknapsack/", ignore_errors=True)
    +write_pkl_gz(
    +    MultiKnapsackGenerator(
    +        n=randint(low=10, high=11),
    +        m=randint(low=5, high=6),
    +        w=uniform(loc=0, scale=1000),
    +        K=uniform(loc=100, scale=0),
    +        u=uniform(loc=1, scale=0),
    +        alpha=uniform(loc=0.25, scale=0),
    +        w_jitter=uniform(loc=0.95, scale=0.1),
    +        p_jitter=uniform(loc=0.75, scale=0.5),
    +        fix_w=True,
    +    ).generate(10),
    +    "data/multiknapsack",
    +)
    +
    +# Run the basic collector
    +BasicCollector().collect(
    +    glob("data/multiknapsack/*"),
    +    build_multiknapsack_model_gurobipy,
    +    n_jobs=4,
    +)
    +
    +ext = H5FieldsExtractor(
    +    # Use as instance features the value of the LP relaxation and the
    +    # vector of objective coefficients.
    +    instance_fields=[
    +        "lp_obj_value",
    +        "static_var_obj_coeffs",
    +    ],
    +    # For each variable, use as features the optimal value of the LP
    +    # relaxation, the variable objective coefficient, the variable's
    +    # value its reduced cost.
    +    var_fields=[
    +        "lp_obj_value",
    +        "static_var_obj_coeffs",
    +        "lp_var_values",
    +        "lp_var_reduced_costs",
    +    ],
    +    # For each constraint, use as features the RHS, dual value and slack.
    +    constr_fields=[
    +        "static_constr_rhs",
    +        "lp_constr_dual_values",
    +        "lp_constr_slacks",
    +    ],
    +)
    +
    +with H5File("data/multiknapsack/00000.h5") as h5:
    +    # Extract and print instance features
    +    x1 = ext.get_instance_features(h5)
    +    print("instance features", x1.shape, "\n", x1)
    +
    +    # Extract and print variable features
    +    x2 = ext.get_var_features(h5)
    +    print("variable features", x2.shape, "\n", x2)
    +
    +    # Extract and print constraint features
    +    x3 = ext.get_constr_features(h5)
    +    print("constraint features", x3.shape, "\n", x3)
    +
    +
    +
    +
    +
    +
    +
    +
    +instance features (11,)
    + [-1531.24308771  -350.          -692.          -454.
    +  -709.          -605.          -543.          -321.
    +  -674.          -571.          -341.        ]
    +variable features (10, 4)
    + [[-1.53124309e+03 -3.50000000e+02  0.00000000e+00  9.43468018e+01]
    + [-1.53124309e+03 -6.92000000e+02  2.51703322e-01  0.00000000e+00]
    + [-1.53124309e+03 -4.54000000e+02  0.00000000e+00  8.25504150e+01]
    + [-1.53124309e+03 -7.09000000e+02  1.11373022e-01  0.00000000e+00]
    + [-1.53124309e+03 -6.05000000e+02  1.00000000e+00 -1.26055283e+02]
    + [-1.53124309e+03 -5.43000000e+02  0.00000000e+00  1.68693771e+02]
    + [-1.53124309e+03 -3.21000000e+02  1.07488781e-01  0.00000000e+00]
    + [-1.53124309e+03 -6.74000000e+02  8.82293701e-01  0.00000000e+00]
    + [-1.53124309e+03 -5.71000000e+02  0.00000000e+00  1.41129074e+02]
    + [-1.53124309e+03 -3.41000000e+02  1.28830120e-01  0.00000000e+00]]
    +constraint features (5, 3)
    + [[ 1.3100000e+03 -1.5978307e-01  0.0000000e+00]
    + [ 9.8800000e+02 -3.2881632e-01  0.0000000e+00]
    + [ 1.0040000e+03 -4.0601316e-01  0.0000000e+00]
    + [ 1.2690000e+03 -1.3659772e-01  0.0000000e+00]
    + [ 1.0070000e+03 -2.8800571e-01  0.0000000e+00]]
    +
    +
    +
    +

    Warning

    +

    You should ensure that the number of features remains the same for all relevant HDF5 files. In the previous example, to illustrate this issue, we used variable objective coefficients as instance features. While this is allowed, note that this requires all problem instances to have the same number of variables; otherwise the number of features would vary from instance to instance and MIPLearn would be unable to concatenate the matrices.

    +
    +
    +
    +
    +

    7.3. AlvLouWeh2017Extractor

    +

    Alvarez, Louveaux and Wehenkel (2017) proposed a set features to describe a particular decision variable in a given node of the branch-and-bound tree, and applied it to the problem of mimicking strong branching decisions. The class AlvLouWeh2017Extractor implements a subset of these features (40 out of 64), which are available outside of the branch-and-bound tree. Some features are derived from the static defintion of the problem (i.e. from objective function and +constraint data), while some features are derived from the solution to the LP relaxation. The features have been designed to be: (i) independent of the size of the problem; (ii) invariant with respect to irrelevant problem transformations, such as row and column permutation; and (iii) independent of the scale of the problem. We refer to the paper for a more complete description.

    +
    +

    Example

    +
    +
    [2]:
    +
    +
    +
    from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor
    +from miplearn.h5 import H5File
    +
    +# Build the extractor
    +ext = AlvLouWeh2017Extractor()
    +
    +# Open previously-created multiknapsack training data
    +with H5File("data/multiknapsack/00000.h5") as h5:
    +    # Extract and print variable features
    +    x1 = ext.get_var_features(h5)
    +    print("x1", x1.shape, "\n", x1.round(1))
    +
    +
    +
    +
    +
    +
    +
    +
    +x1 (10, 40)
    + [[-1.00e+00  1.00e+20  1.00e-01  1.00e+00  0.00e+00  1.00e+00  6.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  6.00e-01  1.00e+00  1.75e+01  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00 -1.00e+00  0.00e+00  1.00e+20]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  1.00e-01  1.00e+00  1.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  7.00e-01  1.00e+00  5.10e+00  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   3.00e-01 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  0.00e+00  1.00e+00  9.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  5.00e-01  1.00e+00  1.30e+01  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00 -1.00e+00  0.00e+00  1.00e+20]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  2.00e-01  1.00e+00  9.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  8.00e-01  1.00e+00  3.40e+00  1.00e+00  2.00e-01
    +   1.00e+00  1.00e-01  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   1.00e-01 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  1.00e-01  1.00e+00  7.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  6.00e-01  1.00e+00  3.80e+00  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  1.00e-01  1.00e+00  8.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  7.00e-01  1.00e+00  3.30e+00  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00 -1.00e+00  0.00e+00  1.00e+20]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  0.00e+00  1.00e+00  3.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  1.00e+00  1.00e+00  5.70e+00  1.00e+00  1.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   1.00e-01 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  1.00e-01  1.00e+00  6.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  8.00e-01  1.00e+00  6.80e+00  1.00e+00  2.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   1.00e-01 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  4.00e-01  1.00e+00  6.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  8.00e-01  1.00e+00  1.40e+00  1.00e+00  1.00e-01
    +   1.00e+00  1.00e-01  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00 -1.00e+00  0.00e+00  1.00e+20]
    + [-1.00e+00  1.00e+20  1.00e-01  1.00e+00  0.00e+00  1.00e+00  5.00e-01
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  1.00e+00  5.00e-01  1.00e+00  7.60e+00  1.00e+00  1.00e-01
    +   1.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00  0.00e+00
    +   1.00e-01 -1.00e+00 -1.00e+00  0.00e+00  0.00e+00]]
    +
    +
    +
    +

    References

    +
      +
    • Alvarez, Alejandro Marcos. Computational and theoretical synergies between linear optimization and supervised machine learning. (2016). University of Liège.

    • +
    • Alvarez, Alejandro Marcos, Quentin Louveaux, and Louis Wehenkel. A machine learning-based approximation of strong branching. INFORMS Journal on Computing 29.1 (2017): 185-195.

    • +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/guide/primal.ipynb b/0.4/guide/primal.ipynb new file mode 100644 index 0000000..26464ce --- /dev/null +++ b/0.4/guide/primal.ipynb @@ -0,0 +1,291 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "880cf4c7-d3c4-4b92-85c7-04a32264cdae", + "metadata": {}, + "source": [ + "# Primal Components\n", + "\n", + "In MIPLearn, a **primal component** is class that uses machine learning to predict a (potentially partial) assignment of values to the decision variables of the problem. Predicting high-quality primal solutions may be beneficial, as they allow the MIP solver to prune potentially large portions of the search space. Alternatively, if proof of optimality is not required, the MIP solver can be used to complete the partial solution generated by the machine learning model and and double-check its feasibility. MIPLearn allows both of these usage patterns.\n", + "\n", + "In this page, we describe the four primal components currently included in MIPLearn, which employ machine learning in different ways. Each component is highly configurable, and accepts an user-provided machine learning model, which it uses for all predictions. Each component can also be configured to provide the solution to the solver in multiple ways, depending on whether proof of optimality is required.\n", + "\n", + "## Primal component actions\n", + "\n", + "Before presenting the primal components themselves, we briefly discuss the three ways a solution may be provided to the solver. Each approach has benefits and limitations, which we also discuss in this section. All primal components can be configured to use any of the following approaches.\n", + "\n", + "The first approach is to provide the solution to the solver as a **warm start**. This is implemented by the class [SetWarmStart](SetWarmStart). The main advantage is that this method maintains all optimality and feasibility guarantees of the MIP solver, while still providing significant performance benefits for various classes of problems. If the machine learning model is able to predict multiple solutions, it is also possible to set multiple warm starts. In this case, the solver evaluates each warm start, discards the infeasible ones, then proceeds with the one that has the best objective value. The main disadvantage of this approach, compared to the next two, is that it provides relatively modest speedups for most problem classes, and no speedup at all for many others, even when the machine learning predictions are 100% accurate.\n", + "\n", + "[SetWarmStart]: ../../api/components/#miplearn.components.primal.actions.SetWarmStart\n", + "\n", + "The second approach is to **fix the decision variables** to their predicted values, then solve a restricted optimization problem on the remaining variables. This approach is implemented by the class `FixVariables`. The main advantage is its potential speedup: if machine learning can accurately predict values for a significant portion of the decision variables, then the MIP solver can typically complete the solution in a small fraction of the time it would take to find the same solution from scratch. The main disadvantage of this approach is that it loses optimality guarantees; that is, the complete solution found by the MIP solver may no longer be globally optimal. Also, if the machine learning predictions are not sufficiently accurate, there might not even be a feasible assignment for the variables that were left free.\n", + "\n", + "Finally, the third approach, which tries to strike a balance between the two previous ones, is to **enforce proximity** to a given solution. This strategy is implemented by the class `EnforceProximity`. More precisely, given values $\\bar{x}_1,\\ldots,\\bar{x}_n$ for a subset of binary decision variables $x_1,\\ldots,x_n$, this approach adds the constraint\n", + "\n", + "$$\n", + "\\sum_{i : \\bar{x}_i=0} x_i + \\sum_{i : \\bar{x}_i=1} \\left(1 - x_i\\right) \\leq k,\n", + "$$\n", + "to the problem, where $k$ is a user-defined parameter, which indicates how many of the predicted variables are allowed to deviate from the machine learning suggestion. The main advantage of this approach, compared to fixing variables, is its tolerance to lower-quality machine learning predictions. Its main disadvantage is that it typically leads to smaller speedups, especially for larger values of $k$. This approach also loses optimality guarantees.\n", + "\n", + "## Memorizing primal component\n", + "\n", + "A simple machine learning strategy for the prediction of primal solutions is to memorize all distinct solutions seen during training, then try to predict, during inference time, which of those memorized solutions are most likely to be feasible and to provide a good objective value for the current instance. The most promising solutions may alternatively be combined into a single partial solution, which is then provided to the MIP solver. Both variations of this strategy are implemented by the `MemorizingPrimalComponent` class. Note that it is only applicable if the problem size, and in fact if the meaning of the decision variables, remains the same across problem instances.\n", + "\n", + "More precisely, let $I_1,\\ldots,I_n$ be the training instances, and let $\\bar{x}^1,\\ldots,\\bar{x}^n$ be their respective optimal solutions. Given a new instance $I_{n+1}$, `MemorizingPrimalComponent` expects a user-provided binary classifier that assigns (through the `predict_proba` method, following scikit-learn's conventions) a score $\\delta_i$ to each solution $\\bar{x}^i$, such that solutions with higher score are more likely to be good solutions for $I_{n+1}$. The features provided to the classifier are the instance features computed by an user-provided extractor. Given these scores, the component then performs one of the following to actions, as decided by the user:\n", + "\n", + "1. Selects the top $k$ solutions with the highest scores and provides them to the solver; this is implemented by `SelectTopSolutions`, and it is typically used with the `SetWarmStart` action.\n", + "\n", + "2. Merges the top $k$ solutions into a single partial solution, then provides it to the solver. This is implemented by `MergeTopSolutions`. More precisely, suppose that the machine learning regressor ordered the solutions in the sequence $\\bar{x}^{i_1},\\ldots,\\bar{x}^{i_n}$, with the most promising solutions appearing first, and with ties being broken arbitrarily. The component starts by keeping only the $k$ most promising solutions $\\bar{x}^{i_1},\\ldots,\\bar{x}^{i_k}$. Then it computes, for each binary decision variable $x_l$, its average assigned value $\\tilde{x}_l$:\n", + "$$\n", + " \\tilde{x}_l = \\frac{1}{k} \\sum_{j=1}^k \\bar{x}^{i_j}_l.\n", + "$$\n", + " Finally, the component constructs a merged solution $y$, defined as:\n", + "$$\n", + " y_j = \\begin{cases}\n", + " 0 & \\text{ if } \\tilde{x}_l \\le \\theta_0 \\\\\n", + " 1 & \\text{ if } \\tilde{x}_l \\ge \\theta_1 \\\\\n", + " \\square & \\text{otherwise,}\n", + " \\end{cases}\n", + "$$\n", + " where $\\theta_0$ and $\\theta_1$ are user-specified parameters, and where $\\square$ indicates that the variable is left undefined. The solution $y$ is then provided by the solver using any of the three approaches defined in the previous section.\n", + "\n", + "The above specification of `MemorizingPrimalComponent` is meant to be as general as possible. Simpler strategies can be implemented by configuring this component in specific ways. For example, a simpler approach employed in the literature is to collect all optimal solutions, then provide the entire list of solutions to the solver as warm starts, without any filtering or post-processing. This strategy can be implemented with `MemorizingPrimalComponent` by using a model that returns a constant value for all solutions (e.g. [scikit-learn's DummyClassifier][DummyClassifier]), then selecting the top $n$ (instead of $k$) solutions. See example below. Another simple approach is taking the solution to the most similar instance, and using it, by itself, as a warm start. This can be implemented by using a model that computes distances between the current instance and the training ones (e.g. [scikit-learn's KNeighborsClassifier][KNeighborsClassifier]), then select the solution to the nearest one. See also example below. More complex strategies, of course, can also be configured.\n", + "\n", + "[DummyClassifier]: https://scikit-learn.org/stable/modules/generated/sklearn.dummy.DummyClassifier.html\n", + "[KNeighborsClassifier]: https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "253adbf4", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.dummy import DummyClassifier\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "\n", + "from miplearn.components.primal.actions import (\n", + " SetWarmStart,\n", + " FixVariables,\n", + " EnforceProximity,\n", + ")\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " SelectTopSolutions,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.dummy import DummyExtractor\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "# Configures a memorizing primal component that collects\n", + "# all distinct solutions seen during training and provides\n", + "# them to the solver without any filtering or post-processing.\n", + "comp1 = MemorizingPrimalComponent(\n", + " clf=DummyClassifier(),\n", + " extractor=DummyExtractor(),\n", + " constructor=SelectTopSolutions(1_000_000),\n", + " action=SetWarmStart(),\n", + ")\n", + "\n", + "# Configures a memorizing primal component that finds the\n", + "# training instance with the closest objective function, then\n", + "# fixes the decision variables to the values they assumed\n", + "# at the optimal solution for that instance.\n", + "comp2 = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=1),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " constructor=SelectTopSolutions(1),\n", + " action=FixVariables(),\n", + ")\n", + "\n", + "# Configures a memorizing primal component that finds the distinct\n", + "# solutions to the 10 most similar training problem instances,\n", + "# selects the 3 solutions that were most often optimal to these\n", + "# training instances, combines them into a single partial solution,\n", + "# then enforces proximity, allowing at most 3 variables to deviate\n", + "# from the machine learning suggestion.\n", + "comp3 = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=10),\n", + " extractor=H5FieldsExtractor(instance_fields=[\"static_var_obj_coeffs\"]),\n", + " constructor=MergeTopSolutions(k=3, thresholds=[0.25, 0.75]),\n", + " action=EnforceProximity(3),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f194a793", + "metadata": {}, + "source": [ + "## Independent vars primal component\n", + "\n", + "Instead of memorizing previously-seen primal solutions, it is also natural to use machine learning models to directly predict the values of the decision variables, constructing a solution from scratch. This approach has the benefit of potentially constructing novel high-quality solutions, never observed in the training data. Two variations of this strategy are supported by MIPLearn: (i) predicting the values of the decision variables independently, using multiple ML models; or (ii) predicting the values jointly, with a single model. We describe the first variation in this section, and the second variation in the next section.\n", + "\n", + "Let $I_1,\\ldots,I_n$ be the training instances, and let $\\bar{x}^1,\\ldots,\\bar{x}^n$ be their respective optimal solutions. For each binary decision variable $x_j$, the component `IndependentVarsPrimalComponent` creates a copy of a user-provided binary classifier and trains it to predict the optimal value of $x_j$, given $\\bar{x}^1_j,\\ldots,\\bar{x}^n_j$ as training labels. The features provided to the model are the variable features computed by an user-provided extractor. During inference time, the component uses these $n$ binary classifiers to construct a solution and provides it to the solver using one of the available actions.\n", + "\n", + "Three issues often arise in practice when using this approach:\n", + "\n", + " 1. For certain binary variables $x_j$, it is frequently the case that its optimal value is either always zero or always one in the training dataset, which poses problems to some standard scikit-learn classifiers, since they do not expect a single class. The wrapper `SingleClassFix` can be used to fix this issue (see example below).\n", + "2. It is also frequently the case that machine learning classifier can only reliably predict the values of some variables with high accuracy, not all of them. In this situation, instead of computing a complete primal solution, it may be more beneficial to construct a partial solution containing values only for the variables for which the ML made a high-confidence prediction. The meta-classifier `MinProbabilityClassifier` can be used for this purpose. It asks the base classifier for the probability of the value being zero or one (using the `predict_proba` method) and erases from the primal solution all values whose probabilities are below a given threshold.\n", + "3. To make multiple copies of the provided ML classifier, MIPLearn uses the standard `sklearn.base.clone` method, which may not be suitable for classifiers from other frameworks. To handle this, it is possible to override the clone function using the `clone_fn` constructor argument.\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3fc0b5d1", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.linear_model import LogisticRegression\n", + "from miplearn.classifiers.minprob import MinProbabilityClassifier\n", + "from miplearn.classifiers.singleclass import SingleClassFix\n", + "from miplearn.components.primal.indep import IndependentVarsPrimalComponent\n", + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures a primal component that independently predicts the value of each\n", + "# binary variable using logistic regression and provides it to the solver as\n", + "# warm start. Erases predictions with probability less than 99%; applies\n", + "# single-class fix; and uses AlvLouWeh2017 features.\n", + "comp = IndependentVarsPrimalComponent(\n", + " base_clf=SingleClassFix(\n", + " MinProbabilityClassifier(\n", + " base_clf=LogisticRegression(),\n", + " thresholds=[0.99, 0.99],\n", + " ),\n", + " ),\n", + " extractor=AlvLouWeh2017Extractor(),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "45107a0c", + "metadata": {}, + "source": [ + "## Joint vars primal component\n", + "In the previous subsection, we used multiple machine learning models to independently predict the values of the binary decision variables. When these values are correlated, an alternative approach is to jointly predict the values of all binary variables using a single machine learning model. This strategy is implemented by `JointVarsPrimalComponent`. Compared to the previous ones, this component is much more straightforwad. It simply extracts instance features, using the user-provided feature extractor, then directly trains the user-provided binary classifier (using the `fit` method), without making any copies. The trained classifier is then used to predict entire solutions (using the `predict` method), which are given to the solver using one of the previously discussed methods. In the example below, we illustrate the usage of this component with a simple feed-forward neural network.\n", + "\n", + "`JointVarsPrimalComponent` can also be used to implement strategies that use multiple machine learning models, but not indepedently. For example, a common strategy in multioutput prediction is building a *classifier chain*. In this approach, the first decision variable is predicted using the instance features alone; but the $n$-th decision variable is predicted using the instance features plus the predicted values of the $n-1$ previous variables. This can be easily implemented using scikit-learn's `ClassifierChain` estimator, as shown in the example below.\n", + "\n", + "### Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cf9b52dd", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from sklearn.multioutput import ClassifierChain\n", + "from sklearn.neural_network import MLPClassifier\n", + "from miplearn.components.primal.joint import JointVarsPrimalComponent\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures a primal component that uses a feedforward neural network\n", + "# to jointly predict the values of the binary variables, based on the\n", + "# objective cost function, and provides the solution to the solver as\n", + "# a warm start.\n", + "comp = JointVarsPrimalComponent(\n", + " clf=MLPClassifier(),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " action=SetWarmStart(),\n", + ")\n", + "\n", + "# Configures a primal component that uses a chain of logistic regression\n", + "# models to jointly predict the values of the binary variables, based on\n", + "# the objective function.\n", + "comp = JointVarsPrimalComponent(\n", + " clf=ClassifierChain(SingleClassFix(LogisticRegression())),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_var_obj_coeffs\"],\n", + " ),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "dddf7be4", + "metadata": {}, + "source": [ + "## Expert primal component\n", + "\n", + "Before spending time and effort choosing a machine learning strategy and tweaking its parameters, it is usually a good idea to evaluate what would be the performance impact of the model if its predictions were 100% accurate. This is especially important for the prediction of warm starts, since they are not always very beneficial. To simplify this task, MIPLearn provides `ExpertPrimalComponent`, a component which simply loads the optimal solution from the HDF5 file, assuming that it has already been computed, then directly provides it to the solver using one of the available methods. This component is useful in benchmarks, to evaluate how close to the best theoretical performance the machine learning components are.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9e2e81b9", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from miplearn.components.primal.expert import ExpertPrimalComponent\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "\n", + "# Configures an expert primal component, which reads a pre-computed\n", + "# optimal solution from the HDF5 file and provides it to the solver\n", + "# as warm start.\n", + "comp = ExpertPrimalComponent(action=SetWarmStart())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/guide/primal/index.html b/0.4/guide/primal/index.html new file mode 100644 index 0000000..bfe31b7 --- /dev/null +++ b/0.4/guide/primal/index.html @@ -0,0 +1,541 @@ + + + + + + + + 8. Primal Components — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    8. Primal Components

    +

    In MIPLearn, a primal component is class that uses machine learning to predict a (potentially partial) assignment of values to the decision variables of the problem. Predicting high-quality primal solutions may be beneficial, as they allow the MIP solver to prune potentially large portions of the search space. Alternatively, if proof of optimality is not required, the MIP solver can be used to complete the partial solution generated by the machine learning model and and double-check its +feasibility. MIPLearn allows both of these usage patterns.

    +

    In this page, we describe the four primal components currently included in MIPLearn, which employ machine learning in different ways. Each component is highly configurable, and accepts an user-provided machine learning model, which it uses for all predictions. Each component can also be configured to provide the solution to the solver in multiple ways, depending on whether proof of optimality is required.

    +
    +

    8.1. Primal component actions

    +

    Before presenting the primal components themselves, we briefly discuss the three ways a solution may be provided to the solver. Each approach has benefits and limitations, which we also discuss in this section. All primal components can be configured to use any of the following approaches.

    +

    The first approach is to provide the solution to the solver as a warm start. This is implemented by the class SetWarmStart. The main advantage is that this method maintains all optimality and feasibility guarantees of the MIP solver, while still providing significant performance benefits for various classes of problems. If the machine learning model is able to predict multiple solutions, it is also possible to set multiple warm starts. In this case, the solver evaluates +each warm start, discards the infeasible ones, then proceeds with the one that has the best objective value. The main disadvantage of this approach, compared to the next two, is that it provides relatively modest speedups for most problem classes, and no speedup at all for many others, even when the machine learning predictions are 100% accurate.

    +

    The second approach is to fix the decision variables to their predicted values, then solve a restricted optimization problem on the remaining variables. This approach is implemented by the class FixVariables. The main advantage is its potential speedup: if machine learning can accurately predict values for a significant portion of the decision variables, then the MIP solver can typically complete the solution in a small fraction of the time it would take to find the same solution from +scratch. The main disadvantage of this approach is that it loses optimality guarantees; that is, the complete solution found by the MIP solver may no longer be globally optimal. Also, if the machine learning predictions are not sufficiently accurate, there might not even be a feasible assignment for the variables that were left free.

    +

    Finally, the third approach, which tries to strike a balance between the two previous ones, is to enforce proximity to a given solution. This strategy is implemented by the class EnforceProximity. More precisely, given values \(\bar{x}_1,\ldots,\bar{x}_n\) for a subset of binary decision variables \(x_1,\ldots,x_n\), this approach adds the constraint

    +
    +\[\sum_{i : \bar{x}_i=0} x_i + \sum_{i : \bar{x}_i=1} \left(1 - x_i\right) \leq k,\]
    +

    to the problem, where \(k\) is a user-defined parameter, which indicates how many of the predicted variables are allowed to deviate from the machine learning suggestion. The main advantage of this approach, compared to fixing variables, is its tolerance to lower-quality machine learning predictions. Its main disadvantage is that it typically leads to smaller speedups, especially for larger values of \(k\). This approach also loses optimality guarantees.

    +
    +
    +

    8.2. Memorizing primal component

    +

    A simple machine learning strategy for the prediction of primal solutions is to memorize all distinct solutions seen during training, then try to predict, during inference time, which of those memorized solutions are most likely to be feasible and to provide a good objective value for the current instance. The most promising solutions may alternatively be combined into a single partial solution, which is then provided to the MIP solver. Both variations of this strategy are implemented by the +MemorizingPrimalComponent class. Note that it is only applicable if the problem size, and in fact if the meaning of the decision variables, remains the same across problem instances.

    +

    More precisely, let \(I_1,\ldots,I_n\) be the training instances, and let \(\bar{x}^1,\ldots,\bar{x}^n\) be their respective optimal solutions. Given a new instance \(I_{n+1}\), MemorizingPrimalComponent expects a user-provided binary classifier that assigns (through the predict_proba method, following scikit-learn’s conventions) a score \(\delta_i\) to each solution \(\bar{x}^i\), such that solutions with higher score are more likely to be good solutions for +\(I_{n+1}\). The features provided to the classifier are the instance features computed by an user-provided extractor. Given these scores, the component then performs one of the following to actions, as decided by the user:

    +
      +
    1. Selects the top \(k\) solutions with the highest scores and provides them to the solver; this is implemented by SelectTopSolutions, and it is typically used with the SetWarmStart action.

    2. +
    3. Merges the top \(k\) solutions into a single partial solution, then provides it to the solver. This is implemented by MergeTopSolutions. More precisely, suppose that the machine learning regressor ordered the solutions in the sequence \(\bar{x}^{i_1},\ldots,\bar{x}^{i_n}\), with the most promising solutions appearing first, and with ties being broken arbitrarily. The component starts by keeping only the \(k\) most promising solutions \(\bar{x}^{i_1},\ldots,\bar{x}^{i_k}\). +Then it computes, for each binary decision variable \(x_l\), its average assigned value \(\tilde{x}_l\):

      +
      +\[\tilde{x}_l = \frac{1}{k} \sum_{j=1}^k \bar{x}^{i_j}_l.\]
      +

      Finally, the component constructs a merged solution \(y\), defined as:

      +
      +\[\begin{split}y_j = \begin{cases} + 0 & \text{ if } \tilde{x}_l \le \theta_0 \\ + 1 & \text{ if } \tilde{x}_l \ge \theta_1 \\ + \square & \text{otherwise,} +\end{cases}\end{split}\]
      +

      where \(\theta_0\) and \(\theta_1\) are user-specified parameters, and where \(\square\) indicates that the variable is left undefined. The solution \(y\) is then provided by the solver using any of the three approaches defined in the previous section.

      +
    4. +
    +

    The above specification of MemorizingPrimalComponent is meant to be as general as possible. Simpler strategies can be implemented by configuring this component in specific ways. For example, a simpler approach employed in the literature is to collect all optimal solutions, then provide the entire list of solutions to the solver as warm starts, without any filtering or post-processing. This strategy can be implemented with MemorizingPrimalComponent by using a model that returns a constant +value for all solutions (e.g. scikit-learn’s DummyClassifier), then selecting the top \(n\) (instead of \(k\)) solutions. See example below. Another simple approach is taking the solution to the most similar instance, and using it, by itself, as a warm start. This can be implemented by using a model that computes distances between the current instance and the training ones (e.g. scikit-learn’s +KNeighborsClassifier), then select the solution to the nearest one. See also example below. More complex strategies, of course, can also be configured.

    +
    +

    Examples

    +
    +
    [1]:
    +
    +
    +
    from sklearn.dummy import DummyClassifier
    +from sklearn.neighbors import KNeighborsClassifier
    +
    +from miplearn.components.primal.actions import (
    +    SetWarmStart,
    +    FixVariables,
    +    EnforceProximity,
    +)
    +from miplearn.components.primal.mem import (
    +    MemorizingPrimalComponent,
    +    SelectTopSolutions,
    +    MergeTopSolutions,
    +)
    +from miplearn.extractors.dummy import DummyExtractor
    +from miplearn.extractors.fields import H5FieldsExtractor
    +
    +# Configures a memorizing primal component that collects
    +# all distinct solutions seen during training and provides
    +# them to the solver without any filtering or post-processing.
    +comp1 = MemorizingPrimalComponent(
    +    clf=DummyClassifier(),
    +    extractor=DummyExtractor(),
    +    constructor=SelectTopSolutions(1_000_000),
    +    action=SetWarmStart(),
    +)
    +
    +# Configures a memorizing primal component that finds the
    +# training instance with the closest objective function, then
    +# fixes the decision variables to the values they assumed
    +# at the optimal solution for that instance.
    +comp2 = MemorizingPrimalComponent(
    +    clf=KNeighborsClassifier(n_neighbors=1),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_var_obj_coeffs"],
    +    ),
    +    constructor=SelectTopSolutions(1),
    +    action=FixVariables(),
    +)
    +
    +# Configures a memorizing primal component that finds the distinct
    +# solutions to the 10 most similar training problem instances,
    +# selects the 3 solutions that were most often optimal to these
    +# training instances, combines them into a single partial solution,
    +# then enforces proximity, allowing at most 3 variables to deviate
    +# from the machine learning suggestion.
    +comp3 = MemorizingPrimalComponent(
    +    clf=KNeighborsClassifier(n_neighbors=10),
    +    extractor=H5FieldsExtractor(instance_fields=["static_var_obj_coeffs"]),
    +    constructor=MergeTopSolutions(k=3, thresholds=[0.25, 0.75]),
    +    action=EnforceProximity(3),
    +)
    +
    +
    +
    +
    +
    +
    +

    8.3. Independent vars primal component

    +

    Instead of memorizing previously-seen primal solutions, it is also natural to use machine learning models to directly predict the values of the decision variables, constructing a solution from scratch. This approach has the benefit of potentially constructing novel high-quality solutions, never observed in the training data. Two variations of this strategy are supported by MIPLearn: (i) predicting the values of the decision variables independently, using multiple ML models; or (ii) predicting +the values jointly, with a single model. We describe the first variation in this section, and the second variation in the next section.

    +

    Let \(I_1,\ldots,I_n\) be the training instances, and let \(\bar{x}^1,\ldots,\bar{x}^n\) be their respective optimal solutions. For each binary decision variable \(x_j\), the component IndependentVarsPrimalComponent creates a copy of a user-provided binary classifier and trains it to predict the optimal value of \(x_j\), given \(\bar{x}^1_j,\ldots,\bar{x}^n_j\) as training labels. The features provided to the model are the variable features computed by an user-provided +extractor. During inference time, the component uses these \(n\) binary classifiers to construct a solution and provides it to the solver using one of the available actions.

    +

    Three issues often arise in practice when using this approach:

    +
      +
    1. For certain binary variables \(x_j\), it is frequently the case that its optimal value is either always zero or always one in the training dataset, which poses problems to some standard scikit-learn classifiers, since they do not expect a single class. The wrapper SingleClassFix can be used to fix this issue (see example below).

    2. +
    3. It is also frequently the case that machine learning classifier can only reliably predict the values of some variables with high accuracy, not all of them. In this situation, instead of computing a complete primal solution, it may be more beneficial to construct a partial solution containing values only for the variables for which the ML made a high-confidence prediction. The meta-classifier MinProbabilityClassifier can be used for this purpose. It asks the base classifier for the +probability of the value being zero or one (using the predict_proba method) and erases from the primal solution all values whose probabilities are below a given threshold.

    4. +
    5. To make multiple copies of the provided ML classifier, MIPLearn uses the standard sklearn.base.clone method, which may not be suitable for classifiers from other frameworks. To handle this, it is possible to override the clone function using the clone_fn constructor argument.

    6. +
    +
    +

    Examples

    +
    +
    [2]:
    +
    +
    +
    from sklearn.linear_model import LogisticRegression
    +from miplearn.classifiers.minprob import MinProbabilityClassifier
    +from miplearn.classifiers.singleclass import SingleClassFix
    +from miplearn.components.primal.indep import IndependentVarsPrimalComponent
    +from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor
    +from miplearn.components.primal.actions import SetWarmStart
    +
    +# Configures a primal component that independently predicts the value of each
    +# binary variable using logistic regression and provides it to the solver as
    +# warm start. Erases predictions with probability less than 99%; applies
    +# single-class fix; and uses AlvLouWeh2017 features.
    +comp = IndependentVarsPrimalComponent(
    +    base_clf=SingleClassFix(
    +        MinProbabilityClassifier(
    +            base_clf=LogisticRegression(),
    +            thresholds=[0.99, 0.99],
    +        ),
    +    ),
    +    extractor=AlvLouWeh2017Extractor(),
    +    action=SetWarmStart(),
    +)
    +
    +
    +
    +
    +
    +
    +

    8.4. Joint vars primal component

    +

    In the previous subsection, we used multiple machine learning models to independently predict the values of the binary decision variables. When these values are correlated, an alternative approach is to jointly predict the values of all binary variables using a single machine learning model. This strategy is implemented by JointVarsPrimalComponent. Compared to the previous ones, this component is much more straightforwad. It simply extracts instance features, using the user-provided feature +extractor, then directly trains the user-provided binary classifier (using the fit method), without making any copies. The trained classifier is then used to predict entire solutions (using the predict method), which are given to the solver using one of the previously discussed methods. In the example below, we illustrate the usage of this component with a simple feed-forward neural network.

    +

    JointVarsPrimalComponent can also be used to implement strategies that use multiple machine learning models, but not indepedently. For example, a common strategy in multioutput prediction is building a classifier chain. In this approach, the first decision variable is predicted using the instance features alone; but the \(n\)-th decision variable is predicted using the instance features plus the predicted values of the \(n-1\) previous variables. This can be easily implemented +using scikit-learn’s ClassifierChain estimator, as shown in the example below.

    +
    +

    Examples

    +
    +
    [3]:
    +
    +
    +
    from sklearn.multioutput import ClassifierChain
    +from sklearn.neural_network import MLPClassifier
    +from miplearn.components.primal.joint import JointVarsPrimalComponent
    +from miplearn.extractors.fields import H5FieldsExtractor
    +from miplearn.components.primal.actions import SetWarmStart
    +
    +# Configures a primal component that uses a feedforward neural network
    +# to jointly predict the values of the binary variables, based on the
    +# objective cost function, and provides the solution to the solver as
    +# a warm start.
    +comp = JointVarsPrimalComponent(
    +    clf=MLPClassifier(),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_var_obj_coeffs"],
    +    ),
    +    action=SetWarmStart(),
    +)
    +
    +# Configures a primal component that uses a chain of logistic regression
    +# models to jointly predict the values of the binary variables, based on
    +# the objective function.
    +comp = JointVarsPrimalComponent(
    +    clf=ClassifierChain(SingleClassFix(LogisticRegression())),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_var_obj_coeffs"],
    +    ),
    +    action=SetWarmStart(),
    +)
    +
    +
    +
    +
    +
    +
    +

    8.5. Expert primal component

    +

    Before spending time and effort choosing a machine learning strategy and tweaking its parameters, it is usually a good idea to evaluate what would be the performance impact of the model if its predictions were 100% accurate. This is especially important for the prediction of warm starts, since they are not always very beneficial. To simplify this task, MIPLearn provides ExpertPrimalComponent, a component which simply loads the optimal solution from the HDF5 file, assuming that it has already +been computed, then directly provides it to the solver using one of the available methods. This component is useful in benchmarks, to evaluate how close to the best theoretical performance the machine learning components are.

    +
    +

    Example

    +
    +
    [4]:
    +
    +
    +
    from miplearn.components.primal.expert import ExpertPrimalComponent
    +from miplearn.components.primal.actions import SetWarmStart
    +
    +# Configures an expert primal component, which reads a pre-computed
    +# optimal solution from the HDF5 file and provides it to the solver
    +# as warm start.
    +comp = ExpertPrimalComponent(action=SetWarmStart())
    +
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/guide/problems.ipynb b/0.4/guide/problems.ipynb new file mode 100644 index 0000000..acc35fb --- /dev/null +++ b/0.4/guide/problems.ipynb @@ -0,0 +1,1607 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f89436b4-5bc5-4ae3-a20a-522a2cd65274", + "metadata": {}, + "source": [ + "# Benchmark Problems\n", + "\n", + "## Overview\n", + "\n", + "Benchmark sets such as [MIPLIB](https://miplib.zib.de/) or [TSPLIB](http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/) are usually employed to evaluate the performance of conventional MIP solvers. Two shortcomings, however, make existing benchmark sets less suitable for evaluating the performance of learning-enhanced MIP solvers: (i) while existing benchmark sets typically contain hundreds or thousands of instances, machine learning (ML) methods typically benefit from having orders of magnitude more instances available for training; (ii) current machine learning methods typically provide best performance on sets of homogeneous instances, buch general-purpose benchmark sets contain relatively few examples of each problem type.\n", + "\n", + "To tackle this challenge, MIPLearn provides random instance generators for a wide variety of classical optimization problems, covering applications from different fields, that can be used to evaluate new learning-enhanced MIP techniques in a measurable and reproducible way. As of MIPLearn 0.3, nine problem generators are available, each customizable with user-provided probability distribution and flexible parameters. The generators can be configured, for example, to produce large sets of very similar instances of same size, where only the objective function changes, or more diverse sets of instances, with various sizes and characteristics, belonging to a particular problem class.\n", + "\n", + "In the following, we describe the problems included in the library, their MIP formulation and the generation algorithm." + ] + }, + { + "cell_type": "markdown", + "id": "bd99c51f", + "metadata": {}, + "source": [ + "
    \n", + "Warning\n", + "\n", + "The random instance generators and formulations shown below are subject to change. If you use them in your research, for reproducibility, you should specify the MIPLearn version and all parameters.\n", + "
    \n", + "\n", + "
    \n", + "Note\n", + "\n", + "- To make the instances easier to process, all formulations are written as a minimization problem.\n", + "- Some problem formulations, such as the one for the *traveling salesman problem*, contain an exponential number of constraints, which are enforced through constraint generation. The MPS files for these problems contain only the constraints that were generated during a trial run, not the entire set of constraints. Resolving the MPS file, therefore, may not generate a feasible primal solution for the problem.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "830f3784-a3fc-4e2f-a484-e7808841ffe8", + "metadata": { + "tags": [] + }, + "source": [ + "## Bin Packing\n", + "\n", + "**Bin packing** is a combinatorial optimization problem that asks for the optimal way to pack a given set of items into a finite number of containers (or bins) of fixed capacity. More specifically, the problem is to assign indivisible items of different sizes to identical bins, while minimizing the number of bins used. The problem is NP-hard and has many practical applications, including logistics and warehouse management, where it is used to determine how to best store and transport goods using a limited amount of space." + ] + }, + { + "cell_type": "markdown", + "id": "af933298-92a9-4c5d-8d07-0d4918dedbb8", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $n$ be the number of items, and $s_i$ the size of the $i$-th item. Also let $B$ be the size of the bins. For each bin $j$, let $y_j$ be a binary decision variable which equals one if the bin is used. For every item-bin pair $(i,j)$, let $x_{ij}$ be a binary decision variable which equals one if item $i$ is assigned to bin $j$. The bin packing problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "5e502345", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{j=1}^n y_j \\\\\n", + "\\text{subject to} \\;\\;\\;\n", + " & \\sum_{i=1}^n s_i x_{ij} \\leq B y_j & \\forall j=1,\\ldots,n \\\\\n", + " & \\sum_{j=1}^n x_{ij} = 1 & \\forall i=1,\\ldots,n \\\\\n", + " & y_i \\in \\{0,1\\} & \\forall i=1,\\ldots,n \\\\\n", + " & x_{ij} \\in \\{0,1\\} & \\forall i,j=1,\\ldots,n \\\\\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "9cba2077", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "Random instances of the bin packing problem can be generated using the class [BinPackGenerator][BinPackGenerator].\n", + "\n", + "If `fix_items=False`, the class samples the user-provided probability distributions `n`, `sizes` and `capacity` to decide, respectively, the number of items, the sizes of the items and capacity of the bin. All values are sampled independently.\n", + "\n", + "If `fix_items=True`, the class creates a reference instance, using the method previously described, then generates additional instances by perturbing its item sizes and bin capacity. More specifically, the sizes of the items are set to $s_i \\gamma_i$, where $s_i$ is the size of the $i$-th item in the reference instance and $\\gamma_i$ is sampled from `sizes_jitter`. Similarly, the bin size is set to $B \\beta$, where $B$ is the reference bin size and $\\beta$ is sampled from `capacity_jitter`. The number of items remains the same across all generated instances.\n", + "\n", + "[BinPackGenerator]: ../../api/problems/#miplearn.problems.binpack.BinPackGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "2bc62803", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f14e560c-ef9f-4c48-8467-72d6acce5f9f", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.409419720Z", + "start_time": "2023-11-07T16:29:47.824353556Z" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 [ 8.47 26. 19.52 14.11 3.65 3.65 1.4 21.76 14.82 16.96] 102.24\n", + "1 [ 8.69 22.78 17.81 14.83 4.12 3.67 1.46 22.05 13.66 18.08] 93.41\n", + "2 [ 8.55 25.9 20. 15.89 3.75 3.59 1.51 21.4 13.89 17.68] 90.69\n", + "3 [10.13 22.62 18.89 14.4 3.92 3.94 1.36 23.69 15.85 19.26] 107.9\n", + "4 [ 9.55 25.77 16.79 14.06 3.55 3.76 1.42 20.66 16.02 17.19] 95.62\n", + "5 [ 9.44 22.06 19.41 13.69 4.28 4.11 1.36 19.51 15.98 18.43] 104.58\n", + "6 [ 9.87 21.74 17.78 13.82 4.18 4. 1.4 19.76 14.46 17.08] 104.59\n", + "7 [ 9.62 25.61 18.2 13.83 4.07 4.1 1.47 22.83 15.01 17.78] 98.55\n", + "8 [ 8.47 21.9 16.58 15.37 3.76 3.91 1.57 20.57 14.76 18.61] 94.58\n", + "9 [ 8.57 22.77 17.06 16.25 4.14 4. 1.56 22.97 14.09 19.09] 100.79\n", + "\n", + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 20 rows, 110 columns and 210 nonzeros\n", + "Model fingerprint: 0x1ff9913f\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+02]\n", + " Objective range [1e+00, 1e+00]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 5.0000000\n", + "Presolve time: 0.00s\n", + "Presolved: 20 rows, 110 columns, 210 nonzeros\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "\n", + "Root relaxation: objective 1.274844e+00, 38 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.27484 0 4 5.00000 1.27484 74.5% - 0s\n", + "H 0 0 4.0000000 1.27484 68.1% - 0s\n", + "H 0 0 2.0000000 1.27484 36.3% - 0s\n", + " 0 0 1.27484 0 4 2.00000 1.27484 36.3% - 0s\n", + "\n", + "Explored 1 nodes (38 simplex iterations) in 0.03 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 3: 2 4 5 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.000000000000e+00, best bound 2.000000000000e+00, gap 0.0000%\n", + "\n", + "User-callback calls 143, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.binpack import BinPackGenerator, build_binpack_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances of the binpack problem with ten items\n", + "data = BinPackGenerator(\n", + " n=randint(low=10, high=11),\n", + " sizes=uniform(loc=0, scale=25),\n", + " capacity=uniform(loc=100, scale=0),\n", + " sizes_jitter=uniform(loc=0.9, scale=0.2),\n", + " capacity_jitter=uniform(loc=0.9, scale=0.2),\n", + " fix_items=True,\n", + ").generate(10)\n", + "\n", + "# Print sizes and capacities\n", + "for i in range(10):\n", + " print(i, data[i].sizes, data[i].capacity)\n", + "print()\n", + "\n", + "# Optimize first instance\n", + "model = build_binpack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "9a3df608-4faf-444b-b5c2-18d3e90cbb5a", + "metadata": { + "tags": [] + }, + "source": [ + "## Multi-Dimensional Knapsack\n", + "\n", + "The **multi-dimensional knapsack problem** is a generalization of the classic knapsack problem, which involves selecting a subset of items to be placed in a knapsack such that the total value of the items is maximized without exceeding a maximum weight. In this generalization, items have multiple weights (representing multiple resources), and multiple weight constraints must be satisfied." + ] + }, + { + "cell_type": "markdown", + "id": "8d989002-d837-4ccf-a224-0504a6d66473", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $n$ be the number of items and $m$ be the number of resources. For each item $j$ and resource $i$, let $p_j$ be the price of the item, let $w_{ij}$ be the amount of resource $j$ item $i$ consumes (i.e. the $j$-th weight of the item), and let $b_i$ be the total amount of resource $i$ available (or the size of the $j$-th knapsack). The formulation is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "d0d3ea42", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & - \\sum_{j=1}^n p_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j=1}^n w_{ij} x_j \\leq b_i\n", + " & \\forall i=1,\\ldots,m \\\\\n", + " & x_j \\in \\{0,1\\}\n", + " & \\forall j=1,\\ldots,n\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "81b5b085-cfa9-45ce-9682-3aeb9be96cba", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [MultiKnapsackGenerator][MultiKnapsackGenerator] can be used to generate random instances of this problem. The number of items $n$ and knapsacks $m$ are sampled from the user-provided probability distributions `n` and `m`. The weights $w_{ij}$ are sampled independently from the provided distribution `w`. The capacity of knapsack $i$ is set to\n", + "\n", + "[MultiKnapsackGenerator]: ../../api/problems/#miplearn.problems.multiknapsack.MultiKnapsackGenerator\n", + "\n", + "$$\n", + " b_i = \\alpha_i \\sum_{j=1}^n w_{ij}\n", + "$$\n", + "\n", + "where $\\alpha_i$, the tightness ratio, is sampled from the provided probability\n", + "distribution `alpha`. To make the instances more challenging, the costs of the items\n", + "are linearly correlated to their average weights. More specifically, the price of each\n", + "item $j$ is set to:\n", + "\n", + "$$\n", + " p_j = \\sum_{i=1}^m \\frac{w_{ij}}{m} + K u_j,\n", + "$$\n", + "\n", + "where $K$, the correlation coefficient, and $u_j$, the correlation multiplier, are sampled\n", + "from the provided probability distributions `K` and `u`.\n", + "\n", + "If `fix_w=True` is provided, then $w_{ij}$ are kept the same in all generated instances. This also implies that $n$ and $m$ are kept fixed. Although the prices and capacities are derived from $w_{ij}$, as long as `u` and `K` are not constants, the generated instances will still not be completely identical.\n", + "\n", + "\n", + "If a probability distribution `w_jitter` is provided, then item weights will be set to $w_{ij} \\gamma_{ij}$ where $\\gamma_{ij}$ is sampled from `w_jitter`. When combined with `fix_w=True`, this argument may be used to generate instances where the weight of each item is roughly the same, but not exactly identical, across all instances. The prices of the items and the capacities of the knapsacks will be calculated as above, but using these perturbed weights instead.\n", + "\n", + "By default, all generated prices, weights and capacities are rounded to the nearest integer number. If `round=False` is provided, this rounding will be disabled." + ] + }, + { + "cell_type": "markdown", + "id": "f92135b8-67e7-4ec5-aeff-2fc17ad5e46d", + "metadata": {}, + "source": [ + "
    \n", + "References\n", + "\n", + "* **Freville, Arnaud, and Gérard Plateau.** *An efficient preprocessing procedure for the multidimensional 0–1 knapsack problem.* Discrete applied mathematics 49.1-3 (1994): 189-212.\n", + "* **Fréville, Arnaud.** *The multidimensional 0–1 knapsack problem: An overview.* European Journal of Operational Research 155.1 (2004): 1-21.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "f12a066f", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ce5f8fb-2769-4fbd-a40c-fd62b897690a", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.485068449Z", + "start_time": "2023-11-07T16:29:48.406139946Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prices\n", + " [350. 692. 454. 709. 605. 543. 321. 674. 571. 341.]\n", + "weights\n", + " [[392. 977. 764. 622. 158. 163. 56. 840. 574. 696.]\n", + " [ 20. 948. 860. 209. 178. 184. 293. 541. 414. 305.]\n", + " [629. 135. 278. 378. 466. 803. 205. 492. 584. 45.]\n", + " [630. 173. 64. 907. 947. 794. 312. 99. 711. 439.]\n", + " [117. 506. 35. 915. 266. 662. 312. 516. 521. 178.]]\n", + "capacities\n", + " [1310. 988. 1004. 1269. 1007.]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 50 nonzeros\n", + "Model fingerprint: 0xaf3ac15e\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [2e+01, 1e+03]\n", + " Objective range [3e+02, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+03, 1e+03]\n", + "Found heuristic solution: objective -804.0000000\n", + "Presolve removed 0 rows and 3 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 7 columns, 34 nonzeros\n", + "Variable types: 0 continuous, 7 integer (7 binary)\n", + "\n", + "Root relaxation: objective -1.428726e+03, 4 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 -1428.7265 0 4 -804.00000 -1428.7265 77.7% - 0s\n", + "H 0 0 -1279.000000 -1428.7265 11.7% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + "\n", + "Explored 1 nodes (4 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: -1279 -804 \n", + "No other solutions better than -1279\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -1.279000000000e+03, best bound -1.279000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 490, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.multiknapsack import (\n", + " MultiKnapsackGenerator,\n", + " build_multiknapsack_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate ten similar random instances of the multiknapsack problem with\n", + "# ten items, five resources and weights around [0, 1000].\n", + "data = MultiKnapsackGenerator(\n", + " n=randint(low=10, high=11),\n", + " m=randint(low=5, high=6),\n", + " w=uniform(loc=0, scale=1000),\n", + " K=uniform(loc=100, scale=0),\n", + " u=uniform(loc=1, scale=0),\n", + " alpha=uniform(loc=0.25, scale=0),\n", + " w_jitter=uniform(loc=0.95, scale=0.1),\n", + " p_jitter=uniform(loc=0.75, scale=0.5),\n", + " fix_w=True,\n", + ").generate(10)\n", + "\n", + "# Print data for one of the instances\n", + "print(\"prices\\n\", data[0].prices)\n", + "print(\"weights\\n\", data[0].weights)\n", + "print(\"capacities\\n\", data[0].capacities)\n", + "print()\n", + "\n", + "# Build model and optimize\n", + "model = build_multiknapsack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "e20376b0-0781-4bfa-968f-ded5fa47e176", + "metadata": { + "tags": [] + }, + "source": [ + "## Capacitated P-Median\n", + "\n", + "The **capacitated p-median** problem is a variation of the classic $p$-median problem, in which a set of customers must be served by a set of facilities. In the capacitated $p$-Median problem, each facility has a fixed capacity, and the goal is to minimize the total cost of serving the customers while ensuring that the capacity of each facility is not exceeded. Variations of problem are often used in logistics and supply chain management to determine the most efficient locations for warehouses or distribution centers." + ] + }, + { + "cell_type": "markdown", + "id": "2af65137-109e-4ca0-8753-bd999825204f", + "metadata": { + "tags": [] + }, + "source": [ + "### Formulation\n", + "\n", + "Let $I=\\{1,\\ldots,n\\}$ be the set of customers. For each customer $i \\in I$, let $d_i$ be its demand and let $y_i$ be a binary decision variable that equals one if we decide to open a facility at that customer's location. For each pair $(i,j) \\in I \\times I$, let $x_{ij}$ be a binary decision variable that equals one if customer $i$ is assigned to facility $j$. Furthermore, let $w_{ij}$ be the cost of serving customer $i$ from facility $j$, let $p$ be the number of facilities we must open, and let $c_j$ be the capacity of facility $j$. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "a2494ab1-d306-4db7-a100-8f1dfd4a55d7", + "metadata": { + "tags": [] + }, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & \\sum_{i \\in I} \\sum_{j \\in I} w_{ij} x_{ij}\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j \\in I} x_{ij} = 1 & \\forall i \\in I \\\\\n", + " & \\sum_{j \\in I} y_j = p \\\\\n", + " & \\sum_{i \\in I} d_i x_{ij} \\leq c_j y_j & \\forall j \\in I \\\\\n", + " & x_{ij} \\in \\{0, 1\\} & \\forall i, j \\in I \\\\\n", + " & y_j \\in \\{0, 1\\} & \\forall j \\in I\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "9dddf0d6-1f86-40d4-93a8-ccfe93d38e0d", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [PMedianGenerator][PMedianGenerator] can be used to generate random instances of this problem. First, it decides the number of customers and the parameter $p$ by sampling the provided `n` and `p` distributions, respectively. Then, for each customer $i$, the class builds its geographical location $(x_i, y_i)$ by sampling the provided `x` and `y` distributions. For each $i$, the demand for customer $i$ and the capacity of facility $i$ are decided by sampling the provided distributions `demands` and `capacities`, respectively. Finally, the costs $w_{ij}$ are set to the Euclidean distance between the locations of customers $i$ and $j$.\n", + "\n", + "If `fixed=True`, then the number of customers, their locations, the parameter $p$, the demands and the capacities are only sampled from their respective distributions exactly once, to build a reference instance which is then randomly perturbed. Specifically, in each perturbation, the distances, demands and capacities are multiplied by random scaling factors sampled from the distributions `distances_jitter`, `demands_jitter` and `capacities_jitter`, respectively. The result is a list of instances that have the same set of customers, but slightly different demands, capacities and distances.\n", + "\n", + "[PMedianGenerator]: ../../api/problems/#miplearn.problems.pmedian.PMedianGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "4e701397", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4e0e4223-b4e0-4962-a157-82a23a86e37d", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.575025403Z", + "start_time": "2023-11-07T16:29:48.453962705Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "p = 5\n", + "distances =\n", + " [[ 0. 50.17 82.42 32.76 33.2 35.45 86.88 79.11 43.17 66.2 ]\n", + " [ 50.17 0. 72.64 72.51 17.06 80.25 39.92 68.93 43.41 42.96]\n", + " [ 82.42 72.64 0. 71.69 70.92 82.51 67.88 3.76 39.74 30.73]\n", + " [ 32.76 72.51 71.69 0. 56.56 11.03 101.35 69.39 42.09 68.58]\n", + " [ 33.2 17.06 70.92 56.56 0. 63.68 54.71 67.16 34.89 44.99]\n", + " [ 35.45 80.25 82.51 11.03 63.68 0. 111.04 80.29 52.78 79.36]\n", + " [ 86.88 39.92 67.88 101.35 54.71 111.04 0. 65.13 61.37 40.82]\n", + " [ 79.11 68.93 3.76 69.39 67.16 80.29 65.13 0. 36.26 27.24]\n", + " [ 43.17 43.41 39.74 42.09 34.89 52.78 61.37 36.26 0. 26.62]\n", + " [ 66.2 42.96 30.73 68.58 44.99 79.36 40.82 27.24 26.62 0. ]]\n", + "demands = [6.12 1.39 2.92 3.66 4.56 7.85 2. 5.14 5.92 0.46]\n", + "capacities = [151.89 42.63 16.26 237.22 241.41 202.1 76.15 24.42 171.06 110.04]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 21 rows, 110 columns and 220 nonzeros\n", + "Model fingerprint: 0x8d8d9346\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Coefficient statistics:\n", + " Matrix range [5e-01, 2e+02]\n", + " Objective range [4e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 5e+00]\n", + "Found heuristic solution: objective 368.7900000\n", + "Presolve time: 0.00s\n", + "Presolved: 21 rows, 110 columns, 220 nonzeros\n", + "Variable types: 0 continuous, 110 integer (110 binary)\n", + "Found heuristic solution: objective 245.6400000\n", + "\n", + "Root relaxation: objective 0.000000e+00, 18 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 0.00000 0 6 245.64000 0.00000 100% - 0s\n", + "H 0 0 185.1900000 0.00000 100% - 0s\n", + "H 0 0 148.6300000 17.14595 88.5% - 0s\n", + "H 0 0 113.1800000 17.14595 84.9% - 0s\n", + " 0 0 17.14595 0 10 113.18000 17.14595 84.9% - 0s\n", + "H 0 0 99.5000000 17.14595 82.8% - 0s\n", + "H 0 0 98.3900000 17.14595 82.6% - 0s\n", + "H 0 0 93.9800000 64.28872 31.6% - 0s\n", + " 0 0 64.28872 0 15 93.98000 64.28872 31.6% - 0s\n", + "H 0 0 93.9200000 64.28872 31.5% - 0s\n", + " 0 0 86.06884 0 15 93.92000 86.06884 8.36% - 0s\n", + "* 0 0 0 91.2300000 91.23000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (70 simplex iterations) in 0.08 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 10: 91.23 93.92 93.98 ... 368.79\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 9.123000000000e+01, best bound 9.123000000000e+01, gap 0.0000%\n", + "\n", + "User-callback calls 190, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.pmedian import PMedianGenerator, build_pmedian_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with ten customers located in a\n", + "# 100x100 square, with demands in [0,10], capacities in [0, 250].\n", + "data = PMedianGenerator(\n", + " x=uniform(loc=0.0, scale=100.0),\n", + " y=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=randint(low=5, high=6),\n", + " demands=uniform(loc=0, scale=10),\n", + " capacities=uniform(loc=0, scale=250),\n", + " distances_jitter=uniform(loc=0.9, scale=0.2),\n", + " demands_jitter=uniform(loc=0.9, scale=0.2),\n", + " capacities_jitter=uniform(loc=0.9, scale=0.2),\n", + " fixed=True,\n", + ").generate(10)\n", + "\n", + "# Print data for one of the instances\n", + "print(\"p =\", data[0].p)\n", + "print(\"distances =\\n\", data[0].distances)\n", + "print(\"demands =\", data[0].demands)\n", + "print(\"capacities =\", data[0].capacities)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_pmedian_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "36129dbf-ecba-4026-ad4d-f2356bad4a26", + "metadata": {}, + "source": [ + "## Set cover\n", + "\n", + "The **set cover problem** is a classical NP-hard optimization problem which aims to minimize the number of sets needed to cover all elements in a given universe. Each set may contain a different number of elements, and sets may overlap with each other. This problem can be useful in various real-world scenarios such as scheduling, resource allocation, and network design." + ] + }, + { + "cell_type": "markdown", + "id": "d5254e7a", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $U = \\{1,\\ldots,n\\}$ be a given universe set, and let $S=\\{S_1,\\ldots,S_m\\}$ be a collection of sets whose union equal $U$. For each $j \\in \\{1,\\ldots,m\\}$, let $w_j$ be the weight of set $S_j$, and let $x_j$ be a binary decision variable that equals one if set $S_j$ is chosen. The set cover problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "5062d606-678c-45ba-9a45-d3c8b7401ad1", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & \\sum_{j=1}^m w_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j : i \\in S_j} x_j \\geq 1 & \\forall i \\in \\{1,\\ldots,n\\} \\\\\n", + " & x_j \\in \\{0, 1\\} & \\forall j \\in \\{1,\\ldots,m\\}\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "2732c050-2e11-44fc-bdd1-1b804a60f166", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [SetCoverGenerator] can generate random instances of this problem. The class first decides the number of elements and sets by sampling the provided distributions `n_elements` and `n_sets`, respectively. Then it generates a random incidence matrix $M$, as follows:\n", + "\n", + "1. The density $d$ of $M$ is decided by sampling the provided probability distribution `density`.\n", + "2. Each entry of $M$ is then sampled from the Bernoulli distribution, with probability $d$.\n", + "3. To ensure that each element belongs to at least one set, the class identifies elements that are not contained in any set, then assigns them to a random set (chosen uniformly).\n", + "4. Similarly, to ensure that each set contains at least one element, the class identifies empty sets, then modifies them to include one random element (chosen uniformly).\n", + "\n", + "Finally, the weight of set $j$ is set to $w_j + K | S_j |$, where $w_j$ and $k$ are sampled from `costs` and `K`, respectively, and where $|S_j|$ denotes the size of set $S_j$. The parameter $K$ is used to introduce some correlation between the size of the set and its weight, making the instance more challenging. Note that `K` is only sampled once for the entire instance.\n", + "\n", + "If `fix_sets=True`, then all generated instances have exactly the same sets and elements. The costs of the sets, however, are multiplied by random scaling factors sampled from the provided probability distribution `costs_jitter`.\n", + "\n", + "[SetCoverGenerator]: ../../api/problems/#miplearn.problems.setcover.SetCoverGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "569aa5ec-d475-41fa-a5d9-0b1a675fdf95", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3224845b-9afd-463e-abf4-e0e93d304859", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.804292323Z", + "start_time": "2023-11-07T16:29:48.492933268Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matrix\n", + " [[1 0 0 0 1 1 1 0 0 0]\n", + " [1 0 0 1 1 1 1 0 1 1]\n", + " [0 1 1 1 1 0 1 0 0 1]\n", + " [0 1 1 0 0 0 1 1 0 1]\n", + " [1 1 1 0 1 0 1 0 0 1]]\n", + "costs [1044.58 850.13 1014.5 944.83 697.9 971.87 213.49 220.98 70.23\n", + " 425.33]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 28 nonzeros\n", + "Model fingerprint: 0xe5c2d4fa\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [7e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 213.4900000\n", + "Presolve removed 5 rows and 10 columns\n", + "Presolve time: 0.00s\n", + "Presolve: All rows and columns removed\n", + "\n", + "Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 1: 213.49 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.134900000000e+02, best bound 2.134900000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 178, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.setcover import SetCoverGenerator, build_setcover_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Build random instances with five elements, ten sets and costs\n", + "# in the [0, 1000] interval, with a correlation factor of 25 and\n", + "# an incidence matrix with 25% density.\n", + "data = SetCoverGenerator(\n", + " n_elements=randint(low=5, high=6),\n", + " n_sets=randint(low=10, high=11),\n", + " costs=uniform(loc=0.0, scale=1000.0),\n", + " costs_jitter=uniform(loc=0.90, scale=0.20),\n", + " density=uniform(loc=0.5, scale=0.00),\n", + " K=uniform(loc=25.0, scale=0.0),\n", + " fix_sets=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for one instance\n", + "print(\"matrix\\n\", data[0].incidence_matrix)\n", + "print(\"costs\", data[0].costs)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_setcover_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "255a4e88-2e38-4a1b-ba2e-806b6bd4c815", + "metadata": {}, + "source": [ + "## Set Packing\n", + "\n", + "**Set packing** is a classical optimization problem that asks for the maximum number of disjoint sets within a given list. This problem often arises in real-world situations where a finite number of resources need to be allocated to tasks, such as airline flight crew scheduling." + ] + }, + { + "cell_type": "markdown", + "id": "19342eb1", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $U=\\{1,\\ldots,n\\}$ be a given universe set, and let $S = \\{S_1, \\ldots, S_m\\}$ be a collection of subsets of $U$. For each subset $j \\in \\{1, \\ldots, m\\}$, let $w_j$ be the weight of $S_j$ and let $x_j$ be a binary decision variable which equals one if set $S_j$ is chosen. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "0391b35b", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + " \\text{minimize}\\;\\;\\;\n", + " & -\\sum_{j=1}^m w_j x_j\n", + " \\\\\n", + " \\text{subject to}\\;\\;\\;\n", + " & \\sum_{j : i \\in S_j} x_j \\leq 1 & \\forall i \\in \\{1,\\ldots,n\\} \\\\\n", + " & x_j \\in \\{0, 1\\} & \\forall j \\in \\{1,\\ldots,m\\}\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "c2d7df7b", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [SetPackGenerator][SetPackGenerator] can generate random instances of this problem. It accepts exactly the same arguments, and generates instance data in exactly the same way as [SetCoverGenerator][SetCoverGenerator]. For more details, please see the documentation for that class.\n", + "\n", + "[SetPackGenerator]: ../../api/problems/#miplearn.problems.setpack.SetPackGenerator\n", + "[SetCoverGenerator]: ../../api/problems/#miplearn.problems.setcover.SetCoverGenerator\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cc797da7", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.806917868Z", + "start_time": "2023-11-07T16:29:48.781619530Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matrix\n", + " [[1 0 0 0 1 1 1 0 0 0]\n", + " [1 0 0 1 1 1 1 0 1 1]\n", + " [0 1 1 1 1 0 1 0 0 1]\n", + " [0 1 1 0 0 0 1 1 0 1]\n", + " [1 1 1 0 1 0 1 0 0 1]]\n", + "costs [1044.58 850.13 1014.5 944.83 697.9 971.87 213.49 220.98 70.23\n", + " 425.33]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 5 rows, 10 columns and 28 nonzeros\n", + "Model fingerprint: 0x4ee91388\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [7e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective -1265.560000\n", + "Presolve removed 5 rows and 10 columns\n", + "Presolve time: 0.00s\n", + "Presolve: All rows and columns removed\n", + "\n", + "Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 2: -1986.37 -1265.56 \n", + "No other solutions better than -1986.37\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -1.986370000000e+03, best bound -1.986370000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 238, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.setpack import SetPackGenerator, build_setpack_model_gurobipy\n", + "\n", + "# Set random seed, to make example reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Build random instances with five elements, ten sets and costs\n", + "# in the [0, 1000] interval, with a correlation factor of 25 and\n", + "# an incidence matrix with 25% density.\n", + "data = SetPackGenerator(\n", + " n_elements=randint(low=5, high=6),\n", + " n_sets=randint(low=10, high=11),\n", + " costs=uniform(loc=0.0, scale=1000.0),\n", + " costs_jitter=uniform(loc=0.90, scale=0.20),\n", + " density=uniform(loc=0.5, scale=0.00),\n", + " K=uniform(loc=25.0, scale=0.0),\n", + " fix_sets=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for one instance\n", + "print(\"matrix\\n\", data[0].incidence_matrix)\n", + "print(\"costs\", data[0].costs)\n", + "print()\n", + "\n", + "# Build and optimize model\n", + "model = build_setpack_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "373e450c-8f8b-4b59-bf73-251bdd6ff67e", + "metadata": {}, + "source": [ + "## Stable Set\n", + "\n", + "The **maximum-weight stable set problem** is a classical optimization problem in graph theory which asks for the maximum-weight subset of vertices in a graph such that no two vertices in the subset are adjacent. The problem often arises in real-world scheduling or resource allocation situations, where stable sets represent tasks or resources that can be chosen simultaneously without conflicts.\n", + "\n", + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple undirected graph, and for each vertex $v \\in V$, let $w_v$ be its weight. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "2f74dd10", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\; & -\\sum_{v \\in V} w_v x_v \\\\\n", + "\\text{such that} \\;\\;\\; & x_v + x_u \\leq 1 & \\forall (v,u) \\in E \\\\\n", + "& x_v \\in \\{0, 1\\} & \\forall v \\in V\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "ef030168", + "metadata": {}, + "source": [ + "\n", + "### Random instance generator\n", + "\n", + "The class [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator] can be used to generate random instances of this problem. The class first samples the user-provided probability distributions `n` and `p` to decide the number of vertices and the density of the graph. Then, it generates a random Erdős-Rényi graph $G_{n,p}$. We recall that, in such a graph, each potential edge is included with probabilty $p$, independently for each other. The class then samples the provided probability distribution `w` to decide the vertex weights.\n", + "\n", + "[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n", + "\n", + "If `fix_graph=True`, then all generated instances have the same random graph. For each instance, the weights are decided by sampling `w`, as described above.\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f996e99-0ec9-472b-be8a-30c9b8556931", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.954896857Z", + "start_time": "2023-11-07T16:29:48.825579097Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]\n", + "weights[0] [37.45 95.07 73.2 59.87 15.6 15.6 5.81 86.62 60.11 70.81]\n", + "weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]\n", + "\n", + "Set parameter PreCrush to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 15 rows, 10 columns and 30 nonzeros\n", + "Model fingerprint: 0x3240ea4a\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [6e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective -219.1400000\n", + "Presolve removed 7 rows and 2 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 8 rows, 8 columns, 19 nonzeros\n", + "Variable types: 0 continuous, 8 integer (8 binary)\n", + "\n", + "Root relaxation: objective -2.205650e+02, 5 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 infeasible 0 -219.14000 -219.14000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: -219.14 \n", + "No other solutions better than -219.14\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective -2.191400000000e+02, best bound -2.191400000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 299, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.stab import (\n", + " MaxWeightStableSetGenerator,\n", + " build_stab_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed 10-node graph,\n", + "# 25% density and random weights in the [0, 100] interval.\n", + "data = MaxWeightStableSetGenerator(\n", + " w=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=uniform(loc=0.25, scale=0.0),\n", + " fix_graph=True,\n", + ").generate(10)\n", + "\n", + "# Print the graph and weights for two instances\n", + "print(\"graph\", data[0].graph.edges)\n", + "print(\"weights[0]\", data[0].weights)\n", + "print(\"weights[1]\", data[1].weights)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_stab_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "444d1092-fd83-4957-b691-a198d56ba066", + "metadata": {}, + "source": [ + "## Traveling Salesman\n", + "\n", + "Given a list of cities and the distances between them, the **traveling salesman problem** asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp's 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes." + ] + }, + { + "cell_type": "markdown", + "id": "da3ca69c", + "metadata": {}, + "source": [ + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple undirected graph. For each edge $e \\in E$, let $d_e$ be its weight (or distance) and let $x_e$ be a binary decision variable which equals one if $e$ is included in the route. The problem is formulated as:" + ] + }, + { + "cell_type": "markdown", + "id": "9cf296e9", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{e \\in E} d_e x_e \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & \\sum_{e : \\delta(v)} x_e = 2 & \\forall v \\in V, \\\\\n", + " & \\sum_{e \\in \\delta(S)} x_e \\geq 2 & \\forall S \\subsetneq V, |S| \\neq \\emptyset, \\\\\n", + " & x_e \\in \\{0, 1\\} & \\forall e \\in E,\n", + "\\end{align*}\n", + "$$\n", + "where $\\delta(v)$ denotes the set of edges adjacent to vertex $v$, and $\\delta(S)$ denotes the set of edges that have one extremity in $S$ and one in $V \\setminus S$. Because of its exponential size, we enforce the second set of inequalities as lazy constraints." + ] + }, + { + "cell_type": "markdown", + "id": "eba3dbe5", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [TravelingSalesmanGenerator][TravelingSalesmanGenerator] can be used to generate random instances of this problem. Initially, the class samples the user-provided probability distribution `n` to decide how many cities to generate. Then, for each city $i$, the class generates its geographical location $(x_i, y_i)$ by sampling the provided distributions `x` and `y`. The distance $d_{ij}$ between cities $i$ and $j$ is then set to\n", + "$$\n", + "\\gamma_{ij} \\sqrt{(x_i - x_j)^2 + (y_i - y_j)^2},\n", + "$$\n", + "where $\\gamma$ is a random scaling factor sampled from the provided probability distribution `gamma`.\n", + "\n", + "If `fix_cities=True`, then the list of cities is kept the same for all generated instances. The $\\gamma$ values, however, and therefore also the distances, are still different. By default, all distances $d_{ij}$ are rounded to the nearest integer. If `round=False` is provided, this rounding will be disabled.\n", + "\n", + "[TravelingSalesmanGenerator]: ../../api/problems/#miplearn.problems.tsp.TravelingSalesmanGenerator" + ] + }, + { + "cell_type": "markdown", + "id": "61f16c56", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9d0c56c6", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:48.958833448Z", + "start_time": "2023-11-07T16:29:48.898121017Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "distances[0]\n", + " [[ 0. 513. 762. 358. 325. 374. 932. 731. 391. 634.]\n", + " [ 513. 0. 726. 765. 163. 754. 409. 719. 446. 400.]\n", + " [ 762. 726. 0. 780. 756. 744. 656. 40. 383. 334.]\n", + " [ 358. 765. 780. 0. 549. 117. 925. 702. 422. 728.]\n", + " [ 325. 163. 756. 549. 0. 663. 526. 708. 377. 462.]\n", + " [ 374. 754. 744. 117. 663. 0. 1072. 802. 501. 853.]\n", + " [ 932. 409. 656. 925. 526. 1072. 0. 654. 603. 433.]\n", + " [ 731. 719. 40. 702. 708. 802. 654. 0. 381. 255.]\n", + " [ 391. 446. 383. 422. 377. 501. 603. 381. 0. 287.]\n", + " [ 634. 400. 334. 728. 462. 853. 433. 255. 287. 0.]]\n", + "distances[1]\n", + " [[ 0. 493. 900. 354. 323. 367. 841. 727. 444. 668.]\n", + " [ 493. 0. 690. 687. 175. 725. 368. 744. 398. 446.]\n", + " [ 900. 690. 0. 666. 728. 827. 736. 41. 371. 317.]\n", + " [ 354. 687. 666. 0. 570. 104. 1090. 712. 454. 648.]\n", + " [ 323. 175. 728. 570. 0. 655. 521. 650. 356. 469.]\n", + " [ 367. 725. 827. 104. 655. 0. 1146. 779. 476. 752.]\n", + " [ 841. 368. 736. 1090. 521. 1146. 0. 681. 565. 394.]\n", + " [ 727. 744. 41. 712. 650. 779. 681. 0. 374. 286.]\n", + " [ 444. 398. 371. 454. 356. 476. 565. 374. 0. 274.]\n", + " [ 668. 446. 317. 648. 469. 752. 394. 286. 274. 0.]]\n", + "\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x719675e5\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "\n", + "Root relaxation: objective 2.921000e+03, 17 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + "* 0 0 0 2921.0000000 2921.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (17 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 2921 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.921000000000e+03, best bound 2.921000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 106, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed ten cities in the 1000x1000 box\n", + "# and random distance scaling factors in the [0.90, 1.10] interval.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(10)\n", + "\n", + "# Print distance matrices for the first two instances\n", + "print(\"distances[0]\\n\", data[0].distances)\n", + "print(\"distances[1]\\n\", data[1].distances)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_tsp_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "26dfc157-11f4-4564-b368-95ee8200875e", + "metadata": {}, + "source": [ + "## Unit Commitment\n", + "\n", + "The **unit commitment problem** is a mixed-integer optimization problem which asks which power generation units should be turned on and off, at what time, and at what capacity, in order to meet the demand for electricity generation at the lowest cost. Numerous operational constraints are typically enforced, such as *ramping constraints*, which prevent generation units from changing power output levels too quickly from one time step to the next, and *minimum-up* and *minimum-down* constraints, which prevent units from switching on and off too frequently. The unit commitment problem is widely used in power systems planning and operations." + ] + }, + { + "cell_type": "markdown", + "id": "7048d771", + "metadata": {}, + "source": [ + "\n", + "
    \n", + "Note\n", + "\n", + "MIPLearn includes a simple formulation for the unit commitment problem, which enforces only minimum and maximum power production, as well as minimum-up and minimum-down constraints. The formulation does not enforce, for example, ramping trajectories, piecewise-linear cost curves, start-up costs or transmission and n-1 security constraints. For a more complete set of formulations, solution methods and realistic benchmark instances for the problem, see [UnitCommitment.jl](https://github.com/ANL-CEEESA/UnitCommitment.jl).\n", + "
    \n", + "\n", + "### Formulation\n", + "\n", + "Let $T$ be the number of time steps, $G$ be the number of generation units, and let $D_t$ be the power demand (in MW) at time $t$. For each generating unit $g$, let $P^\\max_g$ and $P^\\min_g$ be the maximum and minimum amount of power the unit is able to produce when switched on; let $L_g$ and $l_g$ be the minimum up- and down-time for unit $g$; let $C^\\text{fixed}$ be the cost to keep unit $g$ on for one time step, regardless of its power output level; let $C^\\text{start}$ be the cost to switch unit $g$ on; and let $C^\\text{var}$ be the cost for generator $g$ to produce 1 MW of power. In this formulation, we assume linear production costs. For each generator $g$ and time $t$, let $x_{gt}$ be a binary variable which equals one if unit $g$ is on at time $t$, let $w_{gt}$ be a binary variable which equals one if unit $g$ switches from being off at time $t-1$ to being on at time $t$, and let $p_{gt}$ be a continuous variable which indicates the amount of power generated. The formulation is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "bec5ee1c", + "metadata": {}, + "source": [ + "\n", + "$$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{t=1}^T \\sum_{g=1}^G \\left(\n", + " x_{gt} C^\\text{fixed}_g\n", + " + w_{gt} C^\\text{start}_g\n", + " + p_{gt} C^\\text{var}_g\n", + " \\right)\n", + " \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & \\sum_{k=t-L_g+1}^t w_{gk} \\leq x_{gt}\n", + " & \\forall g\\; \\forall t=L_g-1,\\ldots,T-1 \\\\\n", + " & \\sum_{k=g-l_g+1}^T w_{gt} \\leq 1 - x_{g,t-l_g+1}\n", + " & \\forall g \\forall t=l_g-1,\\ldots,T-1 \\\\\n", + " & w_{gt} \\geq x_{gt} - x_{g,t-1}\n", + " & \\forall g \\forall t=1,\\ldots,T-1 \\\\\n", + " & \\sum_{g=1}^G p_{gt} \\geq D_t\n", + " & \\forall t \\\\\n", + " & P^\\text{min}_g x_{gt} \\leq p_{gt}\n", + " & \\forall g, t \\\\\n", + " & p_{gt} \\leq P^\\text{max}_g x_{gt}\n", + " & \\forall g, t \\\\\n", + " & x_{gt} \\in \\{0, 1\\}\n", + " & \\forall g, t.\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "4a1ffb4c", + "metadata": {}, + "source": [ + "\n", + "The first set of inequalities enforces minimum up-time constraints: if unit $g$ is down at time $t$, then it cannot start up during the previous $L_g$ time steps. The second set of inequalities enforces minimum down-time constraints, and is symmetrical to the previous one. The third set ensures that if unit $g$ starts up at time $t$, then the start up variable must be one. The fourth set ensures that demand is satisfied at each time period. The fifth and sixth sets enforce bounds to the quantity of power generated by each unit.\n", + "\n", + "
    \n", + "References\n", + "\n", + "- *Bendotti, P., Fouilhoux, P. & Rottner, C.* **The min-up/min-down unit commitment polytope.** J Comb Optim 36, 1024-1058 (2018). https://doi.org/10.1007/s10878-018-0273-y\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "01bed9fc", + "metadata": {}, + "source": [ + "\n", + "### Random instance generator\n", + "\n", + "The class `UnitCommitmentGenerator` can be used to generate random instances of this problem.\n", + "\n", + "First, the user-provided probability distributions `n_units` and `n_periods` are sampled to determine the number of generating units and the number of time steps, respectively. Then, for each unit, the probabilities `max_power` and `min_power` are sampled to determine the unit's maximum and minimum power output. To make it easier to generate valid ranges, `min_power` is not specified as the absolute power level in MW, but rather as a multiplier of `max_power`; for example, if `max_power` samples to 100 and `min_power` samples to 0.5, then the unit's power range is set to `[50,100]`. Then, the distributions `cost_startup`, `cost_prod` and `cost_fixed` are sampled to determine the unit's startup, variable and fixed costs, while the distributions `min_uptime` and `min_downtime` are sampled to determine its minimum up/down-time.\n", + "\n", + "After parameters for the units have been generated, the class then generates a periodic demand curve, with a peak every 12 time steps, in the range $(0.4C, 0.8C)$, where $C$ is the sum of all units' maximum power output. Finally, all costs and demand values are perturbed by random scaling factors independently sampled from the distributions `cost_jitter` and `demand_jitter`, respectively.\n", + "\n", + "If `fix_units=True`, then the list of generators (with their respective parameters) is kept the same for all generated instances. If `cost_jitter` and `demand_jitter` are provided, the instances will still have slightly different costs and demands." + ] + }, + { + "cell_type": "markdown", + "id": "855b87b4", + "metadata": {}, + "source": [ + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6217da7c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:49.061613905Z", + "start_time": "2023-11-07T16:29:48.941857719Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "min_power[0] [117.79 245.85 271.85 207.7 81.38]\n", + "max_power[0] [218.54 477.82 379.4 319.4 120.21]\n", + "min_uptime[0] [7 6 3 5 7]\n", + "min_downtime[0] [7 3 5 6 2]\n", + "min_power[0] [117.79 245.85 271.85 207.7 81.38]\n", + "cost_startup[0] [3042.42 5247.56 4319.45 2912.29 6118.53]\n", + "cost_prod[0] [ 6.97 14.61 18.32 22.8 39.26]\n", + "cost_fixed[0] [199.67 514.23 592.41 46.45 607.54]\n", + "demand[0]\n", + " [ 905.06 915.41 1166.52 1212.29 1127.81 953.52 905.06 796.21 783.78\n", + " 866.23 768.62 899.59 905.06 946.23 1087.61 1004.24 1048.36 992.03\n", + " 905.06 750.82 691.48 606.15 658.5 809.95]\n", + "\n", + "min_power[1] [117.79 245.85 271.85 207.7 81.38]\n", + "max_power[1] [218.54 477.82 379.4 319.4 120.21]\n", + "min_uptime[1] [7 6 3 5 7]\n", + "min_downtime[1] [7 3 5 6 2]\n", + "min_power[1] [117.79 245.85 271.85 207.7 81.38]\n", + "cost_startup[1] [2458.08 6200.26 4585.74 2666.05 4783.34]\n", + "cost_prod[1] [ 6.31 13.33 20.42 24.37 46.86]\n", + "cost_fixed[1] [196.9 416.42 655.57 52.51 626.15]\n", + "demand[1]\n", + " [ 981.42 840.07 1095.59 1102.03 1088.41 932.29 863.67 848.56 761.33\n", + " 828.28 775.18 834.99 959.76 865.72 1193.52 1058.92 985.19 893.92\n", + " 962.16 781.88 723.15 639.04 602.4 787.02]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 578 rows, 360 columns and 2128 nonzeros\n", + "Model fingerprint: 0x4dc1c661\n", + "Variable types: 120 continuous, 240 integer (240 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 5e+02]\n", + " Objective range [7e+00, 6e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+03]\n", + "Presolve removed 244 rows and 131 columns\n", + "Presolve time: 0.01s\n", + "Presolved: 334 rows, 229 columns, 842 nonzeros\n", + "Variable types: 116 continuous, 113 integer (113 binary)\n", + "Found heuristic solution: objective 440662.46430\n", + "Found heuristic solution: objective 429461.97680\n", + "Found heuristic solution: objective 374043.64040\n", + "\n", + "Root relaxation: objective 3.361348e+05, 142 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 336134.820 0 18 374043.640 336134.820 10.1% - 0s\n", + "H 0 0 368600.14450 336134.820 8.81% - 0s\n", + "H 0 0 364721.76610 336134.820 7.84% - 0s\n", + " 0 0 cutoff 0 364721.766 364721.766 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 3\n", + " Cover: 8\n", + " Implied bound: 29\n", + " Clique: 222\n", + " MIR: 7\n", + " Flow cover: 7\n", + " RLT: 1\n", + " Relax-and-lift: 7\n", + "\n", + "Explored 1 nodes (234 simplex iterations) in 0.02 seconds (0.02 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 5: 364722 368600 374044 ... 440662\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 3.647217661000e+05, best bound 3.647217661000e+05, gap 0.0000%\n", + "\n", + "User-callback calls 677, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.uc import UnitCommitmentGenerator, build_uc_model_gurobipy\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a random instance with 5 generators and 24 time steps\n", + "data = UnitCommitmentGenerator(\n", + " n_units=randint(low=5, high=6),\n", + " n_periods=randint(low=24, high=25),\n", + " max_power=uniform(loc=50, scale=450),\n", + " min_power=uniform(loc=0.5, scale=0.25),\n", + " cost_startup=uniform(loc=0, scale=10_000),\n", + " cost_prod=uniform(loc=0, scale=50),\n", + " cost_fixed=uniform(loc=0, scale=1_000),\n", + " min_uptime=randint(low=2, high=8),\n", + " min_downtime=randint(low=2, high=8),\n", + " cost_jitter=uniform(loc=0.75, scale=0.5),\n", + " demand_jitter=uniform(loc=0.9, scale=0.2),\n", + " fix_units=True,\n", + ").generate(10)\n", + "\n", + "# Print problem data for the two first instances\n", + "for i in range(2):\n", + " print(f\"min_power[{i}]\", data[i].min_power)\n", + " print(f\"max_power[{i}]\", data[i].max_power)\n", + " print(f\"min_uptime[{i}]\", data[i].min_uptime)\n", + " print(f\"min_downtime[{i}]\", data[i].min_downtime)\n", + " print(f\"min_power[{i}]\", data[i].min_power)\n", + " print(f\"cost_startup[{i}]\", data[i].cost_startup)\n", + " print(f\"cost_prod[{i}]\", data[i].cost_prod)\n", + " print(f\"cost_fixed[{i}]\", data[i].cost_fixed)\n", + " print(f\"demand[{i}]\\n\", data[i].demand)\n", + " print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_uc_model_gurobipy(data[0])\n", + "model.optimize()" + ] + }, + { + "cell_type": "markdown", + "id": "169293c7-33e1-4d28-8d39-9982776251d7", + "metadata": {}, + "source": [ + "## Vertex Cover\n", + "\n", + "**Minimum weight vertex cover** is a classical optimization problem in graph theory where the goal is to find the minimum-weight set of vertices that are connected to all of the edges in the graph. The problem generalizes one of Karp's 21 NP-complete problems and has applications in various fields, including bioinformatics and machine learning." + ] + }, + { + "cell_type": "markdown", + "id": "91f5781a", + "metadata": {}, + "source": [ + "\n", + "### Formulation\n", + "\n", + "Let $G=(V,E)$ be a simple graph. For each vertex $v \\in V$, let $w_g$ be its weight, and let $x_v$ be a binary decision variable which equals one if $v$ is included in the cover. The mixed-integer linear formulation for the problem is given by:" + ] + }, + { + "cell_type": "markdown", + "id": "544754cb", + "metadata": {}, + "source": [ + " $$\n", + "\\begin{align*}\n", + "\\text{minimize} \\;\\;\\;\n", + " & \\sum_{v \\in V} w_v \\\\\n", + "\\text{such that} \\;\\;\\;\n", + " & x_i + x_j \\ge 1 & \\forall \\{i, j\\} \\in E, \\\\\n", + " & x_{i,j} \\in \\{0, 1\\}\n", + " & \\forall \\{i,j\\} \\in E.\n", + "\\end{align*}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "35c99166", + "metadata": {}, + "source": [ + "### Random instance generator\n", + "\n", + "The class [MinWeightVertexCoverGenerator][MinWeightVertexCoverGenerator] can be used to generate random instances of this problem. The class accepts exactly the same parameters and behaves exactly in the same way as [MaxWeightStableSetGenerator][MaxWeightStableSetGenerator]. See the [stable set section](#Stable-Set) for more details.\n", + "\n", + "[MinWeightVertexCoverGenerator]: ../../api/problems/#module-miplearn.problems.vertexcover\n", + "[MaxWeightStableSetGenerator]: ../../api/problems/#miplearn.problems.stab.MaxWeightStableSetGenerator\n", + "\n", + "### Example" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5fff7afe-5b7a-4889-a502-66751ec979bf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-11-07T16:29:49.075657363Z", + "start_time": "2023-11-07T16:29:49.049561363Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]\n", + "weights[0] [37.45 95.07 73.2 59.87 15.6 15.6 5.81 86.62 60.11 70.81]\n", + "weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]\n", + "\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 15 rows, 10 columns and 30 nonzeros\n", + "Model fingerprint: 0x2d2d1390\n", + "Variable types: 0 continuous, 10 integer (10 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [6e+00, 1e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+00, 1e+00]\n", + "Found heuristic solution: objective 301.0000000\n", + "Presolve removed 7 rows and 2 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 8 rows, 8 columns, 19 nonzeros\n", + "Variable types: 0 continuous, 8 integer (8 binary)\n", + "\n", + "Root relaxation: objective 2.995750e+02, 8 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 infeasible 0 301.00000 301.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (8 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 301 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 3.010000000000e+02, best bound 3.010000000000e+02, gap 0.0000%\n", + "\n", + "User-callback calls 326, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "import random\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.problems.vertexcover import (\n", + " MinWeightVertexCoverGenerator,\n", + " build_vertexcover_model_gurobipy,\n", + ")\n", + "\n", + "# Set random seed to make example reproducible\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate random instances with a fixed 10-node graph,\n", + "# 25% density and random weights in the [0, 100] interval.\n", + "data = MinWeightVertexCoverGenerator(\n", + " w=uniform(loc=0.0, scale=100.0),\n", + " n=randint(low=10, high=11),\n", + " p=uniform(loc=0.25, scale=0.0),\n", + " fix_graph=True,\n", + ").generate(10)\n", + "\n", + "# Print the graph and weights for two instances\n", + "print(\"graph\", data[0].graph.edges)\n", + "print(\"weights[0]\", data[0].weights)\n", + "print(\"weights[1]\", data[1].weights)\n", + "print()\n", + "\n", + "# Load and optimize the first instance\n", + "model = build_vertexcover_model_gurobipy(data[0])\n", + "model.optimize()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/guide/problems/index.html b/0.4/guide/problems/index.html new file mode 100644 index 0000000..fc9a6d5 --- /dev/null +++ b/0.4/guide/problems/index.html @@ -0,0 +1,1647 @@ + + + + + + + + 5. Benchmark Problems — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    5. Benchmark Problems

    +
    +

    5.1. Overview

    +

    Benchmark sets such as MIPLIB or TSPLIB are usually employed to evaluate the performance of conventional MIP solvers. Two shortcomings, however, make existing benchmark sets less suitable for evaluating the performance of learning-enhanced MIP solvers: (i) while existing benchmark sets typically contain hundreds or thousands of instances, machine learning (ML) methods typically benefit from having orders of +magnitude more instances available for training; (ii) current machine learning methods typically provide best performance on sets of homogeneous instances, buch general-purpose benchmark sets contain relatively few examples of each problem type.

    +

    To tackle this challenge, MIPLearn provides random instance generators for a wide variety of classical optimization problems, covering applications from different fields, that can be used to evaluate new learning-enhanced MIP techniques in a measurable and reproducible way. As of MIPLearn 0.3, nine problem generators are available, each customizable with user-provided probability distribution and flexible parameters. The generators can be configured, for example, to produce large sets of very +similar instances of same size, where only the objective function changes, or more diverse sets of instances, with various sizes and characteristics, belonging to a particular problem class.

    +

    In the following, we describe the problems included in the library, their MIP formulation and the generation algorithm.

    +
    +

    Warning

    +

    The random instance generators and formulations shown below are subject to change. If you use them in your research, for reproducibility, you should specify the MIPLearn version and all parameters.

    +
    +
    +

    Note

    +
      +
    • To make the instances easier to process, all formulations are written as a minimization problem.

    • +
    • Some problem formulations, such as the one for the traveling salesman problem, contain an exponential number of constraints, which are enforced through constraint generation. The MPS files for these problems contain only the constraints that were generated during a trial run, not the entire set of constraints. Resolving the MPS file, therefore, may not generate a feasible primal solution for the problem.

    • +
    +
    +
    +
    +

    5.2. Bin Packing

    +

    Bin packing is a combinatorial optimization problem that asks for the optimal way to pack a given set of items into a finite number of containers (or bins) of fixed capacity. More specifically, the problem is to assign indivisible items of different sizes to identical bins, while minimizing the number of bins used. The problem is NP-hard and has many practical applications, including logistics and warehouse management, where it is used to determine how to best store and transport goods using +a limited amount of space.

    +
    +

    Formulation

    +

    Let \(n\) be the number of items, and \(s_i\) the size of the \(i\)-th item. Also let \(B\) be the size of the bins. For each bin \(j\), let \(y_j\) be a binary decision variable which equals one if the bin is used. For every item-bin pair \((i,j)\), let \(x_{ij}\) be a binary decision variable which equals one if item \(i\) is assigned to bin \(j\). The bin packing problem is formulated as:

    +
    +\[\begin{split}\begin{align*} +\text{minimize} \;\;\; + & \sum_{j=1}^n y_j \\ +\text{subject to} \;\;\; + & \sum_{i=1}^n s_i x_{ij} \leq B y_j & \forall j=1,\ldots,n \\ + & \sum_{j=1}^n x_{ij} = 1 & \forall i=1,\ldots,n \\ + & y_i \in \{0,1\} & \forall i=1,\ldots,n \\ + & x_{ij} \in \{0,1\} & \forall i,j=1,\ldots,n \\ +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    Random instances of the bin packing problem can be generated using the class BinPackGenerator.

    +

    If fix_items=False, the class samples the user-provided probability distributions n, sizes and capacity to decide, respectively, the number of items, the sizes of the items and capacity of the bin. All values are sampled independently.

    +

    If fix_items=True, the class creates a reference instance, using the method previously described, then generates additional instances by perturbing its item sizes and bin capacity. More specifically, the sizes of the items are set to \(s_i \gamma_i\), where \(s_i\) is the size of the \(i\)-th item in the reference instance and \(\gamma_i\) is sampled from sizes_jitter. Similarly, the bin size is set to \(B \beta\), where \(B\) is the reference bin size and +\(\beta\) is sampled from capacity_jitter. The number of items remains the same across all generated instances.

    +
    +
    +

    Example

    +
    +
    [1]:
    +
    +
    +
    import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.binpack import BinPackGenerator, build_binpack_model_gurobipy
    +
    +# Set random seed, to make example reproducible
    +np.random.seed(42)
    +
    +# Generate random instances of the binpack problem with ten items
    +data = BinPackGenerator(
    +    n=randint(low=10, high=11),
    +    sizes=uniform(loc=0, scale=25),
    +    capacity=uniform(loc=100, scale=0),
    +    sizes_jitter=uniform(loc=0.9, scale=0.2),
    +    capacity_jitter=uniform(loc=0.9, scale=0.2),
    +    fix_items=True,
    +).generate(10)
    +
    +# Print sizes and capacities
    +for i in range(10):
    +    print(i, data[i].sizes, data[i].capacity)
    +print()
    +
    +# Optimize first instance
    +model = build_binpack_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +0 [ 8.47 26.   19.52 14.11  3.65  3.65  1.4  21.76 14.82 16.96] 102.24
    +1 [ 8.69 22.78 17.81 14.83  4.12  3.67  1.46 22.05 13.66 18.08] 93.41
    +2 [ 8.55 25.9  20.   15.89  3.75  3.59  1.51 21.4  13.89 17.68] 90.69
    +3 [10.13 22.62 18.89 14.4   3.92  3.94  1.36 23.69 15.85 19.26] 107.9
    +4 [ 9.55 25.77 16.79 14.06  3.55  3.76  1.42 20.66 16.02 17.19] 95.62
    +5 [ 9.44 22.06 19.41 13.69  4.28  4.11  1.36 19.51 15.98 18.43] 104.58
    +6 [ 9.87 21.74 17.78 13.82  4.18  4.    1.4  19.76 14.46 17.08] 104.59
    +7 [ 9.62 25.61 18.2  13.83  4.07  4.1   1.47 22.83 15.01 17.78] 98.55
    +8 [ 8.47 21.9  16.58 15.37  3.76  3.91  1.57 20.57 14.76 18.61] 94.58
    +9 [ 8.57 22.77 17.06 16.25  4.14  4.    1.56 22.97 14.09 19.09] 100.79
    +
    +Restricted license - for non-production use only - expires 2024-10-28
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 20 rows, 110 columns and 210 nonzeros
    +Model fingerprint: 0x1ff9913f
    +Variable types: 0 continuous, 110 integer (110 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+02]
    +  Objective range  [1e+00, 1e+00]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+00]
    +Found heuristic solution: objective 5.0000000
    +Presolve time: 0.00s
    +Presolved: 20 rows, 110 columns, 210 nonzeros
    +Variable types: 0 continuous, 110 integer (110 binary)
    +
    +Root relaxation: objective 1.274844e+00, 38 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0    1.27484    0    4    5.00000    1.27484  74.5%     -    0s
    +H    0     0                       4.0000000    1.27484  68.1%     -    0s
    +H    0     0                       2.0000000    1.27484  36.3%     -    0s
    +     0     0    1.27484    0    4    2.00000    1.27484  36.3%     -    0s
    +
    +Explored 1 nodes (38 simplex iterations) in 0.03 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 3: 2 4 5
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 2.000000000000e+00, best bound 2.000000000000e+00, gap 0.0000%
    +
    +User-callback calls 143, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.3. Multi-Dimensional Knapsack

    +

    The multi-dimensional knapsack problem is a generalization of the classic knapsack problem, which involves selecting a subset of items to be placed in a knapsack such that the total value of the items is maximized without exceeding a maximum weight. In this generalization, items have multiple weights (representing multiple resources), and multiple weight constraints must be satisfied.

    +
    +

    Formulation

    +

    Let \(n\) be the number of items and \(m\) be the number of resources. For each item \(j\) and resource \(i\), let \(p_j\) be the price of the item, let \(w_{ij}\) be the amount of resource \(j\) item \(i\) consumes (i.e. the \(j\)-th weight of the item), and let \(b_i\) be the total amount of resource \(i\) available (or the size of the \(j\)-th knapsack). The formulation is given by:

    +
    +\[\begin{split}\begin{align*} + \text{minimize}\;\;\; + & - \sum_{j=1}^n p_j x_j + \\ + \text{subject to}\;\;\; + & \sum_{j=1}^n w_{ij} x_j \leq b_i + & \forall i=1,\ldots,m \\ + & x_j \in \{0,1\} + & \forall j=1,\ldots,n +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class MultiKnapsackGenerator can be used to generate random instances of this problem. The number of items \(n\) and knapsacks \(m\) are sampled from the user-provided probability distributions n and m. The weights \(w_{ij}\) are sampled independently from the provided distribution w. The capacity of knapsack \(i\) is set to

    +
    +\[b_i = \alpha_i \sum_{j=1}^n w_{ij}\]
    +

    where \(\alpha_i\), the tightness ratio, is sampled from the provided probability distribution alpha. To make the instances more challenging, the costs of the items are linearly correlated to their average weights. More specifically, the price of each item \(j\) is set to:

    +
    +\[p_j = \sum_{i=1}^m \frac{w_{ij}}{m} + K u_j,\]
    +

    where \(K\), the correlation coefficient, and \(u_j\), the correlation multiplier, are sampled from the provided probability distributions K and u.

    +

    If fix_w=True is provided, then \(w_{ij}\) are kept the same in all generated instances. This also implies that \(n\) and \(m\) are kept fixed. Although the prices and capacities are derived from \(w_{ij}\), as long as u and K are not constants, the generated instances will still not be completely identical.

    +

    If a probability distribution w_jitter is provided, then item weights will be set to \(w_{ij} \gamma_{ij}\) where \(\gamma_{ij}\) is sampled from w_jitter. When combined with fix_w=True, this argument may be used to generate instances where the weight of each item is roughly the same, but not exactly identical, across all instances. The prices of the items and the capacities of the knapsacks will be calculated as above, but using these perturbed weights instead.

    +

    By default, all generated prices, weights and capacities are rounded to the nearest integer number. If round=False is provided, this rounding will be disabled.

    +
    +

    References

    +
      +
    • Freville, Arnaud, and Gérard Plateau. An efficient preprocessing procedure for the multidimensional 0–1 knapsack problem. Discrete applied mathematics 49.1-3 (1994): 189-212.

    • +
    • Fréville, Arnaud. The multidimensional 0–1 knapsack problem: An overview. European Journal of Operational Research 155.1 (2004): 1-21.

    • +
    +
    +
    +
    +

    Example

    +
    +
    [2]:
    +
    +
    +
    import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.multiknapsack import (
    +    MultiKnapsackGenerator,
    +    build_multiknapsack_model_gurobipy,
    +)
    +
    +# Set random seed, to make example reproducible
    +np.random.seed(42)
    +
    +# Generate ten similar random instances of the multiknapsack problem with
    +# ten items, five resources and weights around [0, 1000].
    +data = MultiKnapsackGenerator(
    +    n=randint(low=10, high=11),
    +    m=randint(low=5, high=6),
    +    w=uniform(loc=0, scale=1000),
    +    K=uniform(loc=100, scale=0),
    +    u=uniform(loc=1, scale=0),
    +    alpha=uniform(loc=0.25, scale=0),
    +    w_jitter=uniform(loc=0.95, scale=0.1),
    +    p_jitter=uniform(loc=0.75, scale=0.5),
    +    fix_w=True,
    +).generate(10)
    +
    +# Print data for one of the instances
    +print("prices\n", data[0].prices)
    +print("weights\n", data[0].weights)
    +print("capacities\n", data[0].capacities)
    +print()
    +
    +# Build model and optimize
    +model = build_multiknapsack_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +prices
    + [350. 692. 454. 709. 605. 543. 321. 674. 571. 341.]
    +weights
    + [[392. 977. 764. 622. 158. 163.  56. 840. 574. 696.]
    + [ 20. 948. 860. 209. 178. 184. 293. 541. 414. 305.]
    + [629. 135. 278. 378. 466. 803. 205. 492. 584.  45.]
    + [630. 173.  64. 907. 947. 794. 312.  99. 711. 439.]
    + [117. 506.  35. 915. 266. 662. 312. 516. 521. 178.]]
    +capacities
    + [1310.  988. 1004. 1269. 1007.]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 5 rows, 10 columns and 50 nonzeros
    +Model fingerprint: 0xaf3ac15e
    +Variable types: 0 continuous, 10 integer (10 binary)
    +Coefficient statistics:
    +  Matrix range     [2e+01, 1e+03]
    +  Objective range  [3e+02, 7e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+03, 1e+03]
    +Found heuristic solution: objective -804.0000000
    +Presolve removed 0 rows and 3 columns
    +Presolve time: 0.00s
    +Presolved: 5 rows, 7 columns, 34 nonzeros
    +Variable types: 0 continuous, 7 integer (7 binary)
    +
    +Root relaxation: objective -1.428726e+03, 4 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 -1428.7265    0    4 -804.00000 -1428.7265  77.7%     -    0s
    +H    0     0                    -1279.000000 -1428.7265  11.7%     -    0s
    +
    +Cutting planes:
    +  Cover: 1
    +
    +Explored 1 nodes (4 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 2: -1279 -804
    +No other solutions better than -1279
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective -1.279000000000e+03, best bound -1.279000000000e+03, gap 0.0000%
    +
    +User-callback calls 490, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.4. Capacitated P-Median

    +

    The capacitated p-median problem is a variation of the classic \(p\)-median problem, in which a set of customers must be served by a set of facilities. In the capacitated \(p\)-Median problem, each facility has a fixed capacity, and the goal is to minimize the total cost of serving the customers while ensuring that the capacity of each facility is not exceeded. Variations of problem are often used in logistics and supply chain management to determine the most efficient locations for +warehouses or distribution centers.

    +
    +

    Formulation

    +

    Let \(I=\{1,\ldots,n\}\) be the set of customers. For each customer \(i \in I\), let \(d_i\) be its demand and let \(y_i\) be a binary decision variable that equals one if we decide to open a facility at that customer’s location. For each pair \((i,j) \in I \times I\), let \(x_{ij}\) be a binary decision variable that equals one if customer \(i\) is assigned to facility \(j\). Furthermore, let \(w_{ij}\) be the cost of serving customer \(i\) from facility +\(j\), let \(p\) be the number of facilities we must open, and let \(c_j\) be the capacity of facility \(j\). The problem is formulated as:

    +
    +\[\begin{split}\begin{align*} + \text{minimize}\;\;\; + & \sum_{i \in I} \sum_{j \in I} w_{ij} x_{ij} + \\ + \text{subject to}\;\;\; + & \sum_{j \in I} x_{ij} = 1 & \forall i \in I \\ + & \sum_{j \in I} y_j = p \\ + & \sum_{i \in I} d_i x_{ij} \leq c_j y_j & \forall j \in I \\ + & x_{ij} \in \{0, 1\} & \forall i, j \in I \\ + & y_j \in \{0, 1\} & \forall j \in I +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class PMedianGenerator can be used to generate random instances of this problem. First, it decides the number of customers and the parameter \(p\) by sampling the provided n and p distributions, respectively. Then, for each customer \(i\), the class builds its geographical location \((x_i, y_i)\) by sampling the provided x and y distributions. For each \(i\), the demand for customer \(i\) +and the capacity of facility \(i\) are decided by sampling the provided distributions demands and capacities, respectively. Finally, the costs \(w_{ij}\) are set to the Euclidean distance between the locations of customers \(i\) and \(j\).

    +

    If fixed=True, then the number of customers, their locations, the parameter \(p\), the demands and the capacities are only sampled from their respective distributions exactly once, to build a reference instance which is then randomly perturbed. Specifically, in each perturbation, the distances, demands and capacities are multiplied by random scaling factors sampled from the distributions distances_jitter, demands_jitter and capacities_jitter, respectively. The result is a +list of instances that have the same set of customers, but slightly different demands, capacities and distances.

    +
    +
    +

    Example

    +
    +
    [3]:
    +
    +
    +
    import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.pmedian import PMedianGenerator, build_pmedian_model_gurobipy
    +
    +# Set random seed, to make example reproducible
    +np.random.seed(42)
    +
    +# Generate random instances with ten customers located in a
    +# 100x100 square, with demands in [0,10], capacities in [0, 250].
    +data = PMedianGenerator(
    +    x=uniform(loc=0.0, scale=100.0),
    +    y=uniform(loc=0.0, scale=100.0),
    +    n=randint(low=10, high=11),
    +    p=randint(low=5, high=6),
    +    demands=uniform(loc=0, scale=10),
    +    capacities=uniform(loc=0, scale=250),
    +    distances_jitter=uniform(loc=0.9, scale=0.2),
    +    demands_jitter=uniform(loc=0.9, scale=0.2),
    +    capacities_jitter=uniform(loc=0.9, scale=0.2),
    +    fixed=True,
    +).generate(10)
    +
    +# Print data for one of the instances
    +print("p =", data[0].p)
    +print("distances =\n", data[0].distances)
    +print("demands =", data[0].demands)
    +print("capacities =", data[0].capacities)
    +print()
    +
    +# Build and optimize model
    +model = build_pmedian_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +p = 5
    +distances =
    + [[  0.    50.17  82.42  32.76  33.2   35.45  86.88  79.11  43.17  66.2 ]
    + [ 50.17   0.    72.64  72.51  17.06  80.25  39.92  68.93  43.41  42.96]
    + [ 82.42  72.64   0.    71.69  70.92  82.51  67.88   3.76  39.74  30.73]
    + [ 32.76  72.51  71.69   0.    56.56  11.03 101.35  69.39  42.09  68.58]
    + [ 33.2   17.06  70.92  56.56   0.    63.68  54.71  67.16  34.89  44.99]
    + [ 35.45  80.25  82.51  11.03  63.68   0.   111.04  80.29  52.78  79.36]
    + [ 86.88  39.92  67.88 101.35  54.71 111.04   0.    65.13  61.37  40.82]
    + [ 79.11  68.93   3.76  69.39  67.16  80.29  65.13   0.    36.26  27.24]
    + [ 43.17  43.41  39.74  42.09  34.89  52.78  61.37  36.26   0.    26.62]
    + [ 66.2   42.96  30.73  68.58  44.99  79.36  40.82  27.24  26.62   0.  ]]
    +demands = [6.12 1.39 2.92 3.66 4.56 7.85 2.   5.14 5.92 0.46]
    +capacities = [151.89  42.63  16.26 237.22 241.41 202.1   76.15  24.42 171.06 110.04]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 21 rows, 110 columns and 220 nonzeros
    +Model fingerprint: 0x8d8d9346
    +Variable types: 0 continuous, 110 integer (110 binary)
    +Coefficient statistics:
    +  Matrix range     [5e-01, 2e+02]
    +  Objective range  [4e+00, 1e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 5e+00]
    +Found heuristic solution: objective 368.7900000
    +Presolve time: 0.00s
    +Presolved: 21 rows, 110 columns, 220 nonzeros
    +Variable types: 0 continuous, 110 integer (110 binary)
    +Found heuristic solution: objective 245.6400000
    +
    +Root relaxation: objective 0.000000e+00, 18 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0    0.00000    0    6  245.64000    0.00000   100%     -    0s
    +H    0     0                     185.1900000    0.00000   100%     -    0s
    +H    0     0                     148.6300000   17.14595  88.5%     -    0s
    +H    0     0                     113.1800000   17.14595  84.9%     -    0s
    +     0     0   17.14595    0   10  113.18000   17.14595  84.9%     -    0s
    +H    0     0                      99.5000000   17.14595  82.8%     -    0s
    +H    0     0                      98.3900000   17.14595  82.6%     -    0s
    +H    0     0                      93.9800000   64.28872  31.6%     -    0s
    +     0     0   64.28872    0   15   93.98000   64.28872  31.6%     -    0s
    +H    0     0                      93.9200000   64.28872  31.5%     -    0s
    +     0     0   86.06884    0   15   93.92000   86.06884  8.36%     -    0s
    +*    0     0               0      91.2300000   91.23000  0.00%     -    0s
    +
    +Explored 1 nodes (70 simplex iterations) in 0.08 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 10: 91.23 93.92 93.98 ... 368.79
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 9.123000000000e+01, best bound 9.123000000000e+01, gap 0.0000%
    +
    +User-callback calls 190, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.5. Set cover

    +

    The set cover problem is a classical NP-hard optimization problem which aims to minimize the number of sets needed to cover all elements in a given universe. Each set may contain a different number of elements, and sets may overlap with each other. This problem can be useful in various real-world scenarios such as scheduling, resource allocation, and network design.

    +
    +

    Formulation

    +

    Let \(U = \{1,\ldots,n\}\) be a given universe set, and let \(S=\{S_1,\ldots,S_m\}\) be a collection of sets whose union equal \(U\). For each \(j \in \{1,\ldots,m\}\), let \(w_j\) be the weight of set \(S_j\), and let \(x_j\) be a binary decision variable that equals one if set \(S_j\) is chosen. The set cover problem is formulated as:

    +
    +\[\begin{split}\begin{align*} + \text{minimize}\;\;\; + & \sum_{j=1}^m w_j x_j + \\ + \text{subject to}\;\;\; + & \sum_{j : i \in S_j} x_j \geq 1 & \forall i \in \{1,\ldots,n\} \\ + & x_j \in \{0, 1\} & \forall j \in \{1,\ldots,m\} +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class SetCoverGenerator can generate random instances of this problem. The class first decides the number of elements and sets by sampling the provided distributions n_elements and n_sets, respectively. Then it generates a random incidence matrix \(M\), as follows:

    +
      +
    1. The density \(d\) of \(M\) is decided by sampling the provided probability distribution density.

    2. +
    3. Each entry of \(M\) is then sampled from the Bernoulli distribution, with probability \(d\).

    4. +
    5. To ensure that each element belongs to at least one set, the class identifies elements that are not contained in any set, then assigns them to a random set (chosen uniformly).

    6. +
    7. Similarly, to ensure that each set contains at least one element, the class identifies empty sets, then modifies them to include one random element (chosen uniformly).

    8. +
    +

    Finally, the weight of set \(j\) is set to \(w_j + K | S_j |\), where \(w_j\) and \(k\) are sampled from costs and K, respectively, and where \(|S_j|\) denotes the size of set \(S_j\). The parameter \(K\) is used to introduce some correlation between the size of the set and its weight, making the instance more challenging. Note that K is only sampled once for the entire instance.

    +

    If fix_sets=True, then all generated instances have exactly the same sets and elements. The costs of the sets, however, are multiplied by random scaling factors sampled from the provided probability distribution costs_jitter.

    +
    +
    +

    Example

    +
    +
    [4]:
    +
    +
    +
    import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.setcover import SetCoverGenerator, build_setcover_model_gurobipy
    +
    +# Set random seed, to make example reproducible
    +np.random.seed(42)
    +
    +# Build random instances with five elements, ten sets and costs
    +# in the [0, 1000] interval, with a correlation factor of 25 and
    +# an incidence matrix with 25% density.
    +data = SetCoverGenerator(
    +    n_elements=randint(low=5, high=6),
    +    n_sets=randint(low=10, high=11),
    +    costs=uniform(loc=0.0, scale=1000.0),
    +    costs_jitter=uniform(loc=0.90, scale=0.20),
    +    density=uniform(loc=0.5, scale=0.00),
    +    K=uniform(loc=25.0, scale=0.0),
    +    fix_sets=True,
    +).generate(10)
    +
    +# Print problem data for one instance
    +print("matrix\n", data[0].incidence_matrix)
    +print("costs", data[0].costs)
    +print()
    +
    +# Build and optimize model
    +model = build_setcover_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +matrix
    + [[1 0 0 0 1 1 1 0 0 0]
    + [1 0 0 1 1 1 1 0 1 1]
    + [0 1 1 1 1 0 1 0 0 1]
    + [0 1 1 0 0 0 1 1 0 1]
    + [1 1 1 0 1 0 1 0 0 1]]
    +costs [1044.58  850.13 1014.5   944.83  697.9   971.87  213.49  220.98   70.23
    +  425.33]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 5 rows, 10 columns and 28 nonzeros
    +Model fingerprint: 0xe5c2d4fa
    +Variable types: 0 continuous, 10 integer (10 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [7e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+00]
    +Found heuristic solution: objective 213.4900000
    +Presolve removed 5 rows and 10 columns
    +Presolve time: 0.00s
    +Presolve: All rows and columns removed
    +
    +Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
    +Thread count was 1 (of 20 available processors)
    +
    +Solution count 1: 213.49
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 2.134900000000e+02, best bound 2.134900000000e+02, gap 0.0000%
    +
    +User-callback calls 178, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.6. Set Packing

    +

    Set packing is a classical optimization problem that asks for the maximum number of disjoint sets within a given list. This problem often arises in real-world situations where a finite number of resources need to be allocated to tasks, such as airline flight crew scheduling.

    +
    +

    Formulation

    +

    Let \(U=\{1,\ldots,n\}\) be a given universe set, and let \(S = \{S_1, \ldots, S_m\}\) be a collection of subsets of \(U\). For each subset \(j \in \{1, \ldots, m\}\), let \(w_j\) be the weight of \(S_j\) and let \(x_j\) be a binary decision variable which equals one if set \(S_j\) is chosen. The problem is formulated as:

    +
    +\[\begin{split}\begin{align*} + \text{minimize}\;\;\; + & -\sum_{j=1}^m w_j x_j + \\ + \text{subject to}\;\;\; + & \sum_{j : i \in S_j} x_j \leq 1 & \forall i \in \{1,\ldots,n\} \\ + & x_j \in \{0, 1\} & \forall j \in \{1,\ldots,m\} +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class SetPackGenerator can generate random instances of this problem. It accepts exactly the same arguments, and generates instance data in exactly the same way as SetCoverGenerator. For more details, please see the documentation for that class.

    +
    +
    +

    Example

    +
    +
    [5]:
    +
    +
    +
    import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.setpack import SetPackGenerator, build_setpack_model_gurobipy
    +
    +# Set random seed, to make example reproducible
    +np.random.seed(42)
    +
    +# Build random instances with five elements, ten sets and costs
    +# in the [0, 1000] interval, with a correlation factor of 25 and
    +# an incidence matrix with 25% density.
    +data = SetPackGenerator(
    +    n_elements=randint(low=5, high=6),
    +    n_sets=randint(low=10, high=11),
    +    costs=uniform(loc=0.0, scale=1000.0),
    +    costs_jitter=uniform(loc=0.90, scale=0.20),
    +    density=uniform(loc=0.5, scale=0.00),
    +    K=uniform(loc=25.0, scale=0.0),
    +    fix_sets=True,
    +).generate(10)
    +
    +# Print problem data for one instance
    +print("matrix\n", data[0].incidence_matrix)
    +print("costs", data[0].costs)
    +print()
    +
    +# Build and optimize model
    +model = build_setpack_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +matrix
    + [[1 0 0 0 1 1 1 0 0 0]
    + [1 0 0 1 1 1 1 0 1 1]
    + [0 1 1 1 1 0 1 0 0 1]
    + [0 1 1 0 0 0 1 1 0 1]
    + [1 1 1 0 1 0 1 0 0 1]]
    +costs [1044.58  850.13 1014.5   944.83  697.9   971.87  213.49  220.98   70.23
    +  425.33]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 5 rows, 10 columns and 28 nonzeros
    +Model fingerprint: 0x4ee91388
    +Variable types: 0 continuous, 10 integer (10 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [7e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+00]
    +Found heuristic solution: objective -1265.560000
    +Presolve removed 5 rows and 10 columns
    +Presolve time: 0.00s
    +Presolve: All rows and columns removed
    +
    +Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
    +Thread count was 1 (of 20 available processors)
    +
    +Solution count 2: -1986.37 -1265.56
    +No other solutions better than -1986.37
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective -1.986370000000e+03, best bound -1.986370000000e+03, gap 0.0000%
    +
    +User-callback calls 238, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.7. Stable Set

    +

    The maximum-weight stable set problem is a classical optimization problem in graph theory which asks for the maximum-weight subset of vertices in a graph such that no two vertices in the subset are adjacent. The problem often arises in real-world scheduling or resource allocation situations, where stable sets represent tasks or resources that can be chosen simultaneously without conflicts.

    +
    +

    Formulation

    +

    Let \(G=(V,E)\) be a simple undirected graph, and for each vertex \(v \in V\), let \(w_v\) be its weight. The problem is formulated as:

    +
    +\[\begin{split}\begin{align*} +\text{minimize} \;\;\; & -\sum_{v \in V} w_v x_v \\ +\text{such that} \;\;\; & x_v + x_u \leq 1 & \forall (v,u) \in E \\ +& x_v \in \{0, 1\} & \forall v \in V +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class MaxWeightStableSetGenerator can be used to generate random instances of this problem. The class first samples the user-provided probability distributions n and p to decide the number of vertices and the density of the graph. Then, it generates a random Erdős-Rényi graph \(G_{n,p}\). We recall that, in such a graph, each potential edge is included with probabilty \(p\), independently for each +other. The class then samples the provided probability distribution w to decide the vertex weights.

    +

    If fix_graph=True, then all generated instances have the same random graph. For each instance, the weights are decided by sampling w, as described above.

    +
    +
    +

    Example

    +
    +
    [6]:
    +
    +
    +
    import random
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.stab import (
    +    MaxWeightStableSetGenerator,
    +    build_stab_model_gurobipy,
    +)
    +
    +# Set random seed to make example reproducible
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate random instances with a fixed 10-node graph,
    +# 25% density and random weights in the [0, 100] interval.
    +data = MaxWeightStableSetGenerator(
    +    w=uniform(loc=0.0, scale=100.0),
    +    n=randint(low=10, high=11),
    +    p=uniform(loc=0.25, scale=0.0),
    +    fix_graph=True,
    +).generate(10)
    +
    +# Print the graph and weights for two instances
    +print("graph", data[0].graph.edges)
    +print("weights[0]", data[0].weights)
    +print("weights[1]", data[1].weights)
    +print()
    +
    +# Load and optimize the first instance
    +model = build_stab_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]
    +weights[0] [37.45 95.07 73.2  59.87 15.6  15.6   5.81 86.62 60.11 70.81]
    +weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]
    +
    +Set parameter PreCrush to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 15 rows, 10 columns and 30 nonzeros
    +Model fingerprint: 0x3240ea4a
    +Variable types: 0 continuous, 10 integer (10 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [6e+00, 1e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+00]
    +Found heuristic solution: objective -219.1400000
    +Presolve removed 7 rows and 2 columns
    +Presolve time: 0.00s
    +Presolved: 8 rows, 8 columns, 19 nonzeros
    +Variable types: 0 continuous, 8 integer (8 binary)
    +
    +Root relaxation: objective -2.205650e+02, 5 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 infeasible    0      -219.14000 -219.14000  0.00%     -    0s
    +
    +Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: -219.14
    +No other solutions better than -219.14
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective -2.191400000000e+02, best bound -2.191400000000e+02, gap 0.0000%
    +
    +User-callback calls 299, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.8. Traveling Salesman

    +

    Given a list of cities and the distances between them, the traveling salesman problem asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp’s 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes.

    +
    +

    Formulation

    +

    Let \(G=(V,E)\) be a simple undirected graph. For each edge \(e \in E\), let \(d_e\) be its weight (or distance) and let \(x_e\) be a binary decision variable which equals one if \(e\) is included in the route. The problem is formulated as:

    +
    +\[\begin{split}\begin{align*} +\text{minimize} \;\;\; + & \sum_{e \in E} d_e x_e \\ +\text{such that} \;\;\; + & \sum_{e : \delta(v)} x_e = 2 & \forall v \in V, \\ + & \sum_{e \in \delta(S)} x_e \geq 2 & \forall S \subsetneq V, |S| \neq \emptyset, \\ + & x_e \in \{0, 1\} & \forall e \in E, +\end{align*}\end{split}\]
    +

    where \(\delta(v)\) denotes the set of edges adjacent to vertex \(v\), and \(\delta(S)\) denotes the set of edges that have one extremity in \(S\) and one in \(V \setminus S\). Because of its exponential size, we enforce the second set of inequalities as lazy constraints.

    +
    +
    +

    Random instance generator

    +

    The class TravelingSalesmanGenerator can be used to generate random instances of this problem. Initially, the class samples the user-provided probability distribution n to decide how many cities to generate. Then, for each city \(i\), the class generates its geographical location \((x_i, y_i)\) by sampling the provided distributions x and y. The distance \(d_{ij}\) between cities \(i\) and +\(j\) is then set to

    +
    +\[\gamma_{ij} \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2},\]
    +

    where \(\gamma\) is a random scaling factor sampled from the provided probability distribution gamma.

    +

    If fix_cities=True, then the list of cities is kept the same for all generated instances. The \(\gamma\) values, however, and therefore also the distances, are still different. By default, all distances \(d_{ij}\) are rounded to the nearest integer. If round=False is provided, this rounding will be disabled.

    +
    +
    +

    Example

    +
    +
    [7]:
    +
    +
    +
    import random
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.tsp import (
    +    TravelingSalesmanGenerator,
    +    build_tsp_model_gurobipy,
    +)
    +
    +# Set random seed to make example reproducible
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate random instances with a fixed ten cities in the 1000x1000 box
    +# and random distance scaling factors in the [0.90, 1.10] interval.
    +data = TravelingSalesmanGenerator(
    +    n=randint(low=10, high=11),
    +    x=uniform(loc=0.0, scale=1000.0),
    +    y=uniform(loc=0.0, scale=1000.0),
    +    gamma=uniform(loc=0.90, scale=0.20),
    +    fix_cities=True,
    +    round=True,
    +).generate(10)
    +
    +# Print distance matrices for the first two instances
    +print("distances[0]\n", data[0].distances)
    +print("distances[1]\n", data[1].distances)
    +print()
    +
    +# Load and optimize the first instance
    +model = build_tsp_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +distances[0]
    + [[   0.  513.  762.  358.  325.  374.  932.  731.  391.  634.]
    + [ 513.    0.  726.  765.  163.  754.  409.  719.  446.  400.]
    + [ 762.  726.    0.  780.  756.  744.  656.   40.  383.  334.]
    + [ 358.  765.  780.    0.  549.  117.  925.  702.  422.  728.]
    + [ 325.  163.  756.  549.    0.  663.  526.  708.  377.  462.]
    + [ 374.  754.  744.  117.  663.    0. 1072.  802.  501.  853.]
    + [ 932.  409.  656.  925.  526. 1072.    0.  654.  603.  433.]
    + [ 731.  719.   40.  702.  708.  802.  654.    0.  381.  255.]
    + [ 391.  446.  383.  422.  377.  501.  603.  381.    0.  287.]
    + [ 634.  400.  334.  728.  462.  853.  433.  255.  287.    0.]]
    +distances[1]
    + [[   0.  493.  900.  354.  323.  367.  841.  727.  444.  668.]
    + [ 493.    0.  690.  687.  175.  725.  368.  744.  398.  446.]
    + [ 900.  690.    0.  666.  728.  827.  736.   41.  371.  317.]
    + [ 354.  687.  666.    0.  570.  104. 1090.  712.  454.  648.]
    + [ 323.  175.  728.  570.    0.  655.  521.  650.  356.  469.]
    + [ 367.  725.  827.  104.  655.    0. 1146.  779.  476.  752.]
    + [ 841.  368.  736. 1090.  521. 1146.    0.  681.  565.  394.]
    + [ 727.  744.   41.  712.  650.  779.  681.    0.  374.  286.]
    + [ 444.  398.  371.  454.  356.  476.  565.  374.    0.  274.]
    + [ 668.  446.  317.  648.  469.  752.  394.  286.  274.    0.]]
    +
    +Set parameter PreCrush to value 1
    +Set parameter LazyConstraints to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 10 rows, 45 columns and 90 nonzeros
    +Model fingerprint: 0x719675e5
    +Variable types: 0 continuous, 45 integer (45 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [4e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Presolve time: 0.00s
    +Presolved: 10 rows, 45 columns, 90 nonzeros
    +Variable types: 0 continuous, 45 integer (45 binary)
    +
    +Root relaxation: objective 2.921000e+03, 17 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +*    0     0               0    2921.0000000 2921.00000  0.00%     -    0s
    +
    +Cutting planes:
    +  Lazy constraints: 3
    +
    +Explored 1 nodes (17 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: 2921
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 2.921000000000e+03, best bound 2.921000000000e+03, gap 0.0000%
    +
    +User-callback calls 106, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.9. Unit Commitment

    +

    The unit commitment problem is a mixed-integer optimization problem which asks which power generation units should be turned on and off, at what time, and at what capacity, in order to meet the demand for electricity generation at the lowest cost. Numerous operational constraints are typically enforced, such as ramping constraints, which prevent generation units from changing power output levels too quickly from one time step to the next, and minimum-up and minimum-down constraints, +which prevent units from switching on and off too frequently. The unit commitment problem is widely used in power systems planning and operations.

    +
    +

    Note

    +

    MIPLearn includes a simple formulation for the unit commitment problem, which enforces only minimum and maximum power production, as well as minimum-up and minimum-down constraints. The formulation does not enforce, for example, ramping trajectories, piecewise-linear cost curves, start-up costs or transmission and n-1 security constraints. For a more complete set of formulations, solution methods and realistic benchmark instances for the problem, see +UnitCommitment.jl.

    +
    +
    +

    Formulation

    +

    Let \(T\) be the number of time steps, \(G\) be the number of generation units, and let \(D_t\) be the power demand (in MW) at time \(t\). For each generating unit \(g\), let \(P^\max_g\) and \(P^\min_g\) be the maximum and minimum amount of power the unit is able to produce when switched on; let \(L_g\) and \(l_g\) be the minimum up- and down-time for unit \(g\); let \(C^\text{fixed}\) be the cost to keep unit \(g\) on for one time step, +regardless of its power output level; let \(C^\text{start}\) be the cost to switch unit \(g\) on; and let \(C^\text{var}\) be the cost for generator \(g\) to produce 1 MW of power. In this formulation, we assume linear production costs. For each generator \(g\) and time \(t\), let \(x_{gt}\) be a binary variable which equals one if unit \(g\) is on at time \(t\), let \(w_{gt}\) be a binary variable which equals one if unit \(g\) switches from being off +at time \(t-1\) to being on at time \(t\), and let \(p_{gt}\) be a continuous variable which indicates the amount of power generated. The formulation is given by:

    +
    +\[\begin{split}\begin{align*} +\text{minimize} \;\;\; + & \sum_{t=1}^T \sum_{g=1}^G \left( + x_{gt} C^\text{fixed}_g + + w_{gt} C^\text{start}_g + + p_{gt} C^\text{var}_g + \right) + \\ +\text{such that} \;\;\; + & \sum_{k=t-L_g+1}^t w_{gk} \leq x_{gt} + & \forall g\; \forall t=L_g-1,\ldots,T-1 \\ + & \sum_{k=g-l_g+1}^T w_{gt} \leq 1 - x_{g,t-l_g+1} + & \forall g \forall t=l_g-1,\ldots,T-1 \\ + & w_{gt} \geq x_{gt} - x_{g,t-1} + & \forall g \forall t=1,\ldots,T-1 \\ + & \sum_{g=1}^G p_{gt} \geq D_t + & \forall t \\ + & P^\text{min}_g x_{gt} \leq p_{gt} + & \forall g, t \\ + & p_{gt} \leq P^\text{max}_g x_{gt} + & \forall g, t \\ + & x_{gt} \in \{0, 1\} + & \forall g, t. +\end{align*}\end{split}\]
    +

    The first set of inequalities enforces minimum up-time constraints: if unit \(g\) is down at time \(t\), then it cannot start up during the previous \(L_g\) time steps. The second set of inequalities enforces minimum down-time constraints, and is symmetrical to the previous one. The third set ensures that if unit \(g\) starts up at time \(t\), then the start up variable must be one. The fourth set ensures that demand is satisfied at each time period. The fifth and sixth sets +enforce bounds to the quantity of power generated by each unit.

    +
    +

    References

    + +
    +
    +
    +

    Random instance generator

    +

    The class UnitCommitmentGenerator can be used to generate random instances of this problem.

    +

    First, the user-provided probability distributions n_units and n_periods are sampled to determine the number of generating units and the number of time steps, respectively. Then, for each unit, the probabilities max_power and min_power are sampled to determine the unit’s maximum and minimum power output. To make it easier to generate valid ranges, min_power is not specified as the absolute power level in MW, but rather as a multiplier of max_power; for example, if +max_power samples to 100 and min_power samples to 0.5, then the unit’s power range is set to [50,100]. Then, the distributions cost_startup, cost_prod and cost_fixed are sampled to determine the unit’s startup, variable and fixed costs, while the distributions min_uptime and min_downtime are sampled to determine its minimum up/down-time.

    +

    After parameters for the units have been generated, the class then generates a periodic demand curve, with a peak every 12 time steps, in the range \((0.4C, 0.8C)\), where \(C\) is the sum of all units’ maximum power output. Finally, all costs and demand values are perturbed by random scaling factors independently sampled from the distributions cost_jitter and demand_jitter, respectively.

    +

    If fix_units=True, then the list of generators (with their respective parameters) is kept the same for all generated instances. If cost_jitter and demand_jitter are provided, the instances will still have slightly different costs and demands.

    +
    +
    +

    Example

    +
    +
    [8]:
    +
    +
    +
    import random
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.uc import UnitCommitmentGenerator, build_uc_model_gurobipy
    +
    +# Set random seed to make example reproducible
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate a random instance with 5 generators and 24 time steps
    +data = UnitCommitmentGenerator(
    +    n_units=randint(low=5, high=6),
    +    n_periods=randint(low=24, high=25),
    +    max_power=uniform(loc=50, scale=450),
    +    min_power=uniform(loc=0.5, scale=0.25),
    +    cost_startup=uniform(loc=0, scale=10_000),
    +    cost_prod=uniform(loc=0, scale=50),
    +    cost_fixed=uniform(loc=0, scale=1_000),
    +    min_uptime=randint(low=2, high=8),
    +    min_downtime=randint(low=2, high=8),
    +    cost_jitter=uniform(loc=0.75, scale=0.5),
    +    demand_jitter=uniform(loc=0.9, scale=0.2),
    +    fix_units=True,
    +).generate(10)
    +
    +# Print problem data for the two first instances
    +for i in range(2):
    +    print(f"min_power[{i}]", data[i].min_power)
    +    print(f"max_power[{i}]", data[i].max_power)
    +    print(f"min_uptime[{i}]", data[i].min_uptime)
    +    print(f"min_downtime[{i}]", data[i].min_downtime)
    +    print(f"min_power[{i}]", data[i].min_power)
    +    print(f"cost_startup[{i}]", data[i].cost_startup)
    +    print(f"cost_prod[{i}]", data[i].cost_prod)
    +    print(f"cost_fixed[{i}]", data[i].cost_fixed)
    +    print(f"demand[{i}]\n", data[i].demand)
    +    print()
    +
    +# Load and optimize the first instance
    +model = build_uc_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +min_power[0] [117.79 245.85 271.85 207.7   81.38]
    +max_power[0] [218.54 477.82 379.4  319.4  120.21]
    +min_uptime[0] [7 6 3 5 7]
    +min_downtime[0] [7 3 5 6 2]
    +min_power[0] [117.79 245.85 271.85 207.7   81.38]
    +cost_startup[0] [3042.42 5247.56 4319.45 2912.29 6118.53]
    +cost_prod[0] [ 6.97 14.61 18.32 22.8  39.26]
    +cost_fixed[0] [199.67 514.23 592.41  46.45 607.54]
    +demand[0]
    + [ 905.06  915.41 1166.52 1212.29 1127.81  953.52  905.06  796.21  783.78
    +  866.23  768.62  899.59  905.06  946.23 1087.61 1004.24 1048.36  992.03
    +  905.06  750.82  691.48  606.15  658.5   809.95]
    +
    +min_power[1] [117.79 245.85 271.85 207.7   81.38]
    +max_power[1] [218.54 477.82 379.4  319.4  120.21]
    +min_uptime[1] [7 6 3 5 7]
    +min_downtime[1] [7 3 5 6 2]
    +min_power[1] [117.79 245.85 271.85 207.7   81.38]
    +cost_startup[1] [2458.08 6200.26 4585.74 2666.05 4783.34]
    +cost_prod[1] [ 6.31 13.33 20.42 24.37 46.86]
    +cost_fixed[1] [196.9  416.42 655.57  52.51 626.15]
    +demand[1]
    + [ 981.42  840.07 1095.59 1102.03 1088.41  932.29  863.67  848.56  761.33
    +  828.28  775.18  834.99  959.76  865.72 1193.52 1058.92  985.19  893.92
    +  962.16  781.88  723.15  639.04  602.4   787.02]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 578 rows, 360 columns and 2128 nonzeros
    +Model fingerprint: 0x4dc1c661
    +Variable types: 120 continuous, 240 integer (240 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 5e+02]
    +  Objective range  [7e+00, 6e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+03]
    +Presolve removed 244 rows and 131 columns
    +Presolve time: 0.01s
    +Presolved: 334 rows, 229 columns, 842 nonzeros
    +Variable types: 116 continuous, 113 integer (113 binary)
    +Found heuristic solution: objective 440662.46430
    +Found heuristic solution: objective 429461.97680
    +Found heuristic solution: objective 374043.64040
    +
    +Root relaxation: objective 3.361348e+05, 142 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 336134.820    0   18 374043.640 336134.820  10.1%     -    0s
    +H    0     0                    368600.14450 336134.820  8.81%     -    0s
    +H    0     0                    364721.76610 336134.820  7.84%     -    0s
    +     0     0     cutoff    0      364721.766 364721.766  0.00%     -    0s
    +
    +Cutting planes:
    +  Gomory: 3
    +  Cover: 8
    +  Implied bound: 29
    +  Clique: 222
    +  MIR: 7
    +  Flow cover: 7
    +  RLT: 1
    +  Relax-and-lift: 7
    +
    +Explored 1 nodes (234 simplex iterations) in 0.02 seconds (0.02 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 5: 364722 368600 374044 ... 440662
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 3.647217661000e+05, best bound 3.647217661000e+05, gap 0.0000%
    +
    +User-callback calls 677, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +

    5.10. Vertex Cover

    +

    Minimum weight vertex cover is a classical optimization problem in graph theory where the goal is to find the minimum-weight set of vertices that are connected to all of the edges in the graph. The problem generalizes one of Karp’s 21 NP-complete problems and has applications in various fields, including bioinformatics and machine learning.

    +
    +

    Formulation

    +

    Let \(G=(V,E)\) be a simple graph. For each vertex \(v \in V\), let \(w_g\) be its weight, and let \(x_v\) be a binary decision variable which equals one if \(v\) is included in the cover. The mixed-integer linear formulation for the problem is given by:

    +
    +\[\begin{split}\begin{align*} +\text{minimize} \;\;\; + & \sum_{v \in V} w_v \\ +\text{such that} \;\;\; + & x_i + x_j \ge 1 & \forall \{i, j\} \in E, \\ + & x_{i,j} \in \{0, 1\} + & \forall \{i,j\} \in E. +\end{align*}\end{split}\]
    +
    +
    +

    Random instance generator

    +

    The class MinWeightVertexCoverGenerator can be used to generate random instances of this problem. The class accepts exactly the same parameters and behaves exactly in the same way as MaxWeightStableSetGenerator. See the stable set section for more details.

    +
    +
    +

    Example

    +
    +
    [9]:
    +
    +
    +
    import random
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from miplearn.problems.vertexcover import (
    +    MinWeightVertexCoverGenerator,
    +    build_vertexcover_model_gurobipy,
    +)
    +
    +# Set random seed to make example reproducible
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate random instances with a fixed 10-node graph,
    +# 25% density and random weights in the [0, 100] interval.
    +data = MinWeightVertexCoverGenerator(
    +    w=uniform(loc=0.0, scale=100.0),
    +    n=randint(low=10, high=11),
    +    p=uniform(loc=0.25, scale=0.0),
    +    fix_graph=True,
    +).generate(10)
    +
    +# Print the graph and weights for two instances
    +print("graph", data[0].graph.edges)
    +print("weights[0]", data[0].weights)
    +print("weights[1]", data[1].weights)
    +print()
    +
    +# Load and optimize the first instance
    +model = build_vertexcover_model_gurobipy(data[0])
    +model.optimize()
    +
    +
    +
    +
    +
    +
    +
    +
    +graph [(0, 2), (0, 4), (0, 8), (1, 2), (1, 3), (1, 5), (1, 6), (1, 9), (2, 5), (2, 9), (3, 6), (3, 7), (6, 9), (7, 8), (8, 9)]
    +weights[0] [37.45 95.07 73.2  59.87 15.6  15.6   5.81 86.62 60.11 70.81]
    +weights[1] [ 2.06 96.99 83.24 21.23 18.18 18.34 30.42 52.48 43.19 29.12]
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 15 rows, 10 columns and 30 nonzeros
    +Model fingerprint: 0x2d2d1390
    +Variable types: 0 continuous, 10 integer (10 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [6e+00, 1e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+00, 1e+00]
    +Found heuristic solution: objective 301.0000000
    +Presolve removed 7 rows and 2 columns
    +Presolve time: 0.00s
    +Presolved: 8 rows, 8 columns, 19 nonzeros
    +Variable types: 0 continuous, 8 integer (8 binary)
    +
    +Root relaxation: objective 2.995750e+02, 8 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 infeasible    0       301.00000  301.00000  0.00%     -    0s
    +
    +Explored 1 nodes (8 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: 301
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 3.010000000000e+02, best bound 3.010000000000e+02, gap 0.0000%
    +
    +User-callback calls 326, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/guide/solvers.ipynb b/0.4/guide/solvers.ipynb new file mode 100644 index 0000000..c4ee9bc --- /dev/null +++ b/0.4/guide/solvers.ipynb @@ -0,0 +1,251 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "9ec1907b-db93-4840-9439-c9005902b968", + "metadata": {}, + "source": [ + "# Learning Solver\n", + "\n", + "On previous pages, we discussed various components of the MIPLearn framework, including training data collectors, feature extractors, and individual machine learning components. In this page, we introduce **LearningSolver**, the main class of the framework which integrates all the aforementioned components into a cohesive whole. Using **LearningSolver** involves three steps: (i) configuring the solver; (ii) training the ML components; and (iii) solving new MIP instances. In the following, we describe each of these steps, then conclude with a complete runnable example.\n", + "\n", + "### Configuring the solver\n", + "\n", + "**LearningSolver** is composed by multiple individual machine learning components, each targeting a different part of the solution process, or implementing a different machine learning strategy. This architecture allows strategies to be easily enabled, disabled or customized, making the framework flexible. By default, no components are provided and **LearningSolver** is equivalent to a traditional MIP solver. To specify additional components, the `components` constructor argument may be used:\n", + "\n", + "```python\n", + "solver = LearningSolver(\n", + " components=[\n", + " comp1,\n", + " comp2,\n", + " comp3,\n", + " ]\n", + ")\n", + "```\n", + "\n", + "In this example, three components `comp1`, `comp2` and `comp3` are provided. The strategies implemented by these components are applied sequentially when solving the problem. For example, `comp1` and `comp2` could fix a subset of decision variables, while `comp3` constructs a warm start for the remaining problem.\n", + "\n", + "### Training and solving new instances\n", + "\n", + "Once a solver is configured, its ML components need to be trained. This can be achieved by the `solver.fit` method, as illustrated below. The method accepts a list of HDF5 files and trains each individual component sequentially. Once the solver is trained, new instances can be solved using `solver.optimize`. The method returns a dictionary of statistics collected by each component, such as the number of variables fixed.\n", + "\n", + "```python\n", + "# Build instances\n", + "train_data = ...\n", + "test_data = ...\n", + "\n", + "# Collect training data\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_model)\n", + "\n", + "# Build solver\n", + "solver = LearningSolver(...)\n", + "\n", + "# Train components\n", + "solver.fit(train_data)\n", + "\n", + "# Solve a new test instance\n", + "stats = solver.optimize(test_data[0], build_model)\n", + "\n", + "```\n", + "\n", + "### Complete example\n", + "\n", + "In the example below, we illustrate the usage of **LearningSolver** by building instances of the Traveling Salesman Problem, collecting training data, training the ML components, then solving a new instance." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "92b09b98", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x6ddcd141\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.3600000e+02 1.700000e+01 0.000000e+00 0s\n", + " 15 2.7610000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 15 iterations and 0.00 seconds (0.00 work units)\n", + "Optimal objective 2.761000000e+03\n", + "\n", + "User-callback calls 56, time in user-callback 0.00 sec\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 10 rows, 45 columns and 90 nonzeros\n", + "Model fingerprint: 0x74ca3d0a\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [4e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "\n", + "User MIP start produced solution with objective 2796 (0.00s)\n", + "Loaded user MIP start with objective 2796\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 10 rows, 45 columns, 90 nonzeros\n", + "Variable types: 0 continuous, 45 integer (45 binary)\n", + "\n", + "Root relaxation: objective 2.761000e+03, 14 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 2761.00000 0 - 2796.00000 2761.00000 1.25% - 0s\n", + " 0 0 cutoff 0 2796.00000 2796.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (16 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 2796 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 2.796000000000e+03, best bound 2.796000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 110, time in user-callback 0.00 sec\n" + ] + }, + { + "data": { + "text/plain": [ + "{'WS: Count': 1, 'WS: Number of variables set': 41.0}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import random\n", + "\n", + "import numpy as np\n", + "from scipy.stats import uniform, randint\n", + "from sklearn.linear_model import LogisticRegression\n", + "\n", + "from miplearn.classifiers.minprob import MinProbabilityClassifier\n", + "from miplearn.classifiers.singleclass import SingleClassFix\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.indep import IndependentVarsPrimalComponent\n", + "from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor\n", + "from miplearn.io import write_pkl_gz\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanGenerator,\n", + " build_tsp_model_gurobipy,\n", + ")\n", + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "# Set random seed to make example reproducible.\n", + "random.seed(42)\n", + "np.random.seed(42)\n", + "\n", + "# Generate a few instances of the traveling salesman problem.\n", + "data = TravelingSalesmanGenerator(\n", + " n=randint(low=10, high=11),\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " gamma=uniform(loc=0.90, scale=0.20),\n", + " fix_cities=True,\n", + " round=True,\n", + ").generate(50)\n", + "\n", + "# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...\n", + "all_data = write_pkl_gz(data, \"data/tsp\")\n", + "\n", + "# Split train/test data\n", + "train_data = all_data[:40]\n", + "test_data = all_data[40:]\n", + "\n", + "# Collect training data\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_tsp_model_gurobipy, n_jobs=4)\n", + "\n", + "# Build learning solver\n", + "solver = LearningSolver(\n", + " components=[\n", + " IndependentVarsPrimalComponent(\n", + " base_clf=SingleClassFix(\n", + " MinProbabilityClassifier(\n", + " base_clf=LogisticRegression(),\n", + " thresholds=[0.95, 0.95],\n", + " ),\n", + " ),\n", + " extractor=AlvLouWeh2017Extractor(),\n", + " action=SetWarmStart(),\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Train ML models\n", + "solver.fit(train_data)\n", + "\n", + "# Solve a test instance\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e27d2cbd-5341-461d-bbc1-8131aee8d949", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/guide/solvers/index.html b/0.4/guide/solvers/index.html new file mode 100644 index 0000000..1de87b3 --- /dev/null +++ b/0.4/guide/solvers/index.html @@ -0,0 +1,494 @@ + + + + + + + + 9. Learning Solver — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    9. Learning Solver

    +

    On previous pages, we discussed various components of the MIPLearn framework, including training data collectors, feature extractors, and individual machine learning components. In this page, we introduce LearningSolver, the main class of the framework which integrates all the aforementioned components into a cohesive whole. Using LearningSolver involves three steps: (i) configuring the solver; (ii) training the ML components; and (iii) solving new MIP instances. In the following, we +describe each of these steps, then conclude with a complete runnable example.

    +
    +

    9.1. Configuring the solver

    +

    LearningSolver is composed by multiple individual machine learning components, each targeting a different part of the solution process, or implementing a different machine learning strategy. This architecture allows strategies to be easily enabled, disabled or customized, making the framework flexible. By default, no components are provided and LearningSolver is equivalent to a traditional MIP solver. To specify additional components, the components constructor argument may be used:

    +
    solver = LearningSolver(
    +    components=[
    +        comp1,
    +        comp2,
    +        comp3,
    +    ]
    +)
    +
    +
    +

    In this example, three components comp1, comp2 and comp3 are provided. The strategies implemented by these components are applied sequentially when solving the problem. For example, comp1 and comp2 could fix a subset of decision variables, while comp3 constructs a warm start for the remaining problem.

    +
    +
    +

    9.2. Training and solving new instances

    +

    Once a solver is configured, its ML components need to be trained. This can be achieved by the solver.fit method, as illustrated below. The method accepts a list of HDF5 files and trains each individual component sequentially. Once the solver is trained, new instances can be solved using solver.optimize. The method returns a dictionary of statistics collected by each component, such as the number of variables fixed.

    +
    # Build instances
    +train_data = ...
    +test_data = ...
    +
    +# Collect training data
    +bc = BasicCollector()
    +bc.collect(train_data, build_model)
    +
    +# Build solver
    +solver = LearningSolver(...)
    +
    +# Train components
    +solver.fit(train_data)
    +
    +# Solve a new test instance
    +stats = solver.optimize(test_data[0], build_model)
    +
    +
    +
    +
    +

    9.3. Complete example

    +

    In the example below, we illustrate the usage of LearningSolver by building instances of the Traveling Salesman Problem, collecting training data, training the ML components, then solving a new instance.

    +
    +
    [1]:
    +
    +
    +
    import random
    +
    +import numpy as np
    +from scipy.stats import uniform, randint
    +from sklearn.linear_model import LogisticRegression
    +
    +from miplearn.classifiers.minprob import MinProbabilityClassifier
    +from miplearn.classifiers.singleclass import SingleClassFix
    +from miplearn.collectors.basic import BasicCollector
    +from miplearn.components.primal.actions import SetWarmStart
    +from miplearn.components.primal.indep import IndependentVarsPrimalComponent
    +from miplearn.extractors.AlvLouWeh2017 import AlvLouWeh2017Extractor
    +from miplearn.io import write_pkl_gz
    +from miplearn.problems.tsp import (
    +    TravelingSalesmanGenerator,
    +    build_tsp_model_gurobipy,
    +)
    +from miplearn.solvers.learning import LearningSolver
    +
    +# Set random seed to make example reproducible.
    +random.seed(42)
    +np.random.seed(42)
    +
    +# Generate a few instances of the traveling salesman problem.
    +data = TravelingSalesmanGenerator(
    +    n=randint(low=10, high=11),
    +    x=uniform(loc=0.0, scale=1000.0),
    +    y=uniform(loc=0.0, scale=1000.0),
    +    gamma=uniform(loc=0.90, scale=0.20),
    +    fix_cities=True,
    +    round=True,
    +).generate(50)
    +
    +# Save instance data to data/tsp/00000.pkl.gz, data/tsp/00001.pkl.gz, ...
    +all_data = write_pkl_gz(data, "data/tsp")
    +
    +# Split train/test data
    +train_data = all_data[:40]
    +test_data = all_data[40:]
    +
    +# Collect training data
    +bc = BasicCollector()
    +bc.collect(train_data, build_tsp_model_gurobipy, n_jobs=4)
    +
    +# Build learning solver
    +solver = LearningSolver(
    +    components=[
    +        IndependentVarsPrimalComponent(
    +            base_clf=SingleClassFix(
    +                MinProbabilityClassifier(
    +                    base_clf=LogisticRegression(),
    +                    thresholds=[0.95, 0.95],
    +                ),
    +            ),
    +            extractor=AlvLouWeh2017Extractor(),
    +            action=SetWarmStart(),
    +        )
    +    ]
    +)
    +
    +# Train ML models
    +solver.fit(train_data)
    +
    +# Solve a test instance
    +solver.optimize(test_data[0], build_tsp_model_gurobipy)
    +
    +
    +
    +
    +
    +
    +
    +
    +Restricted license - for non-production use only - expires 2024-10-28
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 10 rows, 45 columns and 90 nonzeros
    +Model fingerprint: 0x6ddcd141
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [4e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Presolve time: 0.00s
    +Presolved: 10 rows, 45 columns, 90 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.3600000e+02   1.700000e+01   0.000000e+00      0s
    +      15    2.7610000e+03   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 15 iterations and 0.00 seconds (0.00 work units)
    +Optimal objective  2.761000000e+03
    +
    +User-callback calls 56, time in user-callback 0.00 sec
    +Set parameter PreCrush to value 1
    +Set parameter LazyConstraints to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 10 rows, 45 columns and 90 nonzeros
    +Model fingerprint: 0x74ca3d0a
    +Variable types: 0 continuous, 45 integer (45 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [4e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +
    +User MIP start produced solution with objective 2796 (0.00s)
    +Loaded user MIP start with objective 2796
    +
    +Presolve time: 0.00s
    +Presolved: 10 rows, 45 columns, 90 nonzeros
    +Variable types: 0 continuous, 45 integer (45 binary)
    +
    +Root relaxation: objective 2.761000e+03, 14 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 2761.00000    0    - 2796.00000 2761.00000  1.25%     -    0s
    +     0     0     cutoff    0      2796.00000 2796.00000  0.00%     -    0s
    +
    +Cutting planes:
    +  Lazy constraints: 3
    +
    +Explored 1 nodes (16 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: 2796
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 2.796000000000e+03, best bound 2.796000000000e+03, gap 0.0000%
    +
    +User-callback calls 110, time in user-callback 0.00 sec
    +
    +
    +
    +
    [1]:
    +
    +
    +
    +
    +{'WS: Count': 1, 'WS: Number of variables set': 41.0}
    +
    +
    +
    +
    [ ]:
    +
    +
    +
    
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/index.html b/0.4/index.html new file mode 100644 index 0000000..7cb5bd5 --- /dev/null +++ b/0.4/index.html @@ -0,0 +1,386 @@ + + + + + + + + MIPLearn — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + +
    + +
    + Contents +
    + +
    +
    +
    +
    +
    + +
    + +
    +

    MIPLearn

    +

    MIPLearn is an extensible framework for solving discrete optimization problems using a combination of Mixed-Integer Linear Programming (MIP) and Machine Learning (ML). MIPLearn uses ML methods to automatically identify patterns in previously solved instances of the problem, then uses these patterns to accelerate the performance of conventional state-of-the-art MIP solvers such as CPLEX, Gurobi or XPRESS.

    +

    Unlike pure ML methods, MIPLearn is not only able to find high-quality solutions to discrete optimization problems, but it can also prove the optimality and feasibility of these solutions. Unlike conventional MIP solvers, MIPLearn can take full advantage of very specific observations that happen to be true in a particular family of instances (such as the observation that a particular constraint is typically redundant, or that a particular variable typically assumes a certain value). For certain classes of problems, this approach may provide significant performance benefits.

    +
    +

    Contents

    + + + +
    +
    +

    Authors

    +
      +
    • Alinson S. Xavier (Argonne National Laboratory)

    • +
    • Feng Qiu (Argonne National Laboratory)

    • +
    • Xiaoyi Gu (Georgia Institute of Technology)

    • +
    • Berkay Becu (Georgia Institute of Technology)

    • +
    • Santanu S. Dey (Georgia Institute of Technology)

    • +
    +
    +
    +

    Acknowledgments

    +
      +
    • Based upon work supported by Laboratory Directed Research and Development (LDRD) funding from Argonne National Laboratory, provided by the Director, Office of Science, of the U.S. Department of Energy.

    • +
    • Based upon work supported by the U.S. Department of Energy Advanced Grid Modeling Program.

    • +
    +
    +
    +

    Citing MIPLearn

    +

    If you use MIPLearn in your research (either the solver or the included problem generators), we kindly request that you cite the package as follows:

    +
      +
    • Alinson S. Xavier, Feng Qiu, Xiaoyi Gu, Berkay Becu, Santanu S. Dey. MIPLearn: An Extensible Framework for Learning-Enhanced Optimization (Version 0.3). Zenodo (2023). DOI: https://doi.org/10.5281/zenodo.4287567

    • +
    +

    If you use MIPLearn in the field of power systems optimization, we kindly request that you cite the reference below, in which the main techniques implemented in MIPLearn were first developed:

    + +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/objects.inv b/0.4/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..6d9f27f84abb26f2c066bcb7cea87ed67a26f10c GIT binary patch literal 4746 zcmV;55_Rn(AX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkSNl;8> zVRCK?BOq2~a&u{KZaN?eBOp|0Wgv28ZDDC{WMy(7Z)PBLXlZjGW@&6?AZc?TV{dJ6 za%FRKWn>_Ab7^j8AbMb?@~>aq1WB9?$S*S_(f#$;IC@AB=xM69MrF4zwsqO})pdoszG$j_ z+a0d5_TjhYup4WXJ&rNjifAAKndQL2*^~)i%8S7XZae@_8z*`Zv5C zN0f(n*0hH`+7CT$*{Uh)qTCMEVXy1rLek`-0jYSszdm$Zgoo#S)!@Oo_I5%BLKZFR z@Rdg6crZ5ZEq|9?RbJJoPe4_LOG)#bo}P;G7xZ!hzFORr#4phBsqC7Iq3nhPd{wv< zG|@c<84`4V22{MJUyE9e@Y@DkTp{c}y2R@7RbVio|55SAuJ$|BCa7JCQ~XXo5&qZ@ zc#!q!YS{-qQEhvfimdg24%I$IpQbp)ALKJ_ecOQ()wIJ%WF0rCNl?QWr8q;)p{u^) z8<=zzY{gAfzjhn()r-Nq8}#AOUL5LC-larU$N~?`?fan{x6xED5V_$>$aM{vkSvEh z6@a>1(pW?YU35QmCA;5!QGa=V7(b!gykLh=_4l!0?TjhR>zLB{vWu#;HL9X8QG40C`igzb(=s{GQny`|!qt!r~wzHam zwb$**s)24%jqer>t#f-q&12z38%Qm^IDWW4^k~J)r9~VoyRLiyXfI$jyTtW@a9xjBs%jrJHSU6K#(Jne@5{E2sQ4L3aYoMm<{%9@@A^M>gjFwiV_#lGg@jFFZjV3sih?a2^D}*e0ul# zuh*BSA3t0WxN;b}>T1Nl+R>1&UcY(q@%@LysPA9=_dm%X%iWGY#I5gwL4f?J>$1Ea zP+ zB(8$xI><+=hNYQgNdDEKE(leQ@y!OnY0JXhK=~970K<}FxiCN{i8JsZdE@XgX3qnZxk`CU7Np0t|=q%Z1nUQ(pD;(6J2MS1^jJTqK@^vqTG3u=}8_UnuJ@ z^A)y&#fTDbJJGh5yBcoj1< zlMKnbI`{&o+Y@#W5fqF|$3ldod8ZlhK}*+S0n+%KU@!-+r;CS3<9Xab^G$+P6_fgwto7{si(>(x zKKE!KZ&T^Z)MyuHJDdaMQ#g>Ut+acqfqJ2i@lD9=Ytbdaj#>9)1NBlmrf$T13?Ehe zc!;E)8(wF`yF7ob5v&{PB-_qiP20Kg4`83F|9b6ukJNt*hg0IB(eW7z1kx^LzH%?t7IR0BhwkA>d5v4 z4^YLVzNP5J-uWgHV5D_ErVG0>V8kfH{OoSfaA+g(N;GsU(tmqu|0- zAs+TLxks{Q(j*@V)=1+0e@$^?t5i zhnTV&+MV)-af<+X^6YbwG&zb>0!WhOaEs)~KwJVqhAiJ%Btr(`5&$wJ*}@|E5eR!R z1XPcCb>}@ME*8lrTc5sX)jJ@ETdrG+ATTmwasS>@gvhxQeO3%j%SU zCdl%nW-Ih&z;e3;Ghk}ALT?51DLQ-+=(4wIU#(3=6E zH%w}_LT?6;-k|7NOZ`azdV`{8EwzvkS&2OC3en{SJGz>~$j^wXVYqHLIm>_KXLF`v zD$rP6{1D!r*zB!^m18$}bqD~*!pboNmq{eFzqT?496*Xioa+S+bKg<6510SKqt%*TVgJd zFIGgDnwLkxxw3-13r3HJ0C+t+3m@sOjA+3cu`U3kfmbRvn3|VIUYW>(Jn}+>sRen& zzr(whE>E176oekI+ZYqk`HOn7l^Nm0L3nlQlgfF@`I4k@nSX;Qu5!+KN%_$udElDz z{*LDJ>S-Q0j0K=+$xRm|9YY(~VV%kF!d1MyAPMURk+81k3G2eE-kd-$Ko(9ISm};z zeW40o{4GiKg1B37LWF%ekNXOTACXNkyo2Zy5_-yckyNt-Hr1XDOXXrXXRmyNFg2Y= zi>KICQeqo-m#(oSgvdNqFNDq_*f>;nm{pAq*Br`0;Jkir$4KY4$LAJ-OHadn_R#UD*D5(0@QbtN7?9$G#iKJW^ z%ih(mR5s=`hvI<#a+#qwzWG@_<-inYsRyI!&d>lla5pqb597VjfWlRi`PQcQtcCj3 zrt(U=PB7A~+0wvJO}Jz8G?ej9&GA55aA#&|bK`xP!9kSZp2!@G@m@%%Pz5-9mO?b1 zI>QN)hLe_wh~s%ns8EGC_G)_6G)}09Ks~ricekiRshUJd`yQ?m>%$IRm-qoJnHwV2 zJV-0n84}=TypEe!16SYDxTQ(|XfFd}&j768LoBm2MuS1#<@cMBq$vAFKg$0CMka z%cvhB3}YO8zidp~?=1-a=2GRD6^@Rsx-D*sbJ z9I%2Ao~D0E@D+r!)bE50W;);Y=ph`z7!vE8|N9WEp>bd_^}YIAFbiLosM_=Ji z|Gr_u+t3_+xgoFg3V-u*Ax_V;?UX0lV1~S-&UIPy)+SylqSOnK63+mQ?#rfKZtz*d zzUE^5+X#2NyFSTD{-APjLB4zS<|Kc^M9Fc+j*ZaO?wJdch`1P%7gJvlnw&P%RP3@KdRtE0O!==DS2q+HKFyt4NE~Hv*wO6YV{|ARpG@5g@Qoo4w<*iE z)~=-kPKFHKZvn+mjeokcMCEmpPj%y8U5@m{OH=C{#hOIKT0@3zKS66c{Q|<)O`VOEA7v~B!YuRU8o<`x9LUt#I+)%Arx!pbLS%1k&aUN#j3G;4y$ZXYvFq7( z%J!i#YiL(tLTT0Bpnq)FKX&L(sf2Qp8YZX$es?M>5*d;3s}#CLH=NEN7ByNHKw-)> z5=7z5qBfQJQCI@x+U@Xj`{yQ|^M%Ds?s)gG>|=5o@3siQSBF@Fh;6Ht4#m0MY5}~F z!1=jt!}&rqxaF$_N%b=Yse+Lm85FtA8|!kNN_7eMMiEqw4Y0<*DBy^I7tB*9&Qm`iH=$?QU*&#x zXfly;KD(Kg|C(Trov_D_Ccq*;IzfK)bMhxA&`%I2Pn@tPZYO_sg8b~~s_V~U_ zzv6z0O!qkEuM|D8;W1kkWq-;g_F4|M=g7&T_3tZgRTgCSxbrsoJJu z;@SA5BA2X-*9n<;bE5&Ee+g2fflNvnZ@lF+_UTesT*%NoI4o`KudAD}Bh541;+~>; zYqheZRZ%Wy_;S`;JQMTvKITMK<@{}!SJlaRRXnb#5pujocXI6aR+}H(>T;qLi)36R z!y+<<;KxBSPjbKBypWy!eonnmBsO>$?+5NoVz;4guo*;; zvX4FPKy+OtX*If@k1b5XZAo}pZEX0D0`2$eyuF|C&0CZDRFH{+Y)5zW+4;y(G-9{% z^>R@ollN;yIjTlcMK0^~WxR+1>)!-1vEoVT%o9z>(GVl$GWkoyB4tx~(VEk9H9Mp>cx-+KK5J4z%+ei6*Th-2G`u#!Ipv-twX+{#J2-#P!%K zKS|?yZpnkp=7k*f%B z*!`|*t0zGWhVHN%w@?O(wt5o4K*r!Q?<;bXm-B@w)D>3o`iB+V9;iZYfVTvV{Aa7Y-Tw;j0wwt5T;m88@?E)Zx*@~KN5y5 z=EOG$S?$B2IfTV5s-g*6=j8emPh*i`i*ZO7w^U}afs1oX8oB2D7GZ}LJ_)9`Kw-@n z+pLiI#TkJD*xYt_1e3M_AxyQMHimIl-zKOWZ2kw&l*I0^)%^9dvfwfM zUR{VcPr1KF(EgpXk`Z{RvC5^d=08~qTi2&3Ymt|Fs{|)FK+&1A#oQ&odPEma-G8@S zEvmu>tXrP1@mqsf*NLjDeb%bxRe$p-cb%rCDrb|pYhB~y`&6e~ewuZ}qgzws?L#gK zj>sS!@yKI!uasC+&L4(bJU(ZdJ+`;Kohz|xxcyn#d3l4rGtBR(^r6$A?uRM870}d< Y{T9g2ko6(T5uN-jIBt3V4>aX!rMC`AUH||9 literal 0 HcmV?d00001 diff --git a/0.4/py-modindex/index.html b/0.4/py-modindex/index.html new file mode 100644 index 0000000..ffc9f0f --- /dev/null +++ b/0.4/py-modindex/index.html @@ -0,0 +1,378 @@ + + + + + + + Python Module Index — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
    +
    + +
    + + +

    Python Module Index

    + +
    + m +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
     
    + m
    + miplearn +
        + miplearn.classifiers.minprob +
        + miplearn.classifiers.singleclass +
        + miplearn.collectors.basic +
        + miplearn.components.primal.actions +
        + miplearn.components.primal.expert +
        + miplearn.components.primal.indep +
        + miplearn.components.primal.joint +
        + miplearn.components.primal.mem +
        + miplearn.extractors.AlvLouWeh2017 +
        + miplearn.extractors.fields +
        + miplearn.h5 +
        + miplearn.io +
        + miplearn.problems.binpack +
        + miplearn.problems.multiknapsack +
        + miplearn.problems.pmedian +
        + miplearn.problems.setcover +
        + miplearn.problems.setpack +
        + miplearn.problems.stab +
        + miplearn.problems.tsp +
        + miplearn.problems.uc +
        + miplearn.problems.vertexcover +
        + miplearn.solvers.abstract +
        + miplearn.solvers.gurobi +
        + miplearn.solvers.learning +
    + + +
    + + +
    + + +
    + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/search/index.html b/0.4/search/index.html new file mode 100644 index 0000000..b4a5445 --- /dev/null +++ b/0.4/search/index.html @@ -0,0 +1,272 @@ + + + + + + + Search — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + +
    + + +
    + +
    +
    +
    +
    +
    + +
    + +

    Search

    + + + + +

    + Searching for multiple words only shows matches that contain + all words. +

    + + +
    + + + +
    + + + +
    + +
    + + +
    + + +
    + + +
    + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/searchindex.js b/0.4/searchindex.js new file mode 100644 index 0000000..846edcf --- /dev/null +++ b/0.4/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"docnames": ["api/collectors", "api/components", "api/helpers", "api/problems", "api/solvers", "guide/collectors", "guide/features", "guide/primal", "guide/problems", "guide/solvers", "index", "tutorials/cuts-gurobipy", "tutorials/getting-started-gurobipy", "tutorials/getting-started-jump", "tutorials/getting-started-pyomo"], "filenames": ["api/collectors.rst", "api/components.rst", "api/helpers.rst", "api/problems.rst", "api/solvers.rst", "guide/collectors.ipynb", "guide/features.ipynb", "guide/primal.ipynb", "guide/problems.ipynb", "guide/solvers.ipynb", "index.rst", "tutorials/cuts-gurobipy.ipynb", "tutorials/getting-started-gurobipy.ipynb", "tutorials/getting-started-jump.ipynb", "tutorials/getting-started-pyomo.ipynb"], "titles": ["11. Collectors & Extractors", "12. Components", "14. Helpers", "10. Benchmark Problems", "13. Solvers", "6. Training Data Collectors", "7. Feature Extractors", "8. Primal Components", "5. Benchmark Problems", "9. Learning Solver", "MIPLearn", "4. User cuts and lazy constraints", "2. Getting started (Gurobipy)", "3. Getting started (JuMP)", "1. Getting started (Pyomo)"], "terms": {"class": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "minprobabilityclassifi": [0, 7, 9], "base_clf": [0, 1, 7, 9], "type": [0, 1, 4, 5, 6, 8, 9, 11, 12, 13, 14], "ani": [0, 1, 2, 4, 5, 7, 8, 12, 13, 14], "threshold": [0, 1, 7, 9], "list": [0, 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 14], "float": [0, 1, 4, 5, 12, 14], "clone_fn": [0, 1, 7], "callabl": [0, 1, 4], "function": [0, 1, 5, 6, 7, 8, 11, 12, 13, 14], "clone": [0, 1, 7], "base": [0, 1, 2, 4, 6, 7, 10, 12, 13, 14], "baseestim": 0, "meta": [0, 7], "return": [0, 1, 6, 7, 8, 9, 11, 12, 13, 14], "nan": 0, "predict": [0, 1, 7, 11, 12, 13, 14], "made": [0, 7], "have": [0, 3, 5, 6, 8, 11, 12, 13, 14], "probabl": [0, 3, 7, 8, 12, 13, 14], "below": [0, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "given": [0, 4, 5, 6, 7, 8, 11, 12, 13, 14], "more": [0, 3, 5, 6, 7, 8, 11, 12, 13, 14], "specif": [0, 3, 5, 6, 7, 8, 10, 12, 13, 14], "thi": [0, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "call": [0, 8, 9, 11, 13], "predict_proba": [0, 7], "compar": [0, 5, 7, 11], "result": [0, 3, 8], "against": 0, "provid": [0, 1, 3, 5, 7, 8, 9, 10, 11, 12, 13, 14], "If": [0, 3, 7, 8, 10, 11, 12, 13, 14], "one": [0, 1, 3, 7, 8, 11, 12, 13, 14], "i": [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "abov": [0, 1, 3, 5, 7, 8, 11, 12, 13, 14], "its": [0, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "otherwis": [0, 1, 5, 6, 7], "fit": [0, 1, 4, 7, 9, 11, 12, 13, 14], "x": [0, 3, 5, 7, 8, 9, 11, 12, 13, 14], "ndarrai": [0, 1, 2, 3, 4, 11], "y": [0, 3, 5, 7, 8, 9, 11, 12, 13, 14], "none": [0, 1, 2, 4, 11], "set_fit_request": 0, "bool": [0, 2, 3, 4], "str": [0, 1, 2, 3, 4, 5, 11, 12, 14], "unchang": 0, "request": [0, 10, 12, 13, 14], "metadata": [0, 5], "pass": 0, "method": [0, 3, 5, 6, 7, 8, 9, 10, 12, 13, 14], "note": [0, 3, 6, 7, 8, 11, 12, 13, 14], "onli": [0, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14], "relev": [0, 6], "enable_metadata_rout": 0, "true": [0, 3, 5, 6, 8, 9, 10, 11], "see": [0, 7, 8, 11, 12, 13, 14], "sklearn": [0, 7, 9, 11, 12, 13, 14], "set_config": 0, "pleas": [0, 8, 12, 13, 14], "user": [0, 3, 6, 7, 8, 9, 12, 13, 14], "guid": [0, 12, 13, 14], "how": [0, 5, 7, 8, 11, 12, 13, 14], "rout": [0, 8, 11], "mechan": 0, "work": [0, 8, 9, 10, 11, 12, 13, 14], "The": [0, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "option": [0, 1], "each": [0, 1, 3, 6, 7, 8, 9, 11, 12, 13, 14], "paramet": [0, 3, 7, 8, 9, 11, 14], "ar": [0, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "ignor": 0, "fals": [0, 2, 3, 4, 8], "estim": [0, 7], "rais": 0, "an": [0, 6, 7, 8, 10, 11, 12, 13, 14], "error": [0, 5], "should": [0, 1, 6, 8, 11, 12, 13, 14], "alia": 0, "instead": [0, 3, 7, 8, 11, 14], "origin": [0, 5, 12, 13, 14], "name": [0, 4, 5, 11, 12], "default": [0, 3, 4, 8, 9], "util": [0, 12, 13, 14], "metadata_rout": 0, "retain": 0, "exist": [0, 8], "allow": [0, 5, 6, 7, 9, 12, 13, 14], "you": [0, 6, 8, 10, 13], "chang": [0, 8, 11, 12, 13, 14], "some": [0, 3, 5, 6, 7, 8, 11, 12, 13, 14], "other": [0, 5, 6, 7, 8, 11, 12, 13, 14], "new": [0, 3, 5, 6, 7, 8, 10, 12, 13, 14], "version": [0, 5, 8, 9, 10, 11, 12, 13, 14], "1": [0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "3": [0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "us": [0, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "sub": 0, "e": [0, 5, 6, 7, 8, 11, 12, 13, 14], "g": [0, 5, 7, 8, 12, 13, 14], "insid": [0, 11], "pipelin": 0, "ha": [0, 3, 4, 6, 7, 8, 11], "effect": [0, 11], "self": 0, "updat": 0, "object": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14], "set_predict_request": 0, "singleclassfix": [0, 7, 9], "logist": [0, 7, 8], "regress": [0, 7], "issu": [0, 6, 7, 12, 13, 14], "dataset": [0, 7, 11], "contain": [0, 7, 8, 11, 12, 13, 14], "singl": [0, 1, 5, 7, 12, 13, 14], "fix": [0, 3, 7, 8, 9, 12, 13, 14], "train": [0, 1, 6, 7, 8, 10], "data": [0, 3, 6, 7, 8, 9, 10], "alwai": [0, 7], "basiccollector": [0, 5, 6, 9, 11, 12, 13, 14], "skip_lp": [0, 4], "write_mp": 0, "collect": [0, 5, 6, 7, 8, 9, 11, 12, 13, 14], "filenam": [0, 2, 4, 5, 11], "build_model": [0, 4, 9], "n_job": [0, 2, 5, 6, 9, 11, 12, 14], "int": [0, 1, 2, 3, 11, 12, 13, 14], "progress": [0, 2], "verbos": [0, 11], "h5fieldsextractor": [0, 7, 10, 11, 12, 13, 14], "instance_field": [0, 6, 7, 11, 12, 13, 14], "var_field": [0, 6], "constr_field": [0, 6], "featuresextractor": [0, 1], "get_constr_featur": [0, 6], "h5": [0, 4, 5, 6, 11, 12, 13, 14], "h5file": [0, 2, 4, 5, 6], "get_instance_featur": [0, 6], "get_var_featur": [0, 6], "alvlouweh2017extractor": [0, 7, 9, 10], "with_m1": 0, "with_m2": 0, "with_m3": 0, "comput": [0, 1, 6, 7, 10, 11, 12, 14], "static": [0, 4, 6], "variabl": [0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "featur": [0, 4, 5, 7, 9, 10], "describ": [0, 3, 5, 6, 7, 8, 9, 11], "alvarez": [0, 6], "A": [0, 6, 7, 12, 14], "m": [0, 3, 6, 8, 11], "louveaux": [0, 6], "q": 0, "wehenkel": [0, 6], "l": 0, "2017": [0, 6], "machin": [0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "learn": [0, 5, 6, 7, 8, 10, 12, 13, 14], "approxim": [0, 6], "strong": [0, 6, 11], "branch": [0, 5, 6, 11], "inform": [0, 5, 6, 10, 11], "journal": [0, 6, 8, 10], "29": [0, 6, 8], "185": [0, 6, 8], "195": [0, 6], "enforceproxim": [1, 7], "tol": 1, "primalcomponentact": 1, "perform": [1, 5, 6, 7, 8, 10, 11, 12, 13, 14], "model": [1, 3, 4, 5, 6, 7, 8, 9, 10], "abstractmodel": [1, 4], "var_nam": [1, 4], "var_valu": [1, 4], "stat": [1, 3, 4, 5, 6, 8, 9, 11, 12, 14], "dict": [1, 4], "fixvari": [1, 7], "abc": [1, 4], "abstract": [1, 6, 12, 13, 14], "setwarmstart": [1, 7, 9, 12, 13, 14], "expertprimalcompon": [1, 7], "before_mip": 1, "test_h5": 1, "train_h5": 1, "independentvarsprimalcompon": [1, 7, 9], "extractor": [1, 7, 9, 10, 11, 12, 13, 14], "jointvarsprimalcompon": [1, 7], "clf": [1, 7, 11, 12, 13, 14], "memorizingprimalcompon": [1, 7, 12, 13, 14], "constructor": [1, 3, 7, 9, 12, 13, 14], "solutionconstructor": 1, "memor": [1, 10, 12, 13, 14], "all": [1, 3, 5, 6, 7, 8, 9, 12, 13, 14], "solut": [1, 4, 5, 6, 7, 8, 9, 10, 11], "seen": [1, 7], "dure": [1, 7, 8, 11, 12, 13, 14], "classifi": [1, 7, 9, 12, 13, 14], "which": [1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "solver": [1, 5, 7, 8, 10, 11, 12, 13, 14], "combin": [1, 3, 7, 8, 10], "multipl": [1, 5, 7, 8, 9, 12, 13, 14], "partial": [1, 5, 7, 12, 13, 14], "mergetopsolut": [1, 7, 12, 13, 14], "k": [1, 3, 6, 7, 8, 12, 13, 14], "warm": [1, 7, 9, 12, 13, 14], "start": [1, 7, 8, 9, 10, 11], "construct": [1, 7, 9, 12, 13, 14], "strategi": [1, 7, 9, 11, 12, 13, 14], "first": [1, 3, 5, 7, 8, 10, 11, 12, 13, 14], "select": [1, 7, 8, 11], "top": [1, 5, 7], "merg": [1, 7, 12, 13, 14], "them": [1, 6, 7, 8, 11, 12, 13, 14], "To": [1, 3, 7, 8, 9, 11, 12, 13, 14], "mean": [1, 7], "optim": [1, 3, 4, 5, 6, 7, 8, 9, 10, 11], "valu": [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "decis": [1, 5, 6, 7, 8, 9, 11, 12, 13, 14], "set": [1, 3, 5, 6, 7, 9, 10, 11, 12, 13, 14], "zero": [1, 7, 11], "0": [1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "ii": [1, 5, 6, 7, 8, 9], "iii": [1, 6, 9], "leav": 1, "free": [1, 6, 7], "y_proba": 1, "selecttopsolut": [1, 7], "gzip": [2, 5, 12, 14], "read_pkl_gz": [2, 11, 12, 14], "write_pkl_gz": [2, 5, 6, 9, 11, 12, 14], "obj": [2, 5, 8, 9, 11, 12, 13, 14], "dirnam": 2, "prefix": [2, 5], "mode": [2, 3, 13], "r": [2, 5, 8, 9, 11, 12, 14], "close": [2, 7, 12, 13, 14], "get_arrai": [2, 5], "kei": [2, 5], "get_byt": 2, "byte": 2, "bytearrai": 2, "get_scalar": [2, 5], "get_spars": [2, 5], "coo_matrix": 2, "put_arrai": [2, 5], "put_byt": [2, 5], "put_scalar": [2, 5], "put_spars": [2, 5], "binpackdata": 3, "size": [3, 6, 7, 8, 11], "capac": [3, 8], "bin": [3, 10, 13], "pack": [3, 10], "numpi": [3, 5, 6, 8, 9, 11, 12, 14], "item": [3, 8], "binpackgener": [3, 8], "n": [3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "rv_frozen": 3, "sizes_jitt": [3, 8], "capacity_jitt": [3, 8], "fix_item": [3, 8], "random": [3, 5, 6, 9, 11, 12, 13, 14], "instanc": [3, 5, 6, 7, 10], "gener": [3, 5, 6, 7, 9, 10], "sampl": [3, 8, 12, 13, 14], "distribut": [3, 8, 12, 13, 14], "decid": [3, 7, 8, 12, 13, 14], "respect": [3, 5, 6, 7, 8, 12, 13, 14], "number": [3, 5, 6, 8, 9, 11, 12, 13, 14], "independ": [3, 6, 8, 10, 11], "creat": [3, 5, 6, 7, 8, 11], "refer": [3, 6, 8], "previous": [3, 5, 6, 7, 8, 10, 12, 13, 14], "addit": [3, 8, 9, 11, 12, 13, 14], "perturb": [3, 8, 11], "s_i": [3, 8], "gamma_i": [3, 8], "where": [3, 7, 8, 12, 13, 14], "th": [3, 7, 8], "from": [3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "similarli": [3, 8], "b": [3, 5, 8], "beta": [3, 8], "remain": [3, 5, 6, 7, 8, 9], "same": [3, 6, 7, 8, 11], "across": [3, 7, 8], "appli": [3, 6, 7, 8, 9, 12, 13, 14], "complet": [3, 6, 7, 8, 10, 11], "differ": [3, 7, 8, 9, 11, 12, 13, 14], "n_sampl": 3, "build_binpack_model": [], "gurobimodel": [3, 4, 11, 12], "convert": [3, 6, 11, 12, 13, 14], "concret": [3, 12, 13, 14], "gurobipi": [3, 10, 11, 13, 14], "multiknapsackdata": 3, "price": [3, 5, 8], "weight": [3, 8, 11], "multi": [3, 5, 6, 10], "dimension": [3, 5, 6, 10], "knapsack": [3, 6, 10], "matrix": [3, 5, 6, 8, 9, 11, 12, 13, 14], "multiknapsackgener": [3, 6, 8], "scipi": [3, 5, 6, 8, 9, 11, 12, 14], "_distn_infrastructur": 3, "rv_discrete_frozen": 3, "w": [3, 5, 6, 8, 9, 12], "u": [3, 6, 8, 10, 12, 13, 14], "rv_continuous_frozen": 3, "alpha": [3, 6, 8], "fix_w": [3, 6, 8], "w_jitter": [3, 6, 8], "p_jitter": [3, 6, 8], "round": [3, 5, 6, 8, 9, 11], "constraint": [3, 5, 6, 7, 8, 9, 10, 12, 13, 14], "specifi": [3, 6, 7, 8, 9, 11], "j": [3, 7, 8, 11], "alpha_j": 3, "sum": [3, 8, 12, 13, 14], "rang": [3, 8, 9, 11, 12, 13, 14], "tight": [3, 8], "ratio": [3, 8], "make": [3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "challeng": [3, 8, 12, 13, 14], "cost": [3, 5, 6, 7, 8, 12, 13, 14], "linearli": [3, 8], "correl": [3, 7, 8], "averag": [3, 7, 8], "u_i": 3, "coeffici": [3, 4, 5, 6, 8, 9, 11, 12, 13, 14], "multipli": [3, 8], "onc": [3, 8, 9, 11], "entir": [3, 5, 6, 7, 8, 11, 12, 13, 14], "kept": [3, 8], "also": [3, 5, 6, 7, 8, 10, 11, 12, 13, 14], "impli": [3, 8], "although": [3, 5, 8, 12, 13, 14], "deriv": [3, 6, 8, 11], "long": [3, 8], "constant": [3, 5, 7, 8], "still": [3, 7, 8, 11, 12, 13, 14], "ident": [3, 8], "gamma": [3, 5, 8, 9, 11], "when": [3, 5, 7, 8, 9, 12, 13, 14], "argument": [3, 7, 8, 9], "mai": [3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "roughli": [3, 8], "exactli": [3, 8, 11, 12, 13, 14], "calcul": [3, 8], "By": [3, 8, 9, 12, 13, 14], "nearest": [3, 7, 8, 12, 13, 14], "integ": [3, 4, 5, 8, 9, 10, 11, 12, 13, 14], "disabl": [3, 8, 9], "rv_discret": 3, "rv_continu": 3, "profit": 3, "boolean": 3, "minu": 3, "nois": 3, "ad": [3, 5, 11, 14], "build_multiknapsack_model": [], "pmediandata": 3, "distanc": [3, 7, 8, 11], "demand": [3, 8, 12, 13, 14], "p": [3, 10, 12, 13, 14], "capacit": [3, 10], "median": [3, 10], "between": [3, 6, 7, 8, 11, 12, 13, 14], "custom": [3, 8, 9], "facil": [3, 8], "need": [3, 5, 6, 8, 9, 11, 12, 13, 14], "chosen": [3, 8], "pmediangener": [3, 8], "distances_jitt": [3, 8], "demands_jitt": [3, 8], "capacities_jitt": [3, 8], "Then": [3, 7, 8], "build": [3, 5, 6, 7, 8, 9, 11, 12, 13, 14], "geograph": [3, 8], "locat": [3, 5, 8, 11], "xi": 3, "yi": 3, "For": [3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "final": [3, 7, 8, 11, 12, 13, 14], "euclidean": [3, 8], "factor": [3, 8], "slightli": [3, 8, 11], "coordin": 3, "point": [3, 5, 12, 13, 14], "scale": [3, 5, 6, 8, 9, 10, 11, 12, 13, 14], "build_pmedian_model": [], "setcoverdata": 3, "incidence_matrix": [3, 8], "setpackdata": 3, "maxweightstablesetdata": 3, "graph": [3, 8, 11], "networkx": [3, 11], "maxweightstablesetgener": [3, 8], "fix_graph": [3, 8], "maximum": [3, 8, 11], "stabl": [3, 5, 10, 11], "two": [3, 6, 7, 8, 11, 12, 13, 14], "oper": [3, 8, 12, 13, 14], "erd\u0151": [3, 8], "r\u00e9nyi": [3, 8], "g_": [3, 8], "w_v": [3, 8], "wai": [3, 7, 8], "travelingsalesmandata": [3, 11], "n_citi": [3, 11], "travelingsalesmangener": [3, 5, 8, 9, 11], "fix_citi": [3, 5, 8, 9, 11], "travel": [3, 5, 9, 10], "salesman": [3, 5, 9, 10], "unitcommitmentdata": [3, 12, 13, 14], "min_pow": [3, 8], "max_pow": [3, 8], "min_uptim": [3, 8], "min_downtim": [3, 8], "cost_startup": [3, 8], "cost_prod": [3, 8], "cost_fix": [3, 8], "build_uc_model": [12, 13, 14], "unit": [3, 9, 10, 11, 12, 13, 14], "commit": [3, 10, 12, 13, 14], "accord": 3, "equat": 3, "5": [3, 5, 6, 8, 11, 12, 13, 14], "bendotti": [3, 8], "fouilhoux": [3, 8], "rottner": [3, 8], "c": [3, 5, 8, 12, 13, 14], "min": [3, 5, 8, 11, 12, 13, 14], "up": [3, 5, 8, 9, 11, 12, 13, 14], "down": [3, 5, 8], "polytop": [3, 8], "comb": [3, 8], "36": [3, 8], "1024": [3, 8], "1058": [3, 8], "2018": [3, 8], "http": [3, 8, 10], "doi": [3, 8, 10], "org": [3, 8, 10], "10": [3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "1007": [3, 8], "s10878": [3, 8], "018": [3, 8], "0273": [3, 8], "minweightvertexcoverdata": 3, "where_cut": 4, "cut": [4, 8, 9, 10, 12, 14], "where_default": 4, "where_lazi": 4, "lazi": [4, 8, 9, 10], "add_constr": [4, 11], "constrs_lh": 4, "constrs_sens": 4, "constrs_rh": 4, "extract_after_load": 4, "extract_after_lp": 4, "extract_after_mip": 4, "fix_vari": 4, "lazy_enforc": [4, 11], "violat": [4, 11], "relax": [4, 5, 6, 8, 9, 11, 12, 13, 14], "set_cut": 4, "set_warm_start": 4, "write": [4, 5, 12, 13, 14], "inner": [4, 11, 12, 13, 14], "lazy_separ": [4, 11], "cuts_separ": [4, 11], "cuts_enforc": [4, 11], "constr": 4, "just": [4, 5, 12, 13, 14], "been": [4, 6, 7, 8, 11], "load": [4, 5, 7, 8, 9, 12, 13, 14], "extract": [4, 5, 6, 7], "problem": [4, 5, 6, 7, 9, 10], "etc": [4, 12, 13, 14], "linear": [4, 5, 6, 8, 10, 12, 13, 14], "program": [4, 10, 11, 12, 13, 14], "solv": [4, 5, 6, 7, 10], "dynam": 4, "lp": [4, 5, 6], "basi": [4, 5], "statu": [4, 5], "mix": [4, 5, 8, 10, 11, 12, 13, 14], "mip": [4, 5, 7, 8, 9, 10, 12, 13, 14], "set_time_limit": 4, "time_limit_sec": 4, "learningsolv": [4, 9, 11, 12, 13, 14], "compon": [4, 6, 9, 10, 11, 12, 13, 14], "data_filenam": 4, "step": [5, 6, 8, 9, 12, 13, 14], "assist": 5, "supervis": [5, 6], "larg": [5, 7, 8, 10, 12, 13, 14], "raw": [5, 6], "In": [5, 6, 7, 8, 9, 11, 12, 13, 14], "section": [5, 7, 8], "we": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "variou": [5, 6, 7, 8, 9], "includ": [5, 7, 8, 9, 10, 11], "miplearn": [5, 6, 7, 8, 9, 11, 12, 13, 14], "addition": [5, 11], "framework": [5, 6, 7, 9, 10, 11, 12, 13, 14], "follow": [5, 7, 8, 9, 10, 11, 12, 13, 14], "convent": [5, 7, 8, 10], "store": [5, 6, 8, 11, 12, 13, 14], "file": [5, 6, 7, 8, 9, 11, 12, 13, 14], "briefli": [5, 7], "rational": 5, "choos": [5, 7, 11], "analyz": 5, "later": 5, "take": [5, 6, 7, 10, 11], "input": [5, 6, 11, 12, 13, 14], "pickl": [5, 12, 14], "end": [5, 7, 8, 12, 13, 14], "pkl": [5, 9, 11, 12, 14], "gz": [5, 9, 11, 12, 13, 14], "build_tsp_model": 5, "after": [5, 8, 11, 12, 13, 14], "process": [5, 6, 7, 8, 9, 11, 12, 13, 14], "done": [5, 6], "alongsid": 5, "veri": [5, 7, 8, 10, 11, 12, 13, 14], "time": [5, 6, 7, 8, 9, 11, 12, 13, 14], "consum": [5, 6, 8], "thei": [5, 6, 7], "potenti": [5, 7, 8, 11, 12, 13, 14], "hierarch": 5, "hdf": 5, "wa": [5, 8, 9, 11, 12, 13, 14], "develop": [5, 6, 10, 12, 13, 14], "nation": [5, 10], "center": [5, 8], "supercomput": 5, "applic": [5, 7, 8, 11], "ncsa": 5, "organ": 5, "amount": [5, 8, 12, 13, 14], "support": [5, 7, 10, 11, 14], "varieti": [5, 8], "string": [5, 13], "arrai": 5, "csv": 5, "json": [5, 11], "sqlite": 5, "sever": 5, "advantag": [5, 7, 10], "storag": 5, "scalar": 5, "vector": [5, 6, 13], "matric": [5, 6, 8], "relat": [5, 12, 13, 14], "easier": [5, 8, 12, 13, 14], "transfer": 5, "high": [5, 6, 7, 8, 9, 10, 11, 13], "o": 5, "read": [5, 7, 11], "element": [5, 8], "without": [5, 6, 7, 8, 11], "memori": [5, 12, 13, 14], "begin": [5, 7, 8, 11, 12, 13, 14], "dramat": 5, "improv": [5, 6], "reduc": [5, 6, 11, 14], "requir": [5, 6, 7, 11, 12, 13, 14], "especi": [5, 7], "import": [5, 6, 7, 8, 9, 11, 12, 14], "On": [5, 9], "fly": 5, "compress": [5, 12, 13, 14], "can": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "transpar": 5, "acceler": [5, 6, 10, 11, 12, 13, 14], "network": [5, 7, 8], "portabl": 5, "well": [5, 8, 12, 13, 14], "typic": [5, 6, 7, 8, 10], "expens": 5, "ensur": [5, 6, 8, 12, 13, 14], "usabl": 5, "futur": [5, 12, 13, 14], "even": [5, 7, 12, 13, 14], "non": [5, 8, 9, 11, 12, 14], "python": [5, 11, 12, 13, 14], "ml": [5, 7, 8, 9, 10, 11, 12, 13, 14], "current": [5, 7, 8, 9, 11, 12, 13, 14], "simpl": [5, 6, 7, 8, 11], "numer": [5, 8], "advanc": [5, 10, 11, 12, 13, 14], "librari": [5, 8], "h5py": 5, "conveni": [5, 11], "access": 5, "less": [5, 7, 8], "prone": 5, "built": 5, "dens": 5, "spars": 5, "arbitrari": 5, "binari": [5, 7, 8, 9, 11, 12, 13, 14], "correspond": 5, "get": [5, 10], "pure": [5, 10], "automat": [5, 10, 11], "check": [5, 6, 7], "show": [5, 12, 13, 14], "usag": [5, 6, 7, 9, 11, 12, 13, 14], "np": [5, 6, 8, 9, 11, 12, 14], "seed": [5, 6, 8, 9, 11, 12, 13, 14], "reproduc": [5, 6, 8, 9, 11], "42": [5, 6, 8, 9, 11, 12, 13, 14], "empti": [5, 8, 11], "test": [5, 9, 11], "x1": [5, 6], "x2": [5, 6], "hello": 5, "world": [5, 8, 12, 13, 14], "x3": [5, 6], "2": [5, 6, 7, 8, 9, 11, 12, 13, 14], "x4": 5, "rand": [5, 13], "x5": 5, "re": 5, "open": [5, 6, 8, 12, 13, 14], "print": [5, 6, 8, 11, 12, 14], "37454012": 5, "9507143": 5, "7319939": 5, "5986585": 5, "15601864": 5, "15599452": 5, "05808361": 5, "8661761": 5, "601115": 5, "6803075671195984": 5, "4504992663860321": 5, "4": [5, 6, 7, 8, 9, 11, 12, 13, 14], "013264961540699005": 5, "9422017335891724": 5, "5632882118225098": 5, "38541650772094727": 5, "015966251492500305": 5, "2308938205242157": 5, "24102546274662018": 5, "6832635402679443": 5, "6099966764450073": 5, "83319491147995": 5, "most": [5, 6, 7, 8, 11, 12, 13, 14], "fundament": 5, "right": [5, 7, 8, 12, 13, 14], "hand": [5, 6, 11, 12, 13, 14], "side": [5, 12, 13, 14], "easili": [5, 6, 7, 9], "effici": [5, 8], "rebuild": 5, "invok": 5, "sensit": 5, "among": 5, "along": 5, "statist": [5, 8, 9, 11, 12, 13, 14], "explor": [5, 8, 9, 11, 12, 13, 14], "node": [5, 6, 8, 9, 11, 12, 13, 14], "wallclock": 5, "phase": 5, "static_": 5, "lp_": 5, "mip_": 5, "shown": [5, 7, 8, 11, 12, 13, 14], "tabl": 5, "descript": [5, 6], "static_constr_lh": 5, "nconstr": 5, "nvar": 5, "left": [5, 7, 8, 12, 13, 14], "static_constr_nam": 5, "static_constr_rh": [5, 6, 12, 13, 14], "static_constr_sens": 5, "sens": [5, 6, 12, 13, 14], "static_obj_offset": 5, "static_sens": 5, "minim": [5, 8, 12, 13, 14], "max": [5, 8, 11, 12, 13, 14], "static_var_lower_bound": 5, "lower": [5, 7], "bound": [5, 6, 8, 9, 11, 12, 13, 14], "static_var_nam": 5, "static_var_obj_coeff": [5, 6, 7, 11], "static_var_typ": 5, "continu": [5, 8, 9, 11, 12, 13, 14], "static_var_upper_bound": 5, "upper": 5, "lp_constr_basis_statu": 5, "lp_constr_dual_valu": [5, 6], "dual": [5, 6, 9, 11, 12, 14], "shadow": 5, "lp_constr_sa_rhs_": 5, "rh": [5, 6, 8, 9, 11, 12, 13, 14], "lp_constr_slack": [5, 6], "slack": [5, 6], "lp_obj_valu": [5, 6], "lp_var_basis_statu": 5, "superbas": 5, "lp_var_reduced_cost": [5, 6], "lp_var_sa_": 5, "ub": 5, "lb": 5, "_": [5, 12, 13, 14], "lp_var_valu": [5, 6], "lp_wallclock_tim": 5, "taken": 5, "second": [5, 7, 8, 9, 11, 12, 13, 14], "mip_constr_slack": 5, "best": [5, 7, 8, 9, 11, 12, 13, 14], "mip_gap": 5, "rel": [5, 6, 7, 8], "gap": [5, 8, 9, 11, 12, 13, 14], "mip_node_count": 5, "mip_obj_bound": 5, "mip_obj_valu": 5, "mip_var_valu": 5, "mip_wallclock_tim": 5, "few": [5, 8, 9, 11, 12, 13, 14], "run": [5, 6, 8, 11, 12, 13, 14], "screen": 5, "uniform": [5, 6, 8, 9, 11, 12, 13, 14], "randint": [5, 6, 8, 9, 11], "glob": [5, 6], "tsp": [5, 8, 9, 11], "build_tsp_model_gurobipi": [5, 8, 9, 11], "io": [5, 6, 9, 11, 12, 14], "low": [5, 6, 8, 9, 11], "11": [5, 6, 8, 9, 11], "loc": [5, 6, 8, 9, 11, 12, 14], "1000": [5, 6, 8, 9, 11, 12, 13, 14], "90": [5, 8, 9], "20": [5, 6, 8, 9, 11, 12, 13, 14], "save": [5, 9], "00000": [5, 6, 8, 9, 11, 12, 13, 14], "00001": [5, 9, 12, 13, 14], "four": [5, 7], "parallel": [5, 11], "bc": [5, 9, 11, 12, 13, 14], "2909": 5, "2921": [5, 8], "previou": [6, 7, 8, 9, 11], "page": [6, 7, 9], "introduc": [6, 8, 9], "collector": [6, 9, 10, 11, 12, 14], "hdf5": [6, 7, 9, 10, 11, 12, 13, 14], "order": [6, 7, 8], "becaus": [6, 8, 11, 12, 13, 14], "help": 6, "complex": [6, 7, 12, 13, 14], "format": [6, 10, 12, 13, 14], "research": [6, 8, 10], "propos": 6, "absolut": [6, 8], "invari": 6, "transform": 6, "amen": 6, "treat": 6, "separ": 6, "cycl": 6, "often": [6, 7, 8, 11, 12, 13, 14], "therefor": [6, 8, 11], "focu": 6, "filter": [6, 7], "represent": [6, 11], "much": [6, 7, 11, 12, 13, 14], "faster": [6, 12, 13, 14], "experi": 6, "resolv": [6, 8], "implement": [6, 7, 9, 10, 11, 12, 13, 14], "featureextractor": 6, "produc": [6, 8, 9, 11, 12, 13, 14], "either": [6, 7, 10, 12, 13, 14], "particular": [6, 8, 10], "subset": [6, 7, 8, 9], "known": 6, "alreadi": [6, 7, 11, 12, 13, 14], "avail": [6, 7, 8, 9, 11, 12, 13, 14], "assembl": 6, "field": [6, 7, 8, 10, 11, 12, 14], "shape": 6, "demonstr": [6, 12, 13, 14], "randomli": [6, 8], "shutil": 6, "rmtree": 6, "basic": [6, 9, 10, 11, 12, 13, 14], "multiknapsack": [6, 8], "ignore_error": 6, "6": [6, 8, 9, 11, 12, 13, 14], "100": [6, 7, 8, 11, 12, 13, 14], "25": [6, 7, 8, 9, 11, 12, 13, 14], "95": [6, 8, 9], "75": [6, 7, 8, 13], "ext": 6, "": [6, 7, 8, 10, 11], "1531": 6, "24308771": 6, "350": [6, 8], "692": [6, 8], "454": [6, 8], "709": [6, 8], "605": [6, 8], "543": [6, 8], "321": [6, 8], "674": [6, 8], "571": [6, 8], "341": [6, 8], "53124309e": 6, "03": [6, 8, 9, 11, 12, 13, 14], "50000000e": 6, "02": [6, 8, 9, 11, 12, 13, 14], "00000000e": 6, "00": [6, 8, 9, 11, 12, 13, 14], "9": [6, 8, 11, 12, 13, 14], "43468018e": 6, "01": [6, 8, 9, 11, 12, 13, 14], "92000000e": 6, "51703322e": 6, "54000000e": 6, "8": [6, 8, 11, 12, 13, 14], "25504150e": 6, "7": [6, 8, 11, 12, 13, 14], "09000000e": 6, "11373022e": 6, "05000000e": 6, "26055283e": 6, "43000000e": 6, "68693771e": 6, "21000000e": 6, "07488781e": 6, "74000000e": 6, "82293701e": 6, "71000000e": 6, "41129074e": 6, "41000000e": 6, "28830120e": 6, "3100000e": 6, "5978307e": 6, "0000000e": 6, "8800000e": 6, "2881632e": 6, "0040000e": 6, "0601316e": 6, "2690000e": 6, "3659772e": 6, "0070000e": 6, "8800571e": 6, "warn": [6, 8, 11, 12, 13, 14], "illustr": [6, 7, 9, 11, 12, 13, 14], "while": [6, 7, 8, 9, 11], "would": [6, 7], "vari": 6, "unabl": 6, "concaten": 6, "tree": [6, 11], "mimick": 6, "40": [6, 8, 9, 12, 13, 14], "out": [6, 12, 13, 14], "64": [6, 8], "outsid": 6, "defint": 6, "design": [6, 8, 12, 13, 14], "irrelev": 6, "row": [6, 8, 9, 11, 12, 13, 14], "column": [6, 8, 9, 11, 12, 13, 14], "permut": 6, "paper": 6, "alvlouweh2017": [6, 7, 9], "00e": [6, 8, 9, 11, 12, 13, 14], "75e": 6, "10e": 6, "30e": 6, "40e": 6, "80e": 6, "70e": 6, "60e": 6, "alejandro": 6, "marco": 6, "theoret": [6, 7], "synergi": 6, "2016": 6, "univers": [6, 8], "li\u00e8g": 6, "quentin": 6, "loui": 6, "assign": [7, 8, 12, 13, 14], "qualiti": [7, 10], "benefici": 7, "prune": 7, "portion": 7, "search": 7, "space": [7, 8], "altern": 7, "proof": 7, "doubl": 7, "feasibl": [7, 8, 10, 11], "both": [7, 11, 12, 13, 14], "pattern": [7, 10, 12, 13, 14], "emploi": [7, 8], "highli": 7, "configur": [7, 8, 10, 11], "accept": [7, 8, 9, 12, 13, 14], "depend": 7, "whether": [7, 12, 13, 14], "befor": [7, 11, 12, 13, 14], "present": 7, "themselv": 7, "discuss": [7, 9, 12, 13, 14], "three": [7, 9, 12, 13, 14], "approach": [7, 10], "benefit": [7, 8, 10], "limit": [7, 8, 11, 12, 13, 14], "main": [7, 9, 10], "maintain": 7, "guarante": 7, "signific": [7, 10, 11, 12, 13, 14], "abl": [7, 8, 10, 11, 12, 13, 14], "possibl": 7, "case": [7, 12, 13, 14], "evalu": [7, 8], "discard": 7, "infeas": [7, 8], "ones": [7, 11, 12, 13, 14], "proce": [7, 12, 14], "disadvantag": 7, "next": [7, 8, 11, 12, 13, 14], "modest": 7, "speedup": 7, "mani": [7, 8, 11], "accur": 7, "restrict": [7, 8, 9, 11, 12, 14], "small": [7, 11, 12, 13, 14], "fraction": [7, 11, 12, 13, 14], "find": [7, 8, 10, 11, 12, 13, 14], "scratch": 7, "lose": 7, "found": [7, 8, 9, 11, 12, 13, 14], "longer": 7, "global": 7, "suffici": 7, "might": 7, "were": [7, 8, 10], "third": [7, 8], "tri": 7, "strike": 7, "balanc": 7, "enforc": [7, 8, 11], "proxim": 7, "precis": 7, "bar": 7, "_1": 7, "ldot": [7, 8, 12, 13, 14], "_n": 7, "x_1": 7, "x_n": 7, "add": [7, 11, 13, 14], "sum_": [7, 8, 12, 13, 14], "_i": [7, 12, 13, 14], "x_i": [7, 8, 12, 13, 14], "leq": [7, 8, 12, 13, 14], "defin": [7, 11, 12, 13, 14], "indic": [7, 8, 12, 13, 14], "deviat": 7, "suggest": [7, 12, 13, 14], "toler": [7, 8, 9, 11, 12, 13, 14], "Its": 7, "lead": 7, "smaller": 7, "larger": [7, 11, 12, 13, 14], "distinct": 7, "try": 7, "infer": 7, "those": 7, "like": 7, "good": [7, 8, 12, 13, 14], "promis": 7, "variat": [7, 8], "fact": 7, "let": [7, 8, 12, 13, 14], "i_1": 7, "i_n": 7, "i_": 7, "expect": 7, "through": [7, 8, 11, 12, 13, 14], "scikit": [7, 13], "score": 7, "delta_i": 7, "higher": 7, "highest": 7, "suppos": [7, 12, 13, 14], "regressor": 7, "sequenc": [7, 11], "appear": 7, "ti": 7, "being": [7, 8], "broken": 7, "arbitrarili": 7, "keep": [7, 8, 12, 13, 14], "i_k": 7, "x_l": 7, "tild": 7, "_l": 7, "frac": [7, 8], "i_j": 7, "y_j": [7, 8], "text": [7, 8, 12, 13, 14], "le": 7, "theta_0": 7, "ge": [7, 8], "theta_1": 7, "squar": [7, 8, 11], "undefin": 7, "meant": 7, "simpler": 7, "literatur": 7, "post": 7, "dummyclassifi": 7, "anoth": 7, "similar": [7, 8, 11, 12, 13, 14], "itself": [7, 11, 12, 13, 14], "kneighborsclassifi": [7, 11, 12, 13, 14], "cours": 7, "dummi": 7, "neighbor": [7, 11, 12, 13, 14], "mem": [7, 11, 12, 14], "dummyextractor": 7, "comp1": [7, 9], "1_000_000": 7, "closest": 7, "assum": [7, 8, 10, 11, 12, 13, 14], "comp2": [7, 9], "n_neighbor": [7, 11, 12, 13, 14], "comp3": [7, 9], "natur": 7, "directli": [7, 11], "novel": 7, "never": 7, "observ": [7, 10], "jointli": 7, "x_j": [7, 8], "copi": 7, "1_j": 7, "n_j": 7, "label": 7, "aris": [7, 8], "practic": [7, 8, 11, 12, 13, 14], "certain": [7, 10], "frequent": [7, 8, 12, 13, 14], "pose": 7, "standard": [7, 12, 13, 14], "sinc": 7, "do": [7, 11, 13], "wrapper": [7, 12, 13, 14], "It": [7, 8, 9, 11, 12, 13, 14], "reliabl": 7, "accuraci": 7, "situat": [7, 8, 12, 13, 14], "confid": 7, "purpos": [7, 8, 12, 13, 14], "ask": [7, 8, 11], "eras": 7, "whose": [7, 8], "suitabl": [7, 8], "handl": [7, 12, 13, 14], "overrid": 7, "linear_model": [7, 9], "logisticregress": [7, 9], "minprob": [7, 9], "singleclass": [7, 9], "indep": [7, 9], "than": [7, 8], "99": [7, 8], "comp": [7, 12, 13, 14], "subsect": 7, "straightforwad": 7, "simpli": [7, 11], "feed": 7, "forward": 7, "neural": 7, "indeped": 7, "common": 7, "multioutput": 7, "chain": [7, 8], "alon": 7, "plu": 7, "classifierchain": 7, "neural_network": 7, "mlpclassifi": 7, "feedforward": 7, "spend": [7, 12, 13, 14], "effort": [7, 11], "tweak": 7, "usual": [7, 8, 12, 13, 14], "idea": 7, "what": [7, 8, 11], "impact": 7, "simplifi": [7, 11, 12, 13, 14], "task": [7, 8], "benchmark": [7, 10], "pre": 7, "miplib": 8, "tsplib": 8, "shortcom": 8, "howev": [8, 11, 12, 13, 14], "enhanc": [8, 10], "hundr": 8, "thousand": 8, "magnitud": 8, "homogen": 8, "buch": 8, "tackl": 8, "wide": 8, "classic": 8, "techniqu": [8, 10, 11], "measur": [8, 11], "As": [8, 11], "nine": 8, "customiz": 8, "flexibl": [8, 9], "divers": 8, "characterist": 8, "belong": 8, "algorithm": 8, "subject": [8, 12, 13, 14], "your": [8, 10, 12, 13, 14], "written": 8, "exponenti": 8, "mp": [8, 12, 13, 14], "trial": 8, "primal": [8, 9, 10, 11, 12, 14], "combinatori": 8, "finit": 8, "indivis": 8, "hard": 8, "warehous": 8, "manag": 8, "determin": 8, "transport": 8, "equal": [8, 12, 13, 14], "everi": [8, 11], "pair": [8, 11], "x_": 8, "ij": 8, "align": [8, 12, 13, 14], "foral": 8, "y_i": [8, 12, 13, 14], "binpack": 8, "ten": 8, "47": 8, "26": [8, 12, 13, 14], "19": [8, 11], "52": 8, "14": [8, 9], "65": 8, "21": [8, 11, 12, 13, 14], "76": 8, "82": 8, "16": [8, 9, 11, 13], "96": 8, "102": 8, "24": 8, "69": [8, 11], "22": 8, "78": [8, 11], "17": 8, "81": [8, 11], "83": 8, "12": [8, 11, 14], "67": 8, "46": 8, "05": [8, 12], "13": [8, 12, 13, 14], "66": [8, 11, 13], "18": [8, 11], "08": [8, 12, 13, 14], "93": [8, 11], "41": [8, 9], "55": 8, "15": [8, 9, 11, 12, 13, 14], "89": 8, "59": 8, "51": [8, 11], "68": [8, 11], "62": 8, "92": 8, "94": 8, "23": 8, "85": 8, "107": [8, 11], "77": [8, 11], "79": [8, 11], "06": [8, 12, 13, 14], "44": 8, "28": [8, 9, 11, 12, 14], "98": 8, "43": 8, "104": 8, "58": [8, 11], "87": 8, "74": 8, "61": 8, "07": [8, 12, 13, 14], "37": 8, "91": 8, "57": 8, "56": [8, 9], "97": 8, "09": [8, 12, 13, 14], "licens": [8, 9, 11, 12, 13, 14], "product": [8, 9, 11, 12, 13, 14], "expir": [8, 9, 11, 12, 14], "2024": [8, 9, 11, 12, 14], "gurobi": [8, 9, 10, 11, 12, 13, 14], "v10": [8, 9, 11, 12, 13, 14], "3rc0": [8, 9, 11, 12, 14], "linux64": [8, 9, 11, 12, 13, 14], "cpu": [8, 9, 11, 12, 13, 14], "13th": [8, 9, 11, 12, 14], "gen": [8, 9, 11, 12, 14], "intel": [8, 9, 11, 12, 14], "core": [8, 9, 11, 12, 13, 14], "tm": [8, 9, 11, 12, 14], "i7": [8, 9, 11, 12, 14], "13800h": [8, 9, 11, 12, 14], "instruct": [8, 9, 11, 12, 13, 14], "sse2": [8, 9, 11, 12, 13, 14], "avx": [8, 9, 11, 12, 13, 14], "avx2": [8, 9, 11, 12, 13, 14], "thread": [8, 9, 11, 12, 13, 14], "count": [8, 9, 11, 12, 13, 14], "physic": [8, 9, 11, 12, 13, 14], "logic": [8, 9, 11, 12, 13, 14], "processor": [8, 9, 11, 12, 13, 14], "110": [8, 9], "210": 8, "nonzero": [8, 9, 11, 12, 13, 14], "fingerprint": [8, 9, 11, 12, 13, 14], "0x1ff9913f": 8, "1e": [8, 9, 11, 12, 13, 14], "heurist": [8, 11, 12, 13, 14], "0000000": [8, 11, 12, 13, 14], "presolv": [8, 9, 11, 12, 13, 14], "root": [8, 9, 11, 12, 13, 14], "274844e": 8, "38": 8, "iter": [8, 9, 11, 12, 13, 14], "expl": [8, 9, 11, 12, 13, 14], "unexpl": [8, 9, 11, 12, 13, 14], "depth": [8, 9, 11, 12, 13, 14], "intinf": [8, 9, 11, 12, 13, 14], "incumb": [8, 9, 11, 12, 13, 14], "bestbd": [8, 9, 11, 12, 13, 14], "27484": 8, "h": [8, 11, 12, 13, 14], "simplex": [8, 9, 11, 12, 13, 14], "04": [8, 9, 11, 12, 13, 14], "000000000000e": 8, "0000": [8, 9, 11, 12, 13, 14], "involv": [8, 9], "place": 8, "total": [8, 12, 13, 14], "maxim": 8, "exceed": 8, "repres": [8, 11], "resourc": 8, "must": [8, 11], "satisfi": 8, "p_j": 8, "w_": 8, "b_i": 8, "alpha_i": 8, "u_j": 8, "gamma_": 8, "frevil": 8, "arnaud": 8, "g\u00e9rard": 8, "plateau": 8, "preprocess": 8, "procedur": [8, 11, 12, 13, 14], "multidimension": 8, "discret": [8, 10], "mathemat": [8, 12, 13, 14], "49": 8, "1994": 8, "189": 8, "212": 8, "fr\u00e9vill": 8, "european": 8, "155": 8, "2004": 8, "five": 8, "around": [8, 12, 13, 14], "392": 8, "977": 8, "764": [8, 13], "622": 8, "158": 8, "163": 8, "840": 8, "574": 8, "696": 8, "948": 8, "860": 8, "209": 8, "178": 8, "184": 8, "293": 8, "541": 8, "414": 8, "305": 8, "629": 8, "135": 8, "278": 8, "378": 8, "466": 8, "803": 8, "205": 8, "492": 8, "584": 8, "45": [8, 9], "630": 8, "173": 8, "907": 8, "947": 8, "794": 8, "312": 8, "711": 8, "439": 8, "117": 8, "506": 8, "35": [8, 11, 13], "915": 8, "266": 8, "662": 8, "516": 8, "521": 8, "1310": 8, "988": 8, "1004": 8, "1269": 8, "50": [8, 9, 11, 12, 13, 14], "0xaf3ac15": 8, "2e": [8, 9, 11, 12, 13, 14], "3e": [8, 12, 14], "7e": [8, 12, 13, 14], "804": 8, "remov": [8, 11, 12, 13, 14], "34": 8, "428726e": 8, "1428": 8, "7265": 8, "1279": 8, "000000": [8, 11], "plane": [8, 9, 11, 12, 14], "No": 8, "better": 8, "279000000000e": 8, "serv": 8, "goal": 8, "suppli": 8, "d_i": 8, "furthermor": 8, "c_j": 8, "pmedian": 8, "100x100": 8, "250": 8, "32": [8, 13], "33": 8, "86": 8, "88": 8, "72": 8, "80": 8, "39": [8, 9, 12], "71": 8, "70": [8, 12, 13, 14], "30": [8, 12, 13, 14], "73": 8, "101": 8, "63": 8, "54": 8, "111": 8, "27": 8, "151": 8, "237": 8, "241": 8, "202": 8, "171": 8, "220": 8, "0x8d8d9346": 8, "5e": 8, "4e": [8, 9], "368": 8, "7900000": 8, "245": 8, "6400000": 8, "000000e": [8, 9, 11, 12, 14], "64000": 8, "1900000": 8, "148": 8, "6300000": 8, "14595": 8, "113": 8, "1800000": 8, "84": 8, "18000": 8, "5000000": 8, "3900000": 8, "9800000": 8, "28872": 8, "31": 8, "98000": 8, "9200000": 8, "06884": 8, "92000": 8, "2300000": 8, "23000": 8, "123000000000e": 8, "aim": 8, "overlap": 8, "real": [8, 12, 13, 14], "scenario": 8, "schedul": [8, 11], "alloc": 8, "s_1": 8, "s_m": 8, "union": [8, 12, 14], "w_j": 8, "s_j": 8, "geq": [8, 12, 13, 14], "setcovergener": 8, "n_element": 8, "n_set": 8, "incid": 8, "densiti": 8, "d": [8, 12, 13, 14], "entri": 8, "bernoulli": 8, "least": 8, "identifi": [8, 10, 11], "uniformli": 8, "modifi": 8, "denot": [8, 12, 13, 14], "fix_set": 8, "costs_jitt": 8, "setcov": 8, "build_setcover_model_gurobipi": 8, "interv": 8, "1044": 8, "850": 8, "1014": 8, "944": 8, "697": 8, "971": 8, "213": 8, "425": 8, "0xe5c2d4fa": 8, "4900000": 8, "134900000000e": 8, "disjoint": 8, "within": 8, "airlin": [8, 11], "flight": 8, "crew": 8, "setpackgener": 8, "detail": [8, 11], "document": 8, "setpack": 8, "build_setpack_model": [], "0x4ee91388": 8, "1265": 8, "560000": 8, "1986": 8, "986370000000e": 8, "theori": 8, "vertic": 8, "adjac": [8, 11], "simultan": [8, 12, 13, 14], "conflict": 8, "v": 8, "undirect": 8, "x_v": 8, "x_u": 8, "recal": [8, 11], "edg": [8, 11], "probabilti": 8, "stab": 8, "build_stab_model_gurobipi": [8, 11], "60": [8, 11, 12, 13, 14], "48": 8, "precrush": [8, 9, 11], "0x3240ea4a": 8, "6e": [8, 12, 13, 14], "219": 8, "1400000": 8, "205650e": 8, "14000": 8, "191400000000e": 8, "callback": [8, 9, 11, 13], "299": 8, "sec": [8, 9, 11, 13], "citi": [8, 11], "shortest": [8, 11], "visit": [8, 11], "hamiltonian": [8, 11], "path": [8, 11, 12, 13, 14], "karp": [8, 11], "deliveri": [8, 11], "truck": [8, 11], "d_e": 8, "x_e": 8, "delta": 8, "subsetneq": 8, "neq": 8, "emptyset": 8, "extrem": 8, "setminu": 8, "inequ": 8, "initi": [8, 11, 12, 13, 14], "d_": 8, "sqrt": 8, "1000x1000": 8, "box": 8, "513": 8, "762": 8, "358": 8, "325": 8, "374": 8, "932": 8, "731": 8, "391": 8, "634": 8, "726": 8, "765": 8, "754": 8, "409": 8, "719": 8, "446": 8, "400": 8, "780": 8, "756": 8, "744": 8, "656": 8, "383": 8, "334": 8, "549": 8, "925": 8, "702": 8, "422": 8, "728": 8, "663": 8, "526": 8, "708": 8, "377": 8, "462": 8, "1072": 8, "802": 8, "501": 8, "853": 8, "654": 8, "603": 8, "433": 8, "381": 8, "255": 8, "287": 8, "493": 8, "900": 8, "354": 8, "323": 8, "367": 8, "841": 8, "727": 8, "444": 8, "668": 8, "690": 8, "687": 8, "175": 8, "725": 8, "398": 8, "666": 8, "827": 8, "736": 8, "371": [8, 13], "317": 8, "570": 8, "1090": 8, "712": 8, "648": 8, "655": 8, "650": 8, "356": 8, "469": 8, "1146": 8, "779": 8, "476": 8, "752": 8, "681": 8, "565": [8, 12, 14], "394": 8, "286": 8, "274": 8, "lazyconstraint": [8, 9, 11], "0x719675e5": 8, "921000e": 8, "921000000000e": 8, "106": 8, "power": [8, 10, 12, 13, 14], "turn": [8, 12, 13, 14], "off": 8, "meet": 8, "electr": [8, 12, 13, 14], "lowest": 8, "ramp": 8, "prevent": 8, "output": [8, 13], "level": [8, 11], "too": 8, "quickli": 8, "minimum": 8, "switch": 8, "system": [8, 10, 12, 13, 14], "plan": 8, "doe": [8, 11, 12, 13, 14], "trajectori": 8, "piecewis": 8, "curv": 8, "transmiss": 8, "secur": 8, "realist": [8, 12, 13, 14], "unitcommit": 8, "jl": 8, "t": 8, "d_t": 8, "mw": [8, 12, 13, 14], "max_g": 8, "min_g": 8, "l_g": 8, "regardless": 8, "var": [8, 10, 12, 13, 14], "gt": 8, "p_": 8, "_g": 8, "gk": 8, "cannot": [8, 14], "symmetr": 8, "fourth": 8, "period": 8, "fifth": 8, "sixth": 8, "quantiti": 8, "unitcommitmentgener": 8, "n_unit": 8, "n_period": 8, "valid": 8, "rather": 8, "startup": 8, "peak": 8, "4c": 8, "8c": 8, "cost_jitt": 8, "demand_jitt": 8, "fix_unit": 8, "uc": [8, 12, 13, 14], "450": [8, 11, 12, 13, 14], "10_000": 8, "1_000": 8, "f": [8, 11], "271": 8, "207": 8, "218": 8, "477": 8, "379": 8, "319": 8, "120": 8, "3042": 8, "5247": 8, "4319": 8, "2912": 8, "6118": 8, "53": 8, "199": 8, "514": 8, "592": 8, "607": 8, "905": 8, "1166": 8, "1212": 8, "1127": 8, "953": 8, "796": 8, "783": 8, "866": 8, "768": 8, "899": 8, "946": 8, "1087": 8, "1048": 8, "992": 8, "750": 8, "691": 8, "606": 8, "658": 8, "809": 8, "2458": 8, "6200": 8, "4585": 8, "2666": 8, "4783": 8, "196": 8, "416": 8, "626": 8, "981": 8, "1095": 8, "1102": 8, "1088": 8, "863": 8, "848": 8, "761": 8, "828": 8, "775": 8, "834": 8, "959": 8, "865": 8, "1193": 8, "985": 8, "893": 8, "962": 8, "781": 8, "723": 8, "639": 8, "602": 8, "787": 8, "578": 8, "360": 8, "2128": 8, "0x4dc1c661": 8, "240": 8, "244": 8, "131": 8, "229": 8, "842": 8, "116": 8, "440662": 8, "46430": 8, "429461": 8, "97680": 8, "374043": 8, "64040": 8, "361348e": 8, "142": 8, "336134": 8, "820": 8, "640": 8, "368600": 8, "14450": 8, "364721": 8, "76610": 8, "cutoff": [8, 9], "766": 8, "gomori": [8, 11, 12, 14], "cliqu": 8, "222": [8, 11], "mir": [8, 11, 12, 14], "flow": [8, 12, 14], "rlt": 8, "lift": 8, "234": 8, "364722": 8, "374044": 8, "647217661000e": 8, "connect": 8, "bioinformat": 8, "w_g": 8, "minweightvertexcovergener": 8, "behav": 8, "vertexcov": 8, "build_vertexcover_model": [], "0x2d2d1390": 8, "301": 8, "995750e": 8, "010000000000e": 8, "individu": [9, 12, 13, 14], "integr": 9, "aforement": 9, "cohes": 9, "whole": 9, "conclud": 9, "runnabl": 9, "compos": 9, "target": 9, "part": [9, 11], "architectur": 9, "enabl": 9, "equival": 9, "tradit": 9, "sequenti": 9, "could": [9, 12, 13, 14], "achiev": [9, 12, 13, 14], "dictionari": [9, 11], "train_data": [9, 11, 12, 13, 14], "test_data": [9, 11, 12, 13, 14], "action": [9, 10, 12, 13, 14], "all_data": 9, "split": 9, "0x6ddcd141": 9, "inf": [9, 11, 12, 14], "3600000e": 9, "700000e": [9, 11], "7610000e": 9, "761000000e": 9, "0x74ca3d0a": 9, "2796": 9, "761000e": 9, "2761": 9, "796000000000e": 9, "extens": 10, "state": [10, 12, 13, 14], "art": [10, 12, 13, 14], "cplex": [10, 11, 12, 13, 14], "xpress": [10, 11, 12, 13, 14], "unlik": [10, 11], "prove": 10, "full": 10, "happen": 10, "famili": 10, "redund": 10, "pyomo": [10, 11, 12, 13], "jump": [10, 11, 12, 14], "overview": 10, "cover": [10, 12, 14], "vertex": 10, "joint": 10, "expert": 10, "exampl": [10, 11, 12, 13, 14], "helper": 10, "alinson": 10, "xavier": 10, "argonn": 10, "laboratori": 10, "feng": 10, "qiu": 10, "xiaoyi": 10, "gu": 10, "georgia": 10, "institut": 10, "technologi": 10, "berkai": 10, "becu": 10, "santanu": 10, "dei": 10, "upon": 10, "direct": 10, "ldrd": 10, "fund": 10, "director": 10, "offic": 10, "scienc": 10, "depart": 10, "energi": 10, "grid": [10, 12, 13, 14], "kindli": 10, "packag": [10, 11, 12, 13, 14], "zenodo": 10, "2023": 10, "5281": 10, "4287567": 10, "shabbir": 10, "ahm": 10, "2020": 10, "1287": 10, "ijoc": 10, "0976": 10, "tighten": 11, "region": 11, "elimin": 11, "thu": 11, "formul": [11, 12, 13, 14], "omit": 11, "success": 11, "tutori": [11, 12, 13, 14], "subtour": 11, "correctli": 11, "instal": 11, "compat": [11, 12, 13, 14], "julia": [11, 12, 13, 14], "sourc": [11, 12, 13, 14], "code": [11, 12, 13, 14], "build_tsp_model_pyomo": 11, "build_tsp_model_jump": 11, "gurobi_persist": [11, 14], "pr": 11, "persist": [11, 14], "welcom": [11, 12, 13, 14], "scip": 11, "glpk": 11, "cbc": 11, "newer": [11, 12, 13, 14], "further": 11, "becom": 11, "log": [11, 12, 13, 14], "basicconfig": 11, "getlogg": 11, "setlevel": 11, "critic": [], "memorizingcutscompon": [], "expertcutscompon": [], "expertlazycompon": [], "memorizinglazycompon": 11, "500": [11, 12, 13, 14], "info": 11, "225": [], "1225": 11, "2450": 11, "0x04d7bec1": 11, "0600000e": 11, "5880000e": 11, "588000000e": 11, "ahead": 11, "6091": 11, "0x09bd34d6": 11, "29853": 11, "139000e": 11, "6139": 11, "6390": 11, "6165": 11, "50000": 11, "6198": 11, "6219": 11, "half": 11, "219000000000e": 11, "143": 8, "aot": [], "0x77a94572": 11, "29695": 11, "588000e": 11, "5588": 11, "27241": 11, "5898": 11, "6066": 11, "6128": 11, "6368": 11, "6154": 11, "75000": 11, "6204": 11, "224": 11, "togeth": [12, 13, 14], "broader": [12, 14], "earli": [12, 13, 14], "stage": [12, 13, 14], "bug": [12, 13, 14], "submit": [12, 13, 14], "report": [12, 13, 14], "our": [11, 12, 13, 14], "github": [12, 13, 14], "repositori": [12, 13, 14], "comment": [12, 13, 14], "pull": [12, 13, 14], "languag": [12, 13, 14], "offici": [12, 13, 14], "websit": [12, 13, 14], "pip": [12, 14], "commerci": [12, 13, 14], "milp": [12, 13, 14], "demo": [12, 14], "possibli": [12, 13, 14], "incompat": [12, 13, 14], "releas": [12, 13, 14], "recommend": [11, 12, 13, 14], "project": [12, 13, 14], "simplif": [12, 13, 14], "daili": [12, 13, 14], "compani": [12, 13, 14], "onlin": [12, 13, 14], "hour": [12, 13, 14], "dai": [12, 13, 14], "own": [12, 13, 14], "g_1": [12, 13, 14], "g_n": [12, 13, 14], "offlin": [12, 13, 14], "g_i": [12, 13, 14], "megawatt": [12, 13, 14], "noth": [12, 13, 14], "quad": [12, 13, 14], "hold": [11, 12, 13, 14], "dataclass": [11, 12, 14], "pmin": [12, 13, 14], "pmax": [12, 13, 14], "cfix": [12, 13, 14], "cvar": [12, 13, 14], "structur": [12, 13, 14], "gp": [11, 12], "grb": [11, 12], "quicksum": [11, 12], "def": [11, 12, 14], "isinst": [11, 12, 14], "len": [11, 12, 14], "_x": 12, "addvar": [11, 12], "vtype": [11, 12], "_y": 12, "setobject": [11, 12], "addconstr": [11, 12], "At": [12, 13, 14], "700": [12, 13, 14], "600": [12, 13, 14], "objval": 12, "0x58dfdd53": 12, "1400": [12, 13, 14], "035000e": [12, 13, 14], "1035": [12, 13, 14], "1105": [12, 13, 14], "71429": [12, 13, 14], "1320": [12, 13, 14], "320000000000e": [12, 13, 14], "thin": [12, 13, 14], "agnost": [11, 12, 13, 14], "control": [12, 13, 14], "queri": [11, 12, 13, 14], "consist": 12, "slower": [12, 13, 14], "upfront": [12, 13, 14], "histor": [12, 13, 14], "random_uc_data": [12, 13, 14], "100_000": [12, 13, 14], "400_000": [12, 14], "rv": [12, 14], "simplic": [12, 13, 14], "now": [12, 13, 14], "impract": [12, 13, 14], "export": [12, 13, 14], "With": [12, 13, 14], "agre": [12, 13, 14], "unanim": [12, 13, 14], "solver_ml": [12, 13, 14], "1001": [12, 13, 14], "2500": [12, 13, 14], "0xa8b70287": 12, "6166537e": [12, 14], "648803e": [12, 14], "2906219e": [12, 14], "290621916e": [12, 14], "0xcf27855a": 12, "29153e": [12, 14], "290622e": [12, 14], "512": [12, 14], "2906e": [12, 14], "2915e": [12, 14], "2907e": [12, 14], "291528276179e": [12, 14], "290733258025e": [12, 14], "0096": [12, 14], "482": 12, "examin": [12, 13, 14], "line": [12, 13, 14], "repeat": [12, 13, 14], "solver_baselin": [12, 13, 14], "0x4cbbf7c7": 12, "757128e": [12, 14], "7571e": [12, 14], "298273e": [12, 14], "2983e": [12, 14], "293980e": [12, 14], "2940e": [12, 14], "2908e": [12, 14], "291465e": [12, 14], "1031": 12, "29147e": [12, 14], "29398e": [12, 14], "29827e": [12, 14], "75713e": [12, 14], "291465302389e": [12, 14], "290781665333e": [12, 14], "0082": [12, 14], "miss": [12, 13, 14], "had": [12, 13, 14], "significantli": [12, 13, 14], "inferior": [12, 13, 14], "intern": [12, 13, 14], "almost": [12, 13, 14], "term": [12, 13, 14], "0x19042f12": 12, "5917580e": [12, 14], "627453e": [12, 14], "2535968e": [12, 14], "253596777e": [12, 14], "0xf97cde91": 12, "25814e": [12, 14], "25512e": [12, 14], "25483e": [12, 14], "25459e": [12, 14], "253597e": [12, 14], "2536e": [12, 14], "2546e": [12, 14], "2537e": [12, 14], "2538e": [12, 14], "strongcg": [12, 14], "575": [12, 14], "254590409970e": [12, 14], "253768093811e": [12, 14], "0100": [12, 14], "8254590409": [12, 14], "969726": 12, "935662": [12, 14], "0949262811": [12, 14], "1604270": [12, 14], "0218116897": [12, 14], "launch": 13, "repl": 13, "enter": 13, "pkg": 13, "pycal": 13, "suppressor": 13, "cleaner": 13, "replac": [13, 14], "struct": 13, "float64": 13, "jld2": 13, "isa": 13, "read_jld2": 13, "length": 13, "eq_max_pow": [13, 14], "eq_min_pow": [13, 14], "eq_demand": [13, 14], "jumpmodel": 13, "objective_valu": 13, "1rc0": 13, "amd": 13, "ryzen": 13, "7950x": 13, "avx512": 13, "0x55e33a07": 13, "0e": 13, "500_000": 13, "125": 13, "00002": 13, "write_jld2": 13, "451": 13, "suppress_out": 13, "knn": 13, "pyimport": 13, "0xd2378195": 13, "02165e": 13, "021568e": 13, "510": 13, "0216e": 13, "0217e": 13, "021651058978e": 13, "021567971257e": 13, "0081": 13, "169": 13, "0xb45c0594": 13, "071463e": 13, "0715e": 13, "025162e": 13, "0252e": 13, "023090e": 13, "022335e": 13, "022281e": 13, "021753e": 13, "021752e": 13, "0218e": 13, "021651e": 13, "02175e": 13, "02228e": 13, "07146e": 13, "021573363741e": 13, "0076": 13, "204": 13, "0x974a7fba": 13, "86729e": 13, "86675e": 13, "86654e": 13, "8661e": 13, "865344e": 13, "8653e": 13, "866096485614e": 13, "865343669936e": 13, "182": 13, "866096485613789e9": 13, "environ": 14, "pe": 14, "pyomomodel": 14, "concretemodel": 14, "domain": 14, "nonnegativer": 14, "expr": 14, "constraintlist": 14, "qcpdual": 14, "0x15c7a953": 14, "cplex_persist": 14, "xpress_persist": 14, "0x5e67c6e": 14, "0x4a7cfe2b": 14, "0x8a0f9587": 14, "1025": 14, "0x2dfe4e1c": 14, "0x0f0924a1": 14, "96973": 14, "1369560": 14, "835229226": 14, "602828": 14, "5321028307": 14, "ndar": [], "rai": [], "travelingsalesmandgener": 11, "actual": 11, "annot": 11, "tuplelist": 11, "nx": 11, "build_tsp_model_gurobipy_simplifi": 11, "eq_degre": 11, "x_val": 11, "cbgetsolut": 11, "selected_edg": 11, "add_edges_from": 11, "connected_compon": 11, "cut_edg": 11, "append": 11, "regular": 11, "uniqu": 11, "responsbl": 11, "serial": 11, "tupl": 11, "0x9904dd15": [], "8600000e": [], "6185000e": [], "618500000e": [], "112": [], "12482": [], "0xd7027ec2": [], "237500e": [], "6237": [], "6577": [], "6314": [], "6250": [], "6287": [], "6311": [], "232": [], "314000000000e": [], "145": [], "wherea": 11, "necessari": 11, "verifi": 11, "major": 11, "increas": 11, "so": 11, "0xdcf5ae58": [], "29294": [], "618500e": [], "5618": [], "26112": [], "6110": [], "6583": [], "6243": [], "6278": [], "6581": [], "6519": [], "6283": [], "6313": [], "306": [], "223": [], "There": 11, "12411": [], "0x48832a9e": [], "6939": [], "6147": [], "6194": [], "147": [], "172": [], "focus": 11, "cbgetnoderel": 11, "build_stab_model_pyomo": 11, "build_stab_model_jump": 11, "144": [], "141": 11, "170": 11, "build_binpack_model_gurobipi": [3, 8], "build_multiknapsack_model_gurobipi": [3, 6, 8], "build_pmedian_model_gurobipi": [3, 8], "build_uc_model_gurobipi": [3, 8], "490": 8, "190": 8, "build_setpack_model_gurobipi": 8, "238": 8, "677": 8, "build_vertexcover_model_gurobipi": 8, "326": 8}, "objects": {"miplearn.classifiers": [[0, 0, 0, "-", "minprob"], [0, 0, 0, "-", "singleclass"]], "miplearn.classifiers.minprob": [[0, 1, 1, "", "MinProbabilityClassifier"]], "miplearn.classifiers.minprob.MinProbabilityClassifier": [[0, 2, 1, "", "fit"], [0, 2, 1, "", "predict"], [0, 2, 1, "", "set_fit_request"], [0, 2, 1, "", "set_predict_request"]], "miplearn.classifiers.singleclass": [[0, 1, 1, "", "SingleClassFix"]], "miplearn.classifiers.singleclass.SingleClassFix": [[0, 2, 1, "", "fit"], [0, 2, 1, "", "predict"], [0, 2, 1, "", "set_fit_request"], [0, 2, 1, "", "set_predict_request"]], "miplearn.collectors": [[0, 0, 0, "-", "basic"]], "miplearn.collectors.basic": [[0, 1, 1, "", "BasicCollector"]], "miplearn.collectors.basic.BasicCollector": [[0, 2, 1, "", "collect"]], "miplearn.components.primal": [[1, 0, 0, "-", "actions"], [1, 0, 0, "-", "expert"], [1, 0, 0, "-", "indep"], [1, 0, 0, "-", "joint"], [1, 0, 0, "-", "mem"]], "miplearn.components.primal.actions": [[1, 1, 1, "", "EnforceProximity"], [1, 1, 1, "", "FixVariables"], [1, 1, 1, "", "PrimalComponentAction"], [1, 1, 1, "", "SetWarmStart"]], "miplearn.components.primal.actions.EnforceProximity": [[1, 2, 1, "", "perform"]], "miplearn.components.primal.actions.FixVariables": [[1, 2, 1, "", "perform"]], "miplearn.components.primal.actions.PrimalComponentAction": [[1, 2, 1, "", "perform"]], "miplearn.components.primal.actions.SetWarmStart": [[1, 2, 1, "", "perform"]], "miplearn.components.primal.expert": [[1, 1, 1, "", "ExpertPrimalComponent"]], "miplearn.components.primal.expert.ExpertPrimalComponent": [[1, 2, 1, "", "before_mip"], [1, 2, 1, "", "fit"]], "miplearn.components.primal.indep": [[1, 1, 1, "", "IndependentVarsPrimalComponent"]], "miplearn.components.primal.indep.IndependentVarsPrimalComponent": [[1, 2, 1, "", "before_mip"], [1, 2, 1, "", "fit"]], "miplearn.components.primal.joint": [[1, 1, 1, "", "JointVarsPrimalComponent"]], "miplearn.components.primal.joint.JointVarsPrimalComponent": [[1, 2, 1, "", "before_mip"], [1, 2, 1, "", "fit"]], "miplearn.components.primal.mem": [[1, 1, 1, "", "MemorizingPrimalComponent"], [1, 1, 1, "", "MergeTopSolutions"], [1, 1, 1, "", "SelectTopSolutions"], [1, 1, 1, "", "SolutionConstructor"]], "miplearn.components.primal.mem.MemorizingPrimalComponent": [[1, 2, 1, "", "before_mip"], [1, 2, 1, "", "fit"]], "miplearn.components.primal.mem.MergeTopSolutions": [[1, 2, 1, "", "construct"]], "miplearn.components.primal.mem.SelectTopSolutions": [[1, 2, 1, "", "construct"]], "miplearn.components.primal.mem.SolutionConstructor": [[1, 2, 1, "", "construct"]], "miplearn.extractors": [[0, 0, 0, "-", "AlvLouWeh2017"], [0, 0, 0, "-", "fields"]], "miplearn.extractors.AlvLouWeh2017": [[0, 1, 1, "", "AlvLouWeh2017Extractor"]], "miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor": [[0, 2, 1, "", "get_constr_features"], [0, 2, 1, "", "get_instance_features"], [0, 2, 1, "", "get_var_features"]], "miplearn.extractors.fields": [[0, 1, 1, "", "H5FieldsExtractor"]], "miplearn.extractors.fields.H5FieldsExtractor": [[0, 2, 1, "", "get_constr_features"], [0, 2, 1, "", "get_instance_features"], [0, 2, 1, "", "get_var_features"]], "miplearn": [[2, 0, 0, "-", "h5"], [2, 0, 0, "-", "io"]], "miplearn.h5": [[2, 1, 1, "", "H5File"]], "miplearn.h5.H5File": [[2, 2, 1, "", "close"], [2, 2, 1, "", "get_array"], [2, 2, 1, "", "get_bytes"], [2, 2, 1, "", "get_scalar"], [2, 2, 1, "", "get_sparse"], [2, 2, 1, "", "put_array"], [2, 2, 1, "", "put_bytes"], [2, 2, 1, "", "put_scalar"], [2, 2, 1, "", "put_sparse"]], "miplearn.io": [[2, 3, 1, "", "gzip"], [2, 3, 1, "", "read_pkl_gz"], [2, 3, 1, "", "write_pkl_gz"]], "miplearn.problems": [[3, 0, 0, "-", "binpack"], [3, 0, 0, "-", "multiknapsack"], [3, 0, 0, "-", "pmedian"], [3, 0, 0, "-", "setcover"], [3, 0, 0, "-", "setpack"], [3, 0, 0, "-", "stab"], [3, 0, 0, "-", "tsp"], [3, 0, 0, "-", "uc"], [3, 0, 0, "-", "vertexcover"]], "miplearn.problems.binpack": [[3, 1, 1, "", "BinPackData"], [3, 1, 1, "", "BinPackGenerator"], [3, 3, 1, "", "build_binpack_model_gurobipy"]], "miplearn.problems.binpack.BinPackGenerator": [[3, 2, 1, "", "generate"]], "miplearn.problems.multiknapsack": [[3, 1, 1, "", "MultiKnapsackData"], [3, 1, 1, "", "MultiKnapsackGenerator"], [3, 3, 1, "", "build_multiknapsack_model_gurobipy"]], "miplearn.problems.pmedian": [[3, 1, 1, "", "PMedianData"], [3, 1, 1, "", "PMedianGenerator"], [3, 3, 1, "", "build_pmedian_model_gurobipy"]], "miplearn.problems.setcover": [[3, 1, 1, "", "SetCoverData"]], "miplearn.problems.setpack": [[3, 1, 1, "", "SetPackData"]], "miplearn.problems.stab": [[3, 1, 1, "", "MaxWeightStableSetData"], [3, 1, 1, "", "MaxWeightStableSetGenerator"]], "miplearn.problems.tsp": [[3, 1, 1, "", "TravelingSalesmanData"], [3, 1, 1, "", "TravelingSalesmanGenerator"]], "miplearn.problems.uc": [[3, 1, 1, "", "UnitCommitmentData"], [3, 3, 1, "", "build_uc_model_gurobipy"]], "miplearn.problems.vertexcover": [[3, 1, 1, "", "MinWeightVertexCoverData"]], "miplearn.solvers": [[4, 0, 0, "-", "abstract"], [4, 0, 0, "-", "gurobi"], [4, 0, 0, "-", "learning"]], "miplearn.solvers.abstract": [[4, 1, 1, "", "AbstractModel"]], "miplearn.solvers.abstract.AbstractModel": [[4, 4, 1, "", "WHERE_CUTS"], [4, 4, 1, "", "WHERE_DEFAULT"], [4, 4, 1, "", "WHERE_LAZY"], [4, 2, 1, "", "add_constrs"], [4, 2, 1, "", "extract_after_load"], [4, 2, 1, "", "extract_after_lp"], [4, 2, 1, "", "extract_after_mip"], [4, 2, 1, "", "fix_variables"], [4, 2, 1, "", "lazy_enforce"], [4, 2, 1, "", "optimize"], [4, 2, 1, "", "relax"], [4, 2, 1, "", "set_cuts"], [4, 2, 1, "", "set_warm_starts"], [4, 2, 1, "", "write"]], "miplearn.solvers.gurobi": [[4, 1, 1, "", "GurobiModel"]], "miplearn.solvers.gurobi.GurobiModel": [[4, 2, 1, "", "add_constr"], [4, 2, 1, "", "add_constrs"], [4, 2, 1, "", "extract_after_load"], [4, 2, 1, "", "extract_after_lp"], [4, 2, 1, "", "extract_after_mip"], [4, 2, 1, "", "fix_variables"], [4, 2, 1, "", "optimize"], [4, 2, 1, "", "relax"], [4, 2, 1, "", "set_time_limit"], [4, 2, 1, "", "set_warm_starts"], [4, 2, 1, "", "write"]], "miplearn.solvers.learning": [[4, 1, 1, "", "LearningSolver"]], "miplearn.solvers.learning.LearningSolver": [[4, 2, 1, "", "fit"], [4, 2, 1, "", "optimize"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:attribute"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "attribute", "Python attribute"]}, "titleterms": {"collector": [0, 5], "extractor": [0, 6], "miplearn": [0, 1, 2, 3, 4, 10], "classifi": 0, "minprob": 0, "singleclass": 0, "basic": [0, 5], "field": [0, 5], "alvlouweh2017": 0, "compon": [1, 7], "primal": [1, 7], "action": [1, 7], "expert": [1, 7], "indep": 1, "joint": [1, 7], "mem": 1, "helper": 2, "io": 2, "h5": 2, "benchmark": [3, 8], "problem": [3, 8, 11, 12, 13, 14], "binpack": 3, "multiknapsack": 3, "pmedian": 3, "setcov": 3, "setpack": 3, "stab": 3, "tsp": 3, "uc": 3, "vertexcov": 3, "solver": [4, 9], "abstract": 4, "gurobi": 4, "learn": [4, 9, 11], "train": [5, 9, 11, 12, 13, 14], "data": [5, 11, 12, 13, 14], "overview": [5, 6, 8], "hdf5": 5, "format": 5, "exampl": [5, 6, 7, 8, 9], "featur": 6, "h5fieldsextractor": 6, "alvlouweh2017extractor": 6, "memor": 7, "independ": 7, "var": 7, "bin": 8, "pack": 8, "formul": 8, "random": 8, "instanc": [8, 9, 11, 12, 13, 14], "gener": [8, 11, 12, 13, 14], "multi": 8, "dimension": 8, "knapsack": 8, "capacit": 8, "p": 8, "median": 8, "set": 8, "cover": 8, "stabl": 8, "travel": [8, 11], "salesman": [8, 11], "unit": 8, "commit": 8, "vertex": 8, "configur": 9, "solv": [9, 11, 12, 13, 14], "new": [9, 11], "complet": 9, "content": 10, "tutori": 10, "user": [10, 11], "guid": 10, "python": 10, "api": 10, "refer": 10, "author": 10, "acknowledg": 10, "cite": 10, "cut": 11, "lazi": 11, "constraint": 11, "The": [], "get": [12, 13, 14], "start": [12, 13, 14], "gurobipi": 12, "introduct": [12, 13, 14], "instal": [12, 13, 14], "model": [11, 12, 13, 14], "simpl": [12, 13, 14], "optim": [12, 13, 14], "test": [12, 13, 14], "access": [12, 13, 14], "solut": [12, 13, 14], "jump": 13, "pyomo": 14}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "nbsphinx": 4, "sphinx": 60}, "alltitles": {"Helpers": [[2, "helpers"]], "miplearn.io": [[2, "module-miplearn.io"]], "miplearn.h5": [[2, "module-miplearn.h5"]], "Example": [[7, "Example"], [5, "Example"], [5, "id1"], [6, "Example"], [6, "id1"], [8, "Example"], [8, "id3"], [8, "id6"], [8, "id9"], [8, "id12"], [8, "id15"], [8, "id18"], [8, "id21"], [8, "id24"]], "Primal Components": [[7, "Primal-Components"]], "Primal component actions": [[7, "Primal-component-actions"]], "Memorizing primal component": [[7, "Memorizing-primal-component"]], "Examples": [[7, "Examples"], [7, "id1"], [7, "id2"]], "Independent vars primal component": [[7, "Independent-vars-primal-component"]], "Joint vars primal component": [[7, "Joint-vars-primal-component"]], "Expert primal component": [[7, "Expert-primal-component"]], "MIPLearn": [[10, "miplearn"]], "Contents": [[10, "contents"]], "Tutorials": [[10, null]], "User Guide": [[10, null]], "Python API Reference": [[10, null]], "Authors": [[10, "authors"]], "Acknowledgments": [[10, "acknowledgments"]], "Citing MIPLearn": [[10, "citing-miplearn"]], "Generating training data": [[12, "Generating-training-data"], [13, "Generating-training-data"], [14, "Generating-training-data"], [11, "Generating-training-data"]], "Getting started (Gurobipy)": [[12, "Getting-started-(Gurobipy)"]], "Introduction": [[12, "Introduction"], [13, "Introduction"], [14, "Introduction"]], "Installation": [[12, "Installation"], [13, "Installation"], [14, "Installation"]], "Modeling a simple optimization problem": [[12, "Modeling-a-simple-optimization-problem"], [13, "Modeling-a-simple-optimization-problem"], [14, "Modeling-a-simple-optimization-problem"]], "Training and solving test instances": [[12, "Training-and-solving-test-instances"], [13, "Training-and-solving-test-instances"], [14, "Training-and-solving-test-instances"]], "Accessing the solution": [[12, "Accessing-the-solution"], [13, "Accessing-the-solution"], [14, "Accessing-the-solution"]], "Getting started (JuMP)": [[13, "Getting-started-(JuMP)"]], "Getting started (Pyomo)": [[14, "Getting-started-(Pyomo)"]], "Collectors & Extractors": [[0, "collectors-extractors"]], "miplearn.classifiers.minprob": [[0, "module-miplearn.classifiers.minprob"]], "miplearn.classifiers.singleclass": [[0, "module-miplearn.classifiers.singleclass"]], "miplearn.collectors.basic": [[0, "module-miplearn.collectors.basic"]], "miplearn.extractors.fields": [[0, "module-miplearn.extractors.fields"]], "miplearn.extractors.AlvLouWeh2017": [[0, "module-miplearn.extractors.AlvLouWeh2017"]], "Components": [[1, "components"]], "miplearn.components.primal.actions": [[1, "module-miplearn.components.primal.actions"]], "miplearn.components.primal.expert": [[1, "module-miplearn.components.primal.expert"]], "miplearn.components.primal.indep": [[1, "module-miplearn.components.primal.indep"]], "miplearn.components.primal.joint": [[1, "module-miplearn.components.primal.joint"]], "miplearn.components.primal.mem": [[1, "module-miplearn.components.primal.mem"]], "Benchmark Problems": [[3, "benchmark-problems"], [8, "Benchmark-Problems"]], "miplearn.problems.binpack": [[3, "module-miplearn.problems.binpack"]], "miplearn.problems.multiknapsack": [[3, "module-miplearn.problems.multiknapsack"]], "miplearn.problems.pmedian": [[3, "module-miplearn.problems.pmedian"]], "miplearn.problems.setcover": [[3, "module-miplearn.problems.setcover"]], "miplearn.problems.setpack": [[3, "module-miplearn.problems.setpack"]], "miplearn.problems.stab": [[3, "module-miplearn.problems.stab"]], "miplearn.problems.tsp": [[3, "module-miplearn.problems.tsp"]], "miplearn.problems.uc": [[3, "module-miplearn.problems.uc"]], "miplearn.problems.vertexcover": [[3, "module-miplearn.problems.vertexcover"]], "Solvers": [[4, "solvers"]], "miplearn.solvers.abstract": [[4, "module-miplearn.solvers.abstract"]], "miplearn.solvers.gurobi": [[4, "module-miplearn.solvers.gurobi"]], "miplearn.solvers.learning": [[4, "module-miplearn.solvers.learning"]], "Training Data Collectors": [[5, "Training-Data-Collectors"]], "Overview": [[5, "Overview"], [6, "Overview"], [8, "Overview"]], "HDF5 Format": [[5, "HDF5-Format"]], "Basic collector": [[5, "Basic-collector"]], "Data fields": [[5, "Data-fields"]], "Feature Extractors": [[6, "Feature-Extractors"]], "H5FieldsExtractor": [[6, "H5FieldsExtractor"]], "AlvLouWeh2017Extractor": [[6, "AlvLouWeh2017Extractor"]], "Bin Packing": [[8, "Bin-Packing"]], "Formulation": [[8, "Formulation"], [8, "id1"], [8, "id4"], [8, "id7"], [8, "id10"], [8, "id13"], [8, "id16"], [8, "id19"], [8, "id22"]], "Random instance generator": [[8, "Random-instance-generator"], [8, "id2"], [8, "id5"], [8, "id8"], [8, "id11"], [8, "id14"], [8, "id17"], [8, "id20"], [8, "id23"]], "Multi-Dimensional Knapsack": [[8, "Multi-Dimensional-Knapsack"]], "Capacitated P-Median": [[8, "Capacitated-P-Median"]], "Set cover": [[8, "Set-cover"]], "Set Packing": [[8, "Set-Packing"]], "Stable Set": [[8, "Stable-Set"]], "Traveling Salesman": [[8, "Traveling-Salesman"]], "Unit Commitment": [[8, "Unit-Commitment"]], "Vertex Cover": [[8, "Vertex-Cover"]], "Learning Solver": [[9, "Learning-Solver"]], "Configuring the solver": [[9, "Configuring-the-solver"]], "Training and solving new instances": [[9, "Training-and-solving-new-instances"], [11, "Training-and-solving-new-instances"]], "Complete example": [[9, "Complete-example"]], "User cuts and lazy constraints": [[11, "User-cuts-and-lazy-constraints"]], "Modeling the traveling salesman problem": [[11, "Modeling-the-traveling-salesman-problem"]], "Learning user cuts": [[11, "Learning-user-cuts"]]}, "indexentries": {"alvlouweh2017extractor (class in miplearn.extractors.alvlouweh2017)": [[0, "miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor"]], "basiccollector (class in miplearn.collectors.basic)": [[0, "miplearn.collectors.basic.BasicCollector"]], "h5fieldsextractor (class in miplearn.extractors.fields)": [[0, "miplearn.extractors.fields.H5FieldsExtractor"]], "minprobabilityclassifier (class in miplearn.classifiers.minprob)": [[0, "miplearn.classifiers.minprob.MinProbabilityClassifier"]], "singleclassfix (class in miplearn.classifiers.singleclass)": [[0, "miplearn.classifiers.singleclass.SingleClassFix"]], "collect() (miplearn.collectors.basic.basiccollector method)": [[0, "miplearn.collectors.basic.BasicCollector.collect"]], "fit() (miplearn.classifiers.minprob.minprobabilityclassifier method)": [[0, "miplearn.classifiers.minprob.MinProbabilityClassifier.fit"]], "fit() (miplearn.classifiers.singleclass.singleclassfix method)": [[0, "miplearn.classifiers.singleclass.SingleClassFix.fit"]], "get_constr_features() (miplearn.extractors.alvlouweh2017.alvlouweh2017extractor method)": [[0, "miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor.get_constr_features"]], "get_constr_features() (miplearn.extractors.fields.h5fieldsextractor method)": [[0, "miplearn.extractors.fields.H5FieldsExtractor.get_constr_features"]], "get_instance_features() (miplearn.extractors.alvlouweh2017.alvlouweh2017extractor method)": [[0, "miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor.get_instance_features"]], "get_instance_features() (miplearn.extractors.fields.h5fieldsextractor method)": [[0, "miplearn.extractors.fields.H5FieldsExtractor.get_instance_features"]], "get_var_features() (miplearn.extractors.alvlouweh2017.alvlouweh2017extractor method)": [[0, "miplearn.extractors.AlvLouWeh2017.AlvLouWeh2017Extractor.get_var_features"]], "get_var_features() (miplearn.extractors.fields.h5fieldsextractor method)": [[0, "miplearn.extractors.fields.H5FieldsExtractor.get_var_features"]], "miplearn.classifiers.minprob": [[0, "module-miplearn.classifiers.minprob"]], "miplearn.classifiers.singleclass": [[0, "module-miplearn.classifiers.singleclass"]], "miplearn.collectors.basic": [[0, "module-miplearn.collectors.basic"]], "miplearn.extractors.alvlouweh2017": [[0, "module-miplearn.extractors.AlvLouWeh2017"]], "miplearn.extractors.fields": [[0, "module-miplearn.extractors.fields"]], "module": [[0, "module-miplearn.classifiers.minprob"], [0, "module-miplearn.classifiers.singleclass"], [0, "module-miplearn.collectors.basic"], [0, "module-miplearn.extractors.AlvLouWeh2017"], [0, "module-miplearn.extractors.fields"], [1, "module-miplearn.components.primal.actions"], [1, "module-miplearn.components.primal.expert"], [1, "module-miplearn.components.primal.indep"], [1, "module-miplearn.components.primal.joint"], [1, "module-miplearn.components.primal.mem"], [3, "module-miplearn.problems.binpack"], [3, "module-miplearn.problems.multiknapsack"], [3, "module-miplearn.problems.pmedian"], [3, "module-miplearn.problems.setcover"], [3, "module-miplearn.problems.setpack"], [3, "module-miplearn.problems.stab"], [3, "module-miplearn.problems.tsp"], [3, "module-miplearn.problems.uc"], [3, "module-miplearn.problems.vertexcover"], [4, "module-miplearn.solvers.abstract"], [4, "module-miplearn.solvers.gurobi"], [4, "module-miplearn.solvers.learning"]], "predict() (miplearn.classifiers.minprob.minprobabilityclassifier method)": [[0, "miplearn.classifiers.minprob.MinProbabilityClassifier.predict"]], "predict() (miplearn.classifiers.singleclass.singleclassfix method)": [[0, "miplearn.classifiers.singleclass.SingleClassFix.predict"]], "set_fit_request() (miplearn.classifiers.minprob.minprobabilityclassifier method)": [[0, "miplearn.classifiers.minprob.MinProbabilityClassifier.set_fit_request"]], "set_fit_request() (miplearn.classifiers.singleclass.singleclassfix method)": [[0, "miplearn.classifiers.singleclass.SingleClassFix.set_fit_request"]], "set_predict_request() (miplearn.classifiers.minprob.minprobabilityclassifier method)": [[0, "miplearn.classifiers.minprob.MinProbabilityClassifier.set_predict_request"]], "set_predict_request() (miplearn.classifiers.singleclass.singleclassfix method)": [[0, "miplearn.classifiers.singleclass.SingleClassFix.set_predict_request"]], "enforceproximity (class in miplearn.components.primal.actions)": [[1, "miplearn.components.primal.actions.EnforceProximity"]], "expertprimalcomponent (class in miplearn.components.primal.expert)": [[1, "miplearn.components.primal.expert.ExpertPrimalComponent"]], "fixvariables (class in miplearn.components.primal.actions)": [[1, "miplearn.components.primal.actions.FixVariables"]], "independentvarsprimalcomponent (class in miplearn.components.primal.indep)": [[1, "miplearn.components.primal.indep.IndependentVarsPrimalComponent"]], "jointvarsprimalcomponent (class in miplearn.components.primal.joint)": [[1, "miplearn.components.primal.joint.JointVarsPrimalComponent"]], "memorizingprimalcomponent (class in miplearn.components.primal.mem)": [[1, "miplearn.components.primal.mem.MemorizingPrimalComponent"]], "mergetopsolutions (class in miplearn.components.primal.mem)": [[1, "miplearn.components.primal.mem.MergeTopSolutions"]], "primalcomponentaction (class in miplearn.components.primal.actions)": [[1, "miplearn.components.primal.actions.PrimalComponentAction"]], "selecttopsolutions (class in miplearn.components.primal.mem)": [[1, "miplearn.components.primal.mem.SelectTopSolutions"]], "setwarmstart (class in miplearn.components.primal.actions)": [[1, "miplearn.components.primal.actions.SetWarmStart"]], "solutionconstructor (class in miplearn.components.primal.mem)": [[1, "miplearn.components.primal.mem.SolutionConstructor"]], "before_mip() (miplearn.components.primal.expert.expertprimalcomponent method)": [[1, "miplearn.components.primal.expert.ExpertPrimalComponent.before_mip"]], "before_mip() (miplearn.components.primal.indep.independentvarsprimalcomponent method)": [[1, "miplearn.components.primal.indep.IndependentVarsPrimalComponent.before_mip"]], "before_mip() (miplearn.components.primal.joint.jointvarsprimalcomponent method)": [[1, "miplearn.components.primal.joint.JointVarsPrimalComponent.before_mip"]], "before_mip() (miplearn.components.primal.mem.memorizingprimalcomponent method)": [[1, "miplearn.components.primal.mem.MemorizingPrimalComponent.before_mip"]], "construct() (miplearn.components.primal.mem.mergetopsolutions method)": [[1, "miplearn.components.primal.mem.MergeTopSolutions.construct"]], "construct() (miplearn.components.primal.mem.selecttopsolutions method)": [[1, "miplearn.components.primal.mem.SelectTopSolutions.construct"]], "construct() (miplearn.components.primal.mem.solutionconstructor method)": [[1, "miplearn.components.primal.mem.SolutionConstructor.construct"]], "fit() (miplearn.components.primal.expert.expertprimalcomponent method)": [[1, "miplearn.components.primal.expert.ExpertPrimalComponent.fit"]], "fit() (miplearn.components.primal.indep.independentvarsprimalcomponent method)": [[1, "miplearn.components.primal.indep.IndependentVarsPrimalComponent.fit"]], "fit() (miplearn.components.primal.joint.jointvarsprimalcomponent method)": [[1, "miplearn.components.primal.joint.JointVarsPrimalComponent.fit"]], "fit() (miplearn.components.primal.mem.memorizingprimalcomponent method)": [[1, "miplearn.components.primal.mem.MemorizingPrimalComponent.fit"]], "miplearn.components.primal.actions": [[1, "module-miplearn.components.primal.actions"]], "miplearn.components.primal.expert": [[1, "module-miplearn.components.primal.expert"]], "miplearn.components.primal.indep": [[1, "module-miplearn.components.primal.indep"]], "miplearn.components.primal.joint": [[1, "module-miplearn.components.primal.joint"]], "miplearn.components.primal.mem": [[1, "module-miplearn.components.primal.mem"]], "perform() (miplearn.components.primal.actions.enforceproximity method)": [[1, "miplearn.components.primal.actions.EnforceProximity.perform"]], "perform() (miplearn.components.primal.actions.fixvariables method)": [[1, "miplearn.components.primal.actions.FixVariables.perform"]], "perform() (miplearn.components.primal.actions.primalcomponentaction method)": [[1, "miplearn.components.primal.actions.PrimalComponentAction.perform"]], "perform() (miplearn.components.primal.actions.setwarmstart method)": [[1, "miplearn.components.primal.actions.SetWarmStart.perform"]], "binpackdata (class in miplearn.problems.binpack)": [[3, "miplearn.problems.binpack.BinPackData"]], "binpackgenerator (class in miplearn.problems.binpack)": [[3, "miplearn.problems.binpack.BinPackGenerator"]], "maxweightstablesetdata (class in miplearn.problems.stab)": [[3, "miplearn.problems.stab.MaxWeightStableSetData"]], "maxweightstablesetgenerator (class in miplearn.problems.stab)": [[3, "miplearn.problems.stab.MaxWeightStableSetGenerator"]], "minweightvertexcoverdata (class in miplearn.problems.vertexcover)": [[3, "miplearn.problems.vertexcover.MinWeightVertexCoverData"]], "multiknapsackdata (class in miplearn.problems.multiknapsack)": [[3, "miplearn.problems.multiknapsack.MultiKnapsackData"]], "multiknapsackgenerator (class in miplearn.problems.multiknapsack)": [[3, "miplearn.problems.multiknapsack.MultiKnapsackGenerator"]], "pmediandata (class in miplearn.problems.pmedian)": [[3, "miplearn.problems.pmedian.PMedianData"]], "pmediangenerator (class in miplearn.problems.pmedian)": [[3, "miplearn.problems.pmedian.PMedianGenerator"]], "setcoverdata (class in miplearn.problems.setcover)": [[3, "miplearn.problems.setcover.SetCoverData"]], "setpackdata (class in miplearn.problems.setpack)": [[3, "miplearn.problems.setpack.SetPackData"]], "travelingsalesmandata (class in miplearn.problems.tsp)": [[3, "miplearn.problems.tsp.TravelingSalesmanData"]], "travelingsalesmangenerator (class in miplearn.problems.tsp)": [[3, "miplearn.problems.tsp.TravelingSalesmanGenerator"]], "unitcommitmentdata (class in miplearn.problems.uc)": [[3, "miplearn.problems.uc.UnitCommitmentData"]], "build_binpack_model_gurobipy() (in module miplearn.problems.binpack)": [[3, "miplearn.problems.binpack.build_binpack_model_gurobipy"]], "build_multiknapsack_model_gurobipy() (in module miplearn.problems.multiknapsack)": [[3, "miplearn.problems.multiknapsack.build_multiknapsack_model_gurobipy"]], "build_pmedian_model_gurobipy() (in module miplearn.problems.pmedian)": [[3, "miplearn.problems.pmedian.build_pmedian_model_gurobipy"]], "build_uc_model_gurobipy() (in module miplearn.problems.uc)": [[3, "miplearn.problems.uc.build_uc_model_gurobipy"]], "generate() (miplearn.problems.binpack.binpackgenerator method)": [[3, "miplearn.problems.binpack.BinPackGenerator.generate"]], "miplearn.problems.binpack": [[3, "module-miplearn.problems.binpack"]], "miplearn.problems.multiknapsack": [[3, "module-miplearn.problems.multiknapsack"]], "miplearn.problems.pmedian": [[3, "module-miplearn.problems.pmedian"]], "miplearn.problems.setcover": [[3, "module-miplearn.problems.setcover"]], "miplearn.problems.setpack": [[3, "module-miplearn.problems.setpack"]], "miplearn.problems.stab": [[3, "module-miplearn.problems.stab"]], "miplearn.problems.tsp": [[3, "module-miplearn.problems.tsp"]], "miplearn.problems.uc": [[3, "module-miplearn.problems.uc"]], "miplearn.problems.vertexcover": [[3, "module-miplearn.problems.vertexcover"]], "abstractmodel (class in miplearn.solvers.abstract)": [[4, "miplearn.solvers.abstract.AbstractModel"]], "gurobimodel (class in miplearn.solvers.gurobi)": [[4, "miplearn.solvers.gurobi.GurobiModel"]], "learningsolver (class in miplearn.solvers.learning)": [[4, "miplearn.solvers.learning.LearningSolver"]], "where_cuts (miplearn.solvers.abstract.abstractmodel attribute)": [[4, "miplearn.solvers.abstract.AbstractModel.WHERE_CUTS"]], "where_default (miplearn.solvers.abstract.abstractmodel attribute)": [[4, "miplearn.solvers.abstract.AbstractModel.WHERE_DEFAULT"]], "where_lazy (miplearn.solvers.abstract.abstractmodel attribute)": [[4, "miplearn.solvers.abstract.AbstractModel.WHERE_LAZY"]], "add_constr() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.add_constr"]], "add_constrs() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.add_constrs"]], "add_constrs() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.add_constrs"]], "extract_after_load() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.extract_after_load"]], "extract_after_load() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.extract_after_load"]], "extract_after_lp() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.extract_after_lp"]], "extract_after_lp() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.extract_after_lp"]], "extract_after_mip() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.extract_after_mip"]], "extract_after_mip() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.extract_after_mip"]], "fit() (miplearn.solvers.learning.learningsolver method)": [[4, "miplearn.solvers.learning.LearningSolver.fit"]], "fix_variables() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.fix_variables"]], "fix_variables() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.fix_variables"]], "lazy_enforce() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.lazy_enforce"]], "miplearn.solvers.abstract": [[4, "module-miplearn.solvers.abstract"]], "miplearn.solvers.gurobi": [[4, "module-miplearn.solvers.gurobi"]], "miplearn.solvers.learning": [[4, "module-miplearn.solvers.learning"]], "optimize() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.optimize"]], "optimize() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.optimize"]], "optimize() (miplearn.solvers.learning.learningsolver method)": [[4, "miplearn.solvers.learning.LearningSolver.optimize"]], "relax() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.relax"]], "relax() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.relax"]], "set_cuts() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.set_cuts"]], "set_time_limit() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.set_time_limit"]], "set_warm_starts() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.set_warm_starts"]], "set_warm_starts() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.set_warm_starts"]], "write() (miplearn.solvers.abstract.abstractmodel method)": [[4, "miplearn.solvers.abstract.AbstractModel.write"]], "write() (miplearn.solvers.gurobi.gurobimodel method)": [[4, "miplearn.solvers.gurobi.GurobiModel.write"]]}}) \ No newline at end of file diff --git a/0.4/tutorials/cuts-gurobipy.ipynb b/0.4/tutorials/cuts-gurobipy.ipynb new file mode 100644 index 0000000..ffdc13d --- /dev/null +++ b/0.4/tutorials/cuts-gurobipy.ipynb @@ -0,0 +1,541 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b4bd8bd6-3ce9-4932-852f-f98a44120a3e", + "metadata": {}, + "source": [ + "# User cuts and lazy constraints\n", + "\n", + "User cuts and lazy constraints are two advanced mixed-integer programming techniques that can accelerate solver performance. User cuts are additional constraints, derived from the constraints already in the model, that can tighten the feasible region and eliminate fractional solutions, thus reducing the size of the branch-and-bound tree. Lazy constraints, on the other hand, are constraints that are potentially part of the problem formulation but are omitted from the initial model to reduce its size; these constraints are added to the formulation only once the solver finds a solution that violates them. While both techniques have been successful, significant computational effort may still be required to generate strong user cuts and to identify violated lazy constraints, which can reduce their effectiveness.\n", + "\n", + "MIPLearn is able to predict which user cuts and which lazy constraints to enforce at the beginning of the optimization process, using machine learning. In this tutorial, we will use the framework to predict subtour elimination constraints for the **traveling salesman problem** using Gurobipy. We assume that MIPLearn has already been correctly installed.\n", + "\n", + "
    \n", + "\n", + "Solver Compatibility\n", + "\n", + "User cuts and lazy constraints are also supported in the Python/Pyomo and Julia/JuMP versions of the package. See the source code of build_tsp_model_pyomo and build_tsp_model_jump for more details. Note, however, the following limitations:\n", + "\n", + "- Python/Pyomo: Only `gurobi_persistent` is currently supported. PRs implementing callbacks for other persistent solvers are welcome.\n", + "- Julia/JuMP: Only solvers supporting solver-independent callbacks are supported. As of JuMP 1.19, this includes Gurobi, CPLEX, XPRESS, SCIP and GLPK. Note that HiGHS and Cbc are not supported. As newer versions of JuMP implement further callback support, MIPLearn should become automatically compatible with these solvers.\n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "72229e1f-cbd8-43f0-82ee-17d6ec9c3b7d", + "metadata": {}, + "source": [ + "## Modeling the traveling salesman problem\n", + "\n", + "Given a list of cities and the distances between them, the **traveling salesman problem (TSP)** asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp's 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes.\n", + "\n", + "To describe an instance of TSP, we need to specify the number of cities $n$, and an $n \\times n$ matrix of distances. The class `TravelingSalesmanData`, in the `miplearn.problems.tsp` package, can hold this data:" + ] + }, + { + "cell_type": "markdown", + "id": "4598a1bc-55b6-48cc-a050-2262786c203a", + "metadata": {}, + "source": [ + "```python\n", + "@dataclass\r\n", + "class TravelingSalesmanData:\r\n", + " n_cities: int\r\n", + " distances: np.ndarray\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "3a43cc12-1207-4247-bdb2-69a6a2910738", + "metadata": {}, + "source": [ + "MIPLearn also provides `TravelingSalesmandGenerator`, a random generator for TSP instances, and `build_tsp_model_gurobipy`, a function which converts `TravelingSalesmanData` into an actual gurobipy optimization model, and which uses lazy constraints to enforce subtour elimination.\n", + "\n", + "The example below is a simplified and annotated version of `build_tsp_model_gurobipy`, illustrating the usage of callbacks with MIPLearn. Compared the the previous tutorial examples, note that, in addition to defining the variables, objective function and constraints of our problem, we also define two callback functions `lazy_separate` and `lazy_enforce`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e4712a85-0327-439c-8889-933e1ff714e7", + "metadata": {}, + "outputs": [], + "source": [ + "import gurobipy as gp\n", + "from gurobipy import quicksum, GRB, tuplelist\n", + "from miplearn.solvers.gurobi import GurobiModel\n", + "import networkx as nx\n", + "import numpy as np\n", + "from miplearn.problems.tsp import (\n", + " TravelingSalesmanData,\n", + " TravelingSalesmanGenerator,\n", + ")\n", + "from scipy.stats import uniform, randint\n", + "from miplearn.io import write_pkl_gz, read_pkl_gz\n", + "from miplearn.collectors.basic import BasicCollector\n", + "from miplearn.solvers.learning import LearningSolver\n", + "from miplearn.components.lazy.mem import MemorizingLazyComponent\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "\n", + "# Set up random seed to make example more reproducible\n", + "np.random.seed(42)\n", + "\n", + "# Set up Python logging\n", + "import logging\n", + "\n", + "logging.basicConfig(level=logging.WARNING)\n", + "\n", + "\n", + "def build_tsp_model_gurobipy_simplified(data):\n", + " # Read data from file if a filename is provided\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " # Create empty gurobipy model\n", + " model = gp.Model()\n", + "\n", + " # Create set of edges between every pair of cities, for convenience\n", + " edges = tuplelist(\n", + " (i, j) for i in range(data.n_cities) for j in range(i + 1, data.n_cities)\n", + " )\n", + "\n", + " # Add binary variable x[e] for each edge e\n", + " x = model.addVars(edges, vtype=GRB.BINARY, name=\"x\")\n", + "\n", + " # Add objective function\n", + " model.setObjective(quicksum(x[(i, j)] * data.distances[i, j] for (i, j) in edges))\n", + "\n", + " # Add constraint: must choose two edges adjacent to each city\n", + " model.addConstrs(\n", + " (\n", + " quicksum(x[min(i, j), max(i, j)] for j in range(data.n_cities) if i != j)\n", + " == 2\n", + " for i in range(data.n_cities)\n", + " ),\n", + " name=\"eq_degree\",\n", + " )\n", + "\n", + " def lazy_separate(m: GurobiModel):\n", + " \"\"\"\n", + " Callback function that finds subtours in the current solution.\n", + " \"\"\"\n", + " # Query current value of the x variables\n", + " x_val = m.inner.cbGetSolution(x)\n", + "\n", + " # Initialize empty set of violations\n", + " violations = []\n", + "\n", + " # Build set of edges we have currently selected\n", + " selected_edges = [e for e in edges if x_val[e] > 0.5]\n", + "\n", + " # Build a graph containing the selected edges, using networkx\n", + " graph = nx.Graph()\n", + " graph.add_edges_from(selected_edges)\n", + "\n", + " # For each component of the graph\n", + " for component in list(nx.connected_components(graph)):\n", + "\n", + " # If the component is not the entire graph, we found a\n", + " # subtour. Add the edge cut to the list of violations.\n", + " if len(component) < data.n_cities:\n", + " cut_edges = [\n", + " [e[0], e[1]]\n", + " for e in edges\n", + " if (e[0] in component and e[1] not in component)\n", + " or (e[0] not in component and e[1] in component)\n", + " ]\n", + " violations.append(cut_edges)\n", + "\n", + " # Return the list of violations\n", + " return violations\n", + "\n", + " def lazy_enforce(m: GurobiModel, violations) -> None:\n", + " \"\"\"\n", + " Callback function that, given a list of subtours, adds lazy\n", + " constraints to remove them from the feasible region.\n", + " \"\"\"\n", + " print(f\"Enforcing {len(violations)} subtour elimination constraints\")\n", + " for violation in violations:\n", + " m.add_constr(quicksum(x[e[0], e[1]] for e in violation) >= 2)\n", + "\n", + " return GurobiModel(\n", + " model,\n", + " lazy_separate=lazy_separate,\n", + " lazy_enforce=lazy_enforce,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "58875042-d6ac-4f93-b3cc-9a5822b11dad", + "metadata": {}, + "source": [ + "The `lazy_separate` function starts by querying the current fractional solution value through `m.inner.cbGetSolution` (recall that `m.inner` is a regular gurobipy model), then finds the set of violated lazy constraints. Unlike a regular lazy constraint solver callback, note that `lazy_separate` does not add the violated constraints to the model; it simply returns a list of objects that uniquely identifies the set of lazy constraints that should be generated. Enforcing the constraints is the responsbility of the second callback function, `lazy_enforce`. This function takes as input the model and the list of violations found by `lazy_separate`, converts them into actual constraints, and adds them to the model through `m.add_constr`.\n", + "\n", + "During training data generation, MIPLearn calls `lazy_separate` and `lazy_enforce` in sequence, inside a regular solver callback. However, once the machine learning models are trained, MIPLearn calls `lazy_enforce` directly, before the optimization process starts, with a list of **predicted** violations, as we will see in the example below." + ] + }, + { + "cell_type": "markdown", + "id": "5839728e-406c-4be2-ba81-83f2b873d4b2", + "metadata": {}, + "source": [ + "
    \n", + "\n", + "Constraint Representation\n", + "\n", + "How should user cuts and lazy constraints be represented is a decision that the user can make; MIPLearn is representation agnostic. The objects returned by `lazy_separate`, however, are serialized as JSON and stored in the HDF5 training data files. Therefore, it is recommended to use only simple objects, such as lists, tuples and dictionaries.\n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "847ae32e-fad7-406a-8797-0d79065a07fd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "To test the callback defined above, we generate a small set of TSP instances, using the provided random instance generator. As in the previous tutorial, we generate some test instances and some training instances, then solve them using `BasicCollector`. Input problem data is stored in `tsp/train/00000.pkl.gz, ...`, whereas solver training data (including list of required lazy constraints) is stored in `tsp/train/00000.h5, ...`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eb63154a-1fa6-4eac-aa46-6838b9c201f6", + "metadata": {}, + "outputs": [], + "source": [ + "# Configure generator to produce instances with 50 cities located\n", + "# in the 1000 x 1000 square, and with slightly perturbed distances.\n", + "gen = TravelingSalesmanGenerator(\n", + " x=uniform(loc=0.0, scale=1000.0),\n", + " y=uniform(loc=0.0, scale=1000.0),\n", + " n=randint(low=50, high=51),\n", + " gamma=uniform(loc=1.0, scale=0.25),\n", + " fix_cities=True,\n", + " round=True,\n", + ")\n", + "\n", + "# Generate 500 instances and store input data file to .pkl.gz files\n", + "data = gen.generate(500)\n", + "train_data = write_pkl_gz(data[0:450], \"tsp/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"tsp/test\")\n", + "\n", + "# Solve the training instances in parallel, collecting the required lazy\n", + "# constraints, in addition to other information, such as optimal solution.\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_tsp_model_gurobipy_simplified, n_jobs=10)" + ] + }, + { + "cell_type": "markdown", + "id": "6903c26c-dbe0-4a2e-bced-fdbf93513dde", + "metadata": {}, + "source": [ + "## Training and solving new instances" + ] + }, + { + "cell_type": "markdown", + "id": "57cd724a-2d27-4698-a1e6-9ab8345ef31f", + "metadata": {}, + "source": [ + "After producing the training dataset, we can train the machine learning models to predict which lazy constraints are necessary. In this tutorial, we use the following ML strategy: given a new instance, find the 50 most similar ones in the training dataset and verify how often each lazy constraint was required. If a lazy constraint was required for the majority of the 50 most-similar instances, enforce it ahead-of-time for the current instance. To measure instance similarity, use the objective function only. This ML strategy can be implemented using `MemorizingLazyComponent` with `H5FieldsExtractor` and `KNeighborsClassifier`, as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "43779e3d-4174-4189-bc75-9f564910e212", + "metadata": {}, + "outputs": [], + "source": [ + "solver = LearningSolver(\n", + " components=[\n", + " MemorizingLazyComponent(\n", + " extractor=H5FieldsExtractor(instance_fields=[\"static_var_obj_coeffs\"]),\n", + " clf=KNeighborsClassifier(n_neighbors=100),\n", + " ),\n", + " ],\n", + ")\n", + "solver.fit(train_data)" + ] + }, + { + "cell_type": "markdown", + "id": "12480712-9d3d-4cbc-a6d7-d6c1e2f950f4", + "metadata": {}, + "source": [ + "Next, we solve one of the test instances using the trained solver. In the run below, we can see that MIPLearn adds many lazy constraints ahead-of-time, before the optimization starts. During the optimization process itself, some additional lazy constraints are required, but very few." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "23f904ad-f1a8-4b5a-81ae-c0b9e813a4b2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter Threads to value 1\n", + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x04d7bec1\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 4.0600000e+02 9.700000e+01 0.000000e+00 0s\n", + " 66 5.5880000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 66 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 5.588000000e+03\n", + "\n", + "User-callback calls 107, time in user-callback 0.00 sec\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:miplearn.components.cuts.mem:Predicting violated lazy constraints...\n", + "INFO:miplearn.components.lazy.mem:Enforcing 19 constraints ahead-of-time...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Enforcing 19 subtour elimination constraints\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 69 rows, 1225 columns and 6091 nonzeros\n", + "Model fingerprint: 0x09bd34d6\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Found heuristic solution: objective 29853.000000\n", + "Presolve time: 0.00s\n", + "Presolved: 69 rows, 1225 columns, 6091 nonzeros\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "\n", + "Root relaxation: objective 6.139000e+03, 93 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 6139.00000 0 6 29853.0000 6139.00000 79.4% - 0s\n", + "H 0 0 6390.0000000 6139.00000 3.93% - 0s\n", + " 0 0 6165.50000 0 10 6390.00000 6165.50000 3.51% - 0s\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6165.50000 0 6 6390.00000 6165.50000 3.51% - 0s\n", + " 0 0 6198.50000 0 16 6390.00000 6198.50000 3.00% - 0s\n", + "* 0 0 0 6219.0000000 6219.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 11\n", + " MIR: 1\n", + " Zero half: 4\n", + " Lazy constraints: 3\n", + "\n", + "Explored 1 nodes (222 simplex iterations) in 0.03 seconds (0.02 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 3: 6219 6390 29853 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 141, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "# Increase log verbosity, so that we can see what is MIPLearn doing\n", + "logging.getLogger(\"miplearn\").setLevel(logging.INFO)\n", + "\n", + "# Solve a new test instance\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);" + ] + }, + { + "cell_type": "markdown", + "id": "79cc3e61-ee2b-4f18-82cb-373d55d67de6", + "metadata": {}, + "source": [ + "Finally, we solve the same instance, but using a regular solver, without ML prediction. We can see that a much larger number of lazy constraints are added during the optimization process itself. Additionally, the solver requires a larger number of iterations to find the optimal solution. There is not a significant difference in running time because of the small size of these instances." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a015c51c-091a-43b6-b761-9f3577fc083e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x04d7bec1\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 4.0600000e+02 9.700000e+01 0.000000e+00 0s\n", + " 66 5.5880000e+03 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 66 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 5.588000000e+03\n", + "\n", + "User-callback calls 107, time in user-callback 0.00 sec\n", + "Set parameter PreCrush to value 1\n", + "Set parameter LazyConstraints to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 1 threads\n", + "\n", + "Optimize a model with 50 rows, 1225 columns and 2450 nonzeros\n", + "Model fingerprint: 0x77a94572\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+00]\n", + " Objective range [1e+01, 1e+03]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [2e+00, 2e+00]\n", + "Found heuristic solution: objective 29695.000000\n", + "Presolve time: 0.00s\n", + "Presolved: 50 rows, 1225 columns, 2450 nonzeros\n", + "Variable types: 0 continuous, 1225 integer (1225 binary)\n", + "\n", + "Root relaxation: objective 5.588000e+03, 68 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 5588.00000 0 12 29695.0000 5588.00000 81.2% - 0s\n", + "Enforcing 9 subtour elimination constraints\n", + "Enforcing 11 subtour elimination constraints\n", + "H 0 0 27241.000000 5588.00000 79.5% - 0s\n", + " 0 0 5898.00000 0 8 27241.0000 5898.00000 78.3% - 0s\n", + "Enforcing 4 subtour elimination constraints\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6066.00000 0 - 27241.0000 6066.00000 77.7% - 0s\n", + "Enforcing 2 subtour elimination constraints\n", + " 0 0 6128.00000 0 - 27241.0000 6128.00000 77.5% - 0s\n", + " 0 0 6139.00000 0 6 27241.0000 6139.00000 77.5% - 0s\n", + "H 0 0 6368.0000000 6139.00000 3.60% - 0s\n", + " 0 0 6154.75000 0 15 6368.00000 6154.75000 3.35% - 0s\n", + "Enforcing 2 subtour elimination constraints\n", + " 0 0 6154.75000 0 6 6368.00000 6154.75000 3.35% - 0s\n", + " 0 0 6165.75000 0 11 6368.00000 6165.75000 3.18% - 0s\n", + "Enforcing 3 subtour elimination constraints\n", + " 0 0 6204.00000 0 6 6368.00000 6204.00000 2.58% - 0s\n", + "* 0 0 0 6219.0000000 6219.00000 0.00% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 5\n", + " MIR: 1\n", + " Zero half: 4\n", + " Lazy constraints: 4\n", + "\n", + "Explored 1 nodes (224 simplex iterations) in 0.10 seconds (0.03 work units)\n", + "Thread count was 1 (of 20 available processors)\n", + "\n", + "Solution count 4: 6219 6368 27241 29695 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 170, time in user-callback 0.01 sec\n" + ] + } + ], + "source": [ + "solver = LearningSolver(components=[]) # empty set of ML components\n", + "solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);" + ] + }, + { + "cell_type": "markdown", + "id": "432c99b2-67fe-409b-8224-ccef91de96d1", + "metadata": {}, + "source": [ + "## Learning user cuts\n", + "\n", + "The example above focused on lazy constraints. To enforce user cuts instead, the procedure is very similar, with the following changes:\n", + "\n", + "- Instead of `lazy_separate` and `lazy_enforce`, use `cuts_separate` and `cuts_enforce`\n", + "- Instead of `m.inner.cbGetSolution`, use `m.inner.cbGetNodeRel`\n", + "\n", + "For a complete example, see `build_stab_model_gurobipy`, `build_stab_model_pyomo` and `build_stab_model_jump`, which solves the maximum-weight stable set problem using user cut callbacks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6cb694d-8c43-410f-9a13-01bf9e0763b7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/tutorials/cuts-gurobipy/index.html b/0.4/tutorials/cuts-gurobipy/index.html new file mode 100644 index 0000000..a339377 --- /dev/null +++ b/0.4/tutorials/cuts-gurobipy/index.html @@ -0,0 +1,714 @@ + + + + + + + + 4. User cuts and lazy constraints — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + +
    + + + +
    +
    +
    +
    + +
    + +
    +

    4. User cuts and lazy constraints

    +

    User cuts and lazy constraints are two advanced mixed-integer programming techniques that can accelerate solver performance. User cuts are additional constraints, derived from the constraints already in the model, that can tighten the feasible region and eliminate fractional solutions, thus reducing the size of the branch-and-bound tree. Lazy constraints, on the other hand, are constraints that are potentially part of the problem formulation but are omitted from the initial model to reduce its +size; these constraints are added to the formulation only once the solver finds a solution that violates them. While both techniques have been successful, significant computational effort may still be required to generate strong user cuts and to identify violated lazy constraints, which can reduce their effectiveness.

    +

    MIPLearn is able to predict which user cuts and which lazy constraints to enforce at the beginning of the optimization process, using machine learning. In this tutorial, we will use the framework to predict subtour elimination constraints for the traveling salesman problem using Gurobipy. We assume that MIPLearn has already been correctly installed.

    +
    +

    Solver Compatibility

    +

    User cuts and lazy constraints are also supported in the Python/Pyomo and Julia/JuMP versions of the package. See the source code of build_tsp_model_pyomo and build_tsp_model_jump for more details. Note, however, the following limitations:

    +
      +
    • Python/Pyomo: Only gurobi_persistent is currently supported. PRs implementing callbacks for other persistent solvers are welcome.

    • +
    • Julia/JuMP: Only solvers supporting solver-independent callbacks are supported. As of JuMP 1.19, this includes Gurobi, CPLEX, XPRESS, SCIP and GLPK. Note that HiGHS and Cbc are not supported. As newer versions of JuMP implement further callback support, MIPLearn should become automatically compatible with these solvers.

    • +
    +
    +
    +

    4.1. Modeling the traveling salesman problem

    +

    Given a list of cities and the distances between them, the traveling salesman problem (TSP) asks for the shortest route starting at the first city, visiting each other city exactly once, then returning to the first city. This problem is a generalization of the Hamiltonian path problem, one of Karp’s 21 NP-complete problems, and has many practical applications, including routing delivery trucks and scheduling airline routes.

    +

    To describe an instance of TSP, we need to specify the number of cities \(n\), and an \(n \times n\) matrix of distances. The class TravelingSalesmanData, in the miplearn.problems.tsp package, can hold this data:

    +
    @dataclass
    +class TravelingSalesmanData:
    +    n_cities: int
    +    distances: np.ndarray
    +
    +
    +

    MIPLearn also provides TravelingSalesmandGenerator, a random generator for TSP instances, and build_tsp_model_gurobipy, a function which converts TravelingSalesmanData into an actual gurobipy optimization model, and which uses lazy constraints to enforce subtour elimination.

    +

    The example below is a simplified and annotated version of build_tsp_model_gurobipy, illustrating the usage of callbacks with MIPLearn. Compared the the previous tutorial examples, note that, in addition to defining the variables, objective function and constraints of our problem, we also define two callback functions lazy_separate and lazy_enforce.

    +
    +
    [1]:
    +
    +
    +
    import gurobipy as gp
    +from gurobipy import quicksum, GRB, tuplelist
    +from miplearn.solvers.gurobi import GurobiModel
    +import networkx as nx
    +import numpy as np
    +from miplearn.problems.tsp import (
    +    TravelingSalesmanData,
    +    TravelingSalesmanGenerator,
    +)
    +from scipy.stats import uniform, randint
    +from miplearn.io import write_pkl_gz, read_pkl_gz
    +from miplearn.collectors.basic import BasicCollector
    +from miplearn.solvers.learning import LearningSolver
    +from miplearn.components.lazy.mem import MemorizingLazyComponent
    +from miplearn.extractors.fields import H5FieldsExtractor
    +from sklearn.neighbors import KNeighborsClassifier
    +
    +# Set up random seed to make example more reproducible
    +np.random.seed(42)
    +
    +# Set up Python logging
    +import logging
    +
    +logging.basicConfig(level=logging.WARNING)
    +
    +
    +def build_tsp_model_gurobipy_simplified(data):
    +    # Read data from file if a filename is provided
    +    if isinstance(data, str):
    +        data = read_pkl_gz(data)
    +
    +    # Create empty gurobipy model
    +    model = gp.Model()
    +
    +    # Create set of edges between every pair of cities, for convenience
    +    edges = tuplelist(
    +        (i, j) for i in range(data.n_cities) for j in range(i + 1, data.n_cities)
    +    )
    +
    +    # Add binary variable x[e] for each edge e
    +    x = model.addVars(edges, vtype=GRB.BINARY, name="x")
    +
    +    # Add objective function
    +    model.setObjective(quicksum(x[(i, j)] * data.distances[i, j] for (i, j) in edges))
    +
    +    # Add constraint: must choose two edges adjacent to each city
    +    model.addConstrs(
    +        (
    +            quicksum(x[min(i, j), max(i, j)] for j in range(data.n_cities) if i != j)
    +            == 2
    +            for i in range(data.n_cities)
    +        ),
    +        name="eq_degree",
    +    )
    +
    +    def lazy_separate(m: GurobiModel):
    +        """
    +        Callback function that finds subtours in the current solution.
    +        """
    +        # Query current value of the x variables
    +        x_val = m.inner.cbGetSolution(x)
    +
    +        # Initialize empty set of violations
    +        violations = []
    +
    +        # Build set of edges we have currently selected
    +        selected_edges = [e for e in edges if x_val[e] > 0.5]
    +
    +        # Build a graph containing the selected edges, using networkx
    +        graph = nx.Graph()
    +        graph.add_edges_from(selected_edges)
    +
    +        # For each component of the graph
    +        for component in list(nx.connected_components(graph)):
    +
    +            # If the component is not the entire graph, we found a
    +            # subtour. Add the edge cut to the list of violations.
    +            if len(component) < data.n_cities:
    +                cut_edges = [
    +                    [e[0], e[1]]
    +                    for e in edges
    +                    if (e[0] in component and e[1] not in component)
    +                    or (e[0] not in component and e[1] in component)
    +                ]
    +                violations.append(cut_edges)
    +
    +        # Return the list of violations
    +        return violations
    +
    +    def lazy_enforce(m: GurobiModel, violations) -> None:
    +        """
    +        Callback function that, given a list of subtours, adds lazy
    +        constraints to remove them from the feasible region.
    +        """
    +        print(f"Enforcing {len(violations)} subtour elimination constraints")
    +        for violation in violations:
    +            m.add_constr(quicksum(x[e[0], e[1]] for e in violation) >= 2)
    +
    +    return GurobiModel(
    +        model,
    +        lazy_separate=lazy_separate,
    +        lazy_enforce=lazy_enforce,
    +    )
    +
    +
    +
    +

    The lazy_separate function starts by querying the current fractional solution value through m.inner.cbGetSolution (recall that m.inner is a regular gurobipy model), then finds the set of violated lazy constraints. Unlike a regular lazy constraint solver callback, note that lazy_separate does not add the violated constraints to the model; it simply returns a list of objects that uniquely identifies the set of lazy constraints that should be generated. Enforcing the constraints is +the responsbility of the second callback function, lazy_enforce. This function takes as input the model and the list of violations found by lazy_separate, converts them into actual constraints, and adds them to the model through m.add_constr.

    +

    During training data generation, MIPLearn calls lazy_separate and lazy_enforce in sequence, inside a regular solver callback. However, once the machine learning models are trained, MIPLearn calls lazy_enforce directly, before the optimization process starts, with a list of predicted violations, as we will see in the example below.

    +
    +

    Constraint Representation

    +

    How should user cuts and lazy constraints be represented is a decision that the user can make; MIPLearn is representation agnostic. The objects returned by lazy_separate, however, are serialized as JSON and stored in the HDF5 training data files. Therefore, it is recommended to use only simple objects, such as lists, tuples and dictionaries.

    +
    +
    +
    +

    4.2. Generating training data

    +

    To test the callback defined above, we generate a small set of TSP instances, using the provided random instance generator. As in the previous tutorial, we generate some test instances and some training instances, then solve them using BasicCollector. Input problem data is stored in tsp/train/00000.pkl.gz, ..., whereas solver training data (including list of required lazy constraints) is stored in tsp/train/00000.h5, ....

    +
    +
    [2]:
    +
    +
    +
    # Configure generator to produce instances with 50 cities located
    +# in the 1000 x 1000 square, and with slightly perturbed distances.
    +gen = TravelingSalesmanGenerator(
    +    x=uniform(loc=0.0, scale=1000.0),
    +    y=uniform(loc=0.0, scale=1000.0),
    +    n=randint(low=50, high=51),
    +    gamma=uniform(loc=1.0, scale=0.25),
    +    fix_cities=True,
    +    round=True,
    +)
    +
    +# Generate 500 instances and store input data file to .pkl.gz files
    +data = gen.generate(500)
    +train_data = write_pkl_gz(data[0:450], "tsp/train")
    +test_data = write_pkl_gz(data[450:500], "tsp/test")
    +
    +# Solve the training instances in parallel, collecting the required lazy
    +# constraints, in addition to other information, such as optimal solution.
    +bc = BasicCollector()
    +bc.collect(train_data, build_tsp_model_gurobipy_simplified, n_jobs=10)
    +
    +
    +
    +
    +
    +

    4.3. Training and solving new instances

    +

    After producing the training dataset, we can train the machine learning models to predict which lazy constraints are necessary. In this tutorial, we use the following ML strategy: given a new instance, find the 50 most similar ones in the training dataset and verify how often each lazy constraint was required. If a lazy constraint was required for the majority of the 50 most-similar instances, enforce it ahead-of-time for the current instance. To measure instance similarity, use the objective +function only. This ML strategy can be implemented using MemorizingLazyComponent with H5FieldsExtractor and KNeighborsClassifier, as shown below.

    +
    +
    [3]:
    +
    +
    +
    solver = LearningSolver(
    +    components=[
    +        MemorizingLazyComponent(
    +            extractor=H5FieldsExtractor(instance_fields=["static_var_obj_coeffs"]),
    +            clf=KNeighborsClassifier(n_neighbors=100),
    +        ),
    +    ],
    +)
    +solver.fit(train_data)
    +
    +
    +
    +

    Next, we solve one of the test instances using the trained solver. In the run below, we can see that MIPLearn adds many lazy constraints ahead-of-time, before the optimization starts. During the optimization process itself, some additional lazy constraints are required, but very few.

    +
    +
    [4]:
    +
    +
    +
    # Increase log verbosity, so that we can see what is MIPLearn doing
    +logging.getLogger("miplearn").setLevel(logging.INFO)
    +
    +# Solve a new test instance
    +solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);
    +
    +
    +
    +
    +
    +
    +
    +
    +Set parameter Threads to value 1
    +Restricted license - for non-production use only - expires 2024-10-28
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 1 threads
    +
    +Optimize a model with 50 rows, 1225 columns and 2450 nonzeros
    +Model fingerprint: 0x04d7bec1
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [1e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Presolve time: 0.00s
    +Presolved: 50 rows, 1225 columns, 2450 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    4.0600000e+02   9.700000e+01   0.000000e+00      0s
    +      66    5.5880000e+03   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 66 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  5.588000000e+03
    +
    +User-callback calls 107, time in user-callback 0.00 sec
    +
    +
    +
    +
    +
    +
    +
    +INFO:miplearn.components.cuts.mem:Predicting violated lazy constraints...
    +INFO:miplearn.components.lazy.mem:Enforcing 19 constraints ahead-of-time...
    +
    +
    +
    +
    +
    +
    +
    +Enforcing 19 subtour elimination constraints
    +Set parameter PreCrush to value 1
    +Set parameter LazyConstraints to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 1 threads
    +
    +Optimize a model with 69 rows, 1225 columns and 6091 nonzeros
    +Model fingerprint: 0x09bd34d6
    +Variable types: 0 continuous, 1225 integer (1225 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [1e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Found heuristic solution: objective 29853.000000
    +Presolve time: 0.00s
    +Presolved: 69 rows, 1225 columns, 6091 nonzeros
    +Variable types: 0 continuous, 1225 integer (1225 binary)
    +
    +Root relaxation: objective 6.139000e+03, 93 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 6139.00000    0    6 29853.0000 6139.00000  79.4%     -    0s
    +H    0     0                    6390.0000000 6139.00000  3.93%     -    0s
    +     0     0 6165.50000    0   10 6390.00000 6165.50000  3.51%     -    0s
    +Enforcing 3 subtour elimination constraints
    +     0     0 6165.50000    0    6 6390.00000 6165.50000  3.51%     -    0s
    +     0     0 6198.50000    0   16 6390.00000 6198.50000  3.00%     -    0s
    +*    0     0               0    6219.0000000 6219.00000  0.00%     -    0s
    +
    +Cutting planes:
    +  Gomory: 11
    +  MIR: 1
    +  Zero half: 4
    +  Lazy constraints: 3
    +
    +Explored 1 nodes (222 simplex iterations) in 0.03 seconds (0.02 work units)
    +Thread count was 1 (of 20 available processors)
    +
    +Solution count 3: 6219 6390 29853
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%
    +
    +User-callback calls 141, time in user-callback 0.00 sec
    +
    +
    +

    Finally, we solve the same instance, but using a regular solver, without ML prediction. We can see that a much larger number of lazy constraints are added during the optimization process itself. Additionally, the solver requires a larger number of iterations to find the optimal solution. There is not a significant difference in running time because of the small size of these instances.

    +
    +
    [5]:
    +
    +
    +
    solver = LearningSolver(components=[])  # empty set of ML components
    +solver.optimize(test_data[0], build_tsp_model_gurobipy_simplified);
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 1 threads
    +
    +Optimize a model with 50 rows, 1225 columns and 2450 nonzeros
    +Model fingerprint: 0x04d7bec1
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [1e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Presolve time: 0.00s
    +Presolved: 50 rows, 1225 columns, 2450 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    4.0600000e+02   9.700000e+01   0.000000e+00      0s
    +      66    5.5880000e+03   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 66 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  5.588000000e+03
    +
    +User-callback calls 107, time in user-callback 0.00 sec
    +Set parameter PreCrush to value 1
    +Set parameter LazyConstraints to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 1 threads
    +
    +Optimize a model with 50 rows, 1225 columns and 2450 nonzeros
    +Model fingerprint: 0x77a94572
    +Variable types: 0 continuous, 1225 integer (1225 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+00]
    +  Objective range  [1e+01, 1e+03]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [2e+00, 2e+00]
    +Found heuristic solution: objective 29695.000000
    +Presolve time: 0.00s
    +Presolved: 50 rows, 1225 columns, 2450 nonzeros
    +Variable types: 0 continuous, 1225 integer (1225 binary)
    +
    +Root relaxation: objective 5.588000e+03, 68 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 5588.00000    0   12 29695.0000 5588.00000  81.2%     -    0s
    +Enforcing 9 subtour elimination constraints
    +Enforcing 11 subtour elimination constraints
    +H    0     0                    27241.000000 5588.00000  79.5%     -    0s
    +     0     0 5898.00000    0    8 27241.0000 5898.00000  78.3%     -    0s
    +Enforcing 4 subtour elimination constraints
    +Enforcing 3 subtour elimination constraints
    +     0     0 6066.00000    0    - 27241.0000 6066.00000  77.7%     -    0s
    +Enforcing 2 subtour elimination constraints
    +     0     0 6128.00000    0    - 27241.0000 6128.00000  77.5%     -    0s
    +     0     0 6139.00000    0    6 27241.0000 6139.00000  77.5%     -    0s
    +H    0     0                    6368.0000000 6139.00000  3.60%     -    0s
    +     0     0 6154.75000    0   15 6368.00000 6154.75000  3.35%     -    0s
    +Enforcing 2 subtour elimination constraints
    +     0     0 6154.75000    0    6 6368.00000 6154.75000  3.35%     -    0s
    +     0     0 6165.75000    0   11 6368.00000 6165.75000  3.18%     -    0s
    +Enforcing 3 subtour elimination constraints
    +     0     0 6204.00000    0    6 6368.00000 6204.00000  2.58%     -    0s
    +*    0     0               0    6219.0000000 6219.00000  0.00%     -    0s
    +
    +Cutting planes:
    +  Gomory: 5
    +  MIR: 1
    +  Zero half: 4
    +  Lazy constraints: 4
    +
    +Explored 1 nodes (224 simplex iterations) in 0.10 seconds (0.03 work units)
    +Thread count was 1 (of 20 available processors)
    +
    +Solution count 4: 6219 6368 27241 29695
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 6.219000000000e+03, best bound 6.219000000000e+03, gap 0.0000%
    +
    +User-callback calls 170, time in user-callback 0.01 sec
    +
    +
    +
    +
    +

    4.4. Learning user cuts

    +

    The example above focused on lazy constraints. To enforce user cuts instead, the procedure is very similar, with the following changes:

    +
      +
    • Instead of lazy_separate and lazy_enforce, use cuts_separate and cuts_enforce

    • +
    • Instead of m.inner.cbGetSolution, use m.inner.cbGetNodeRel

    • +
    +

    For a complete example, see build_stab_model_gurobipy, build_stab_model_pyomo and build_stab_model_jump, which solves the maximum-weight stable set problem using user cut callbacks.

    +
    +
    [ ]:
    +
    +
    +
    
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/tutorials/getting-started-gurobipy.ipynb b/0.4/tutorials/getting-started-gurobipy.ipynb new file mode 100644 index 0000000..110e3f4 --- /dev/null +++ b/0.4/tutorials/getting-started-gurobipy.ipynb @@ -0,0 +1,837 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (Gurobipy)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Python/Gurobipy version of MIPLearn\n", + "2. Model a simple optimization problem using Gurobipy\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
    \n", + "Note\n", + " \n", + "The Python/Gurobipy version of MIPLearn is only compatible with the Gurobi Optimizer. For broader solver compatibility, see the Python/Pyomo and Julia/JuMP versions of the package.\n", + "
    \n", + "\n", + "
    \n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
    \n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Gurobipy version of the package. The first step is to install Python 3.8+ in your computer. See the [official Python website for more instructions](https://www.python.org/downloads/). After Python is installed, we proceed to install MIPLearn using `pip`:\n", + "\n", + "```\n", + "$ pip install MIPLearn==0.3\n", + "```\n", + "\n", + "In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.\n", + "\n", + "```\n", + "$ pip install 'gurobipy>=10,<10.1'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Note\n", + " \n", + "In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.\n", + " \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
    \n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "22a67170-10b4-43d3-8708-014d91141e73", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:18:25.442346786Z", + "start_time": "2023-06-06T20:18:25.329017476Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "import numpy as np\n", + "\n", + "\n", + "@dataclass\n", + "class UnitCommitmentData:\n", + " demand: float\n", + " pmin: List[float]\n", + " pmax: List[float]\n", + " cfix: List[float]\n", + " cvar: List[float]" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete Pyomo model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a compressed pickle file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2f67032f-0d74-4317-b45c-19da0ec859e9", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:48:05.953902842Z", + "start_time": "2023-06-06T20:48:05.909747925Z" + } + }, + "outputs": [], + "source": [ + "import gurobipy as gp\n", + "from gurobipy import GRB, quicksum\n", + "from typing import Union\n", + "from miplearn.io import read_pkl_gz\n", + "from miplearn.solvers.gurobi import GurobiModel\n", + "\n", + "\n", + "def build_uc_model(data: Union[str, UnitCommitmentData]) -> GurobiModel:\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " model = gp.Model()\n", + " n = len(data.pmin)\n", + " x = model._x = model.addVars(n, vtype=GRB.BINARY, name=\"x\")\n", + " y = model._y = model.addVars(n, name=\"y\")\n", + " model.setObjective(\n", + " quicksum(data.cfix[i] * x[i] + data.cvar[i] * y[i] for i in range(n))\n", + " )\n", + " model.addConstrs(y[i] <= data.pmax[i] * x[i] for i in range(n))\n", + " model.addConstrs(y[i] >= data.pmin[i] * x[i] for i in range(n))\n", + " model.addConstr(quicksum(y[i] for i in range(n)) == data.demand)\n", + " return GurobiModel(model)" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2a896f47", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:14.266758244Z", + "start_time": "2023-06-06T20:49:14.223514806Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x58dfdd53\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "obj = 1320.0\n", + "x = [-0.0, 1.0, 1.0]\n", + "y = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " demand=100.0,\n", + " pmin=[10, 20, 30],\n", + " pmax=[50, 60, 70],\n", + " cfix=[700, 600, 500],\n", + " cvar=[1.5, 2.0, 2.5],\n", + " )\n", + ")\n", + "\n", + "model.optimize()\n", + "print(\"obj =\", model.inner.objVal)\n", + "print(\"x =\", [model.inner._x[i].x for i in range(3)])\n", + "print(\"y =\", [model.inner._y[i].x for i in range(3)])" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Note\n", + "\n", + "- In the example above, `GurobiModel` is just a thin wrapper around a standard Gurobi model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original Gurobi model can be accessed through `model.inner`, as illustrated above.\n", + "- To ensure training data consistency, MIPLearn requires all decision variables to have names.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5eb09fab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:22.758192368Z", + "start_time": "2023-06-06T20:49:22.724784572Z" + } + }, + "outputs": [], + "source": [ + "from scipy.stats import uniform\n", + "from typing import List\n", + "import random\n", + "\n", + "\n", + "def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)\n", + " pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)\n", + " cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)\n", + " cvar = uniform(loc=1.25, scale=0.25).rvs(n)\n", + " return [\n", + " UnitCommitmentData(\n", + " demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),\n", + " pmin=pmin,\n", + " pmax=pmax,\n", + " cfix=cfix,\n", + " cvar=cvar,\n", + " )\n", + " for _ in range(samples)\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00000.pkl.gz`, `uc/train/00001.pkl.gz`, etc., which contain the input data in compressed (gzipped) pickle format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:24.811192929Z", + "start_time": "2023-06-06T20:49:24.575639142Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.io import write_pkl_gz\n", + "\n", + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_pkl_gz(data[0:450], \"uc/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"uc/test\")" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00000.h5`, `uc/train/00001.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00000.mps.gz`, `uc/train/00001.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:34.936729253Z", + "start_time": "2023-06-06T20:49:25.936126612Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_uc_model, n_jobs=4)" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:38.997939600Z", + "start_time": "2023-06-06T20:49:38.968261432Z" + } + }, + "outputs": [], + "source": [ + "from sklearn.neighbors import KNeighborsClassifier\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:42.072345411Z", + "start_time": "2023-06-06T20:49:41.294040974Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xa8b70287\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.01s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xcf27855a\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.29153e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 8.2915e+09 8.2906e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 3 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2915e+09 8.2907e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 1\n", + " Flow cover: 2\n", + "\n", + "Explored 1 nodes (565 simplex iterations) in 0.03 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 8.29153e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%\n" + ] + }, + { + "data": { + "text/plain": [ + "{'WS: Count': 1, 'WS: Number of variables set': 482.0}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:49:44.012782276Z", + "start_time": "2023-06-06T20:49:43.813974362Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xa8b70287\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x4cbbf7c7\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 9.757128e+09\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 9.7571e+09 8.2906e+09 15.0% - 0s\n", + "H 0 0 8.298273e+09 8.2906e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + "H 0 0 8.293980e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 5 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 1 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + "H 0 0 8.291465e+09 8.2908e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 2\n", + " MIR: 1\n", + "\n", + "Explored 1 nodes (1031 simplex iterations) in 0.15 seconds (0.03 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:50:12.869892930Z", + "start_time": "2023-06-06T20:50:12.509410473Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x19042f12\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.5917580e+09 5.627453e+04 0.000000e+00 0s\n", + " 1 8.2535968e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.253596777e+09\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xf97cde91\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.25814e+09 (0.00s)\n", + "User MIP start produced solution with objective 8.25512e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.25459e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2536e+09 0 1 8.2546e+09 8.2536e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 3 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 1 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 4 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 5 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 6 8.2546e+09 8.2538e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + " MIR: 2\n", + " StrongCG: 1\n", + " Flow cover: 1\n", + "\n", + "Explored 1 nodes (575 simplex iterations) in 0.05 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%\n", + "obj = 8254590409.969726\n", + "x = [1.0, 1.0, 0.0]\n", + "y = [935662.0949262811, 1604270.0218116897, 0.0]\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[0]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "print(\"obj =\", model.inner.objVal)\n", + "print(\"x =\", [model.inner._x[i].x for i in range(3)])\n", + "print(\"y =\", [model.inner._y[i].x for i in range(3)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5593d23a-83bd-4e16-8253-6300f5e3f63b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/tutorials/getting-started-gurobipy/index.html b/0.4/tutorials/getting-started-gurobipy/index.html new file mode 100644 index 0000000..dc6702c --- /dev/null +++ b/0.4/tutorials/getting-started-gurobipy/index.html @@ -0,0 +1,888 @@ + + + + + + + + 2. Getting started (Gurobipy) — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    2. Getting started (Gurobipy)

    +
    +

    2.1. Introduction

    +

    MIPLearn is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:

    +
      +
    1. Install the Python/Gurobipy version of MIPLearn

    2. +
    3. Model a simple optimization problem using Gurobipy

    4. +
    5. Generate training data and train the ML models

    6. +
    7. Use the ML models together Gurobi to solve new instances

    8. +
    +
    +

    Note

    +

    The Python/Gurobipy version of MIPLearn is only compatible with the Gurobi Optimizer. For broader solver compatibility, see the Python/Pyomo and Julia/JuMP versions of the package.

    +
    +
    +

    Warning

    +

    MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!

    +
    +
    +
    +

    2.2. Installation

    +

    MIPLearn is available in two versions:

    +
      +
    • Python version, compatible with the Pyomo and Gurobipy modeling languages,

    • +
    • Julia version, compatible with the JuMP modeling language.

    • +
    +

    In this tutorial, we will demonstrate how to use and install the Python/Gurobipy version of the package. The first step is to install Python 3.8+ in your computer. See the official Python website for more instructions. After Python is installed, we proceed to install MIPLearn using pip:

    +
    $ pip install MIPLearn==0.3
    +
    +
    +

    In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.

    +
    $ pip install 'gurobipy>=10,<10.1'
    +
    +
    +
    +

    Note

    +

    In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.

    +
    +
    +
    +

    2.3. Modeling a simple optimization problem

    +

    To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the unit commitment problem, a practical optimization problem solved daily by electric grid operators around the world.

    +

    Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns \(n\) generators, denoted by \(g_1, \ldots, g_n\). Each generator can either be online or offline. An online generator \(g_i\) can produce between \(p^\text{min}_i\) to \(p^\text{max}_i\) megawatts of power, and it costs the company +\(c^\text{fix}_i + c^\text{var}_i y_i\), where \(y_i\) is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand \(d\) (in megawatts).

    +

    This simple problem can be modeled as a mixed-integer linear optimization problem as follows. For each generator \(g_i\), let \(x_i \in \{0,1\}\) be a decision variable indicating whether \(g_i\) is online, and let \(y_i \geq 0\) be a decision variable indicating how much power does \(g_i\) produce. The problem is then given by:

    +
    +\[\begin{split}\begin{align} +\text{minimize } \quad & \sum_{i=1}^n \left( c^\text{fix}_i x_i + c^\text{var}_i y_i \right) \\ +\text{subject to } \quad & y_i \leq p^\text{max}_i x_i & i=1,\ldots,n \\ +& y_i \geq p^\text{min}_i x_i & i=1,\ldots,n \\ +& \sum_{i=1}^n y_i = d \\ +& x_i \in \{0,1\} & i=1,\ldots,n \\ +& y_i \geq 0 & i=1,\ldots,n +\end{align}\end{split}\]
    +
    +

    Note

    +

    We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.

    +
    +

    Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class UnitCommitmentData, which holds all the input data.

    +
    +
    [1]:
    +
    +
    +
    from dataclasses import dataclass
    +from typing import List
    +
    +import numpy as np
    +
    +
    +@dataclass
    +class UnitCommitmentData:
    +    demand: float
    +    pmin: List[float]
    +    pmax: List[float]
    +    cfix: List[float]
    +    cvar: List[float]
    +
    +
    +
    +

    Next, we write a build_uc_model function, which converts the input data into a concrete Pyomo model. The function accepts UnitCommitmentData, the data structure we previously defined, or the path to a compressed pickle file containing this data.

    +
    +
    [2]:
    +
    +
    +
    import gurobipy as gp
    +from gurobipy import GRB, quicksum
    +from typing import Union
    +from miplearn.io import read_pkl_gz
    +from miplearn.solvers.gurobi import GurobiModel
    +
    +
    +def build_uc_model(data: Union[str, UnitCommitmentData]) -> GurobiModel:
    +    if isinstance(data, str):
    +        data = read_pkl_gz(data)
    +
    +    model = gp.Model()
    +    n = len(data.pmin)
    +    x = model._x = model.addVars(n, vtype=GRB.BINARY, name="x")
    +    y = model._y = model.addVars(n, name="y")
    +    model.setObjective(
    +        quicksum(data.cfix[i] * x[i] + data.cvar[i] * y[i] for i in range(n))
    +    )
    +    model.addConstrs(y[i] <= data.pmax[i] * x[i] for i in range(n))
    +    model.addConstrs(y[i] >= data.pmin[i] * x[i] for i in range(n))
    +    model.addConstr(quicksum(y[i] for i in range(n)) == data.demand)
    +    return GurobiModel(model)
    +
    +
    +
    +

    At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:

    +
    +
    [3]:
    +
    +
    +
    model = build_uc_model(
    +    UnitCommitmentData(
    +        demand=100.0,
    +        pmin=[10, 20, 30],
    +        pmax=[50, 60, 70],
    +        cfix=[700, 600, 500],
    +        cvar=[1.5, 2.0, 2.5],
    +    )
    +)
    +
    +model.optimize()
    +print("obj =", model.inner.objVal)
    +print("x =", [model.inner._x[i].x for i in range(3)])
    +print("y =", [model.inner._y[i].x for i in range(3)])
    +
    +
    +
    +
    +
    +
    +
    +
    +Restricted license - for non-production use only - expires 2024-10-28
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 7 rows, 6 columns and 15 nonzeros
    +Model fingerprint: 0x58dfdd53
    +Variable types: 3 continuous, 3 integer (3 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 7e+01]
    +  Objective range  [2e+00, 7e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+02, 1e+02]
    +Presolve removed 2 rows and 1 columns
    +Presolve time: 0.00s
    +Presolved: 5 rows, 5 columns, 13 nonzeros
    +Variable types: 0 continuous, 5 integer (3 binary)
    +Found heuristic solution: objective 1400.0000000
    +
    +Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 1035.00000    0    1 1400.00000 1035.00000  26.1%     -    0s
    +     0     0 1105.71429    0    1 1400.00000 1105.71429  21.0%     -    0s
    +*    0     0               0    1320.0000000 1320.00000  0.00%     -    0s
    +
    +Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 2: 1320 1400
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%
    +obj = 1320.0
    +x = [-0.0, 1.0, 1.0]
    +y = [0.0, 60.0, 40.0]
    +
    +
    +

    Running the code above, we found that the optimal solution for our small problem instance costs $1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power.

    +
    +

    Note

    +
      +
    • In the example above, GurobiModel is just a thin wrapper around a standard Gurobi model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as optimize. For more control, and to query the solution, the original Gurobi model can be accessed through model.inner, as illustrated above.

    • +
    • To ensure training data consistency, MIPLearn requires all decision variables to have names.

    • +
    +
    +
    +
    +

    2.4. Generating training data

    +

    Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a trained solver, which can optimize new instances (similar to the ones it was trained on) faster.

    +

    In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a +random instance generator:

    +
    +
    [4]:
    +
    +
    +
    from scipy.stats import uniform
    +from typing import List
    +import random
    +
    +
    +def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:
    +    random.seed(seed)
    +    np.random.seed(seed)
    +    pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)
    +    pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)
    +    cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)
    +    cvar = uniform(loc=1.25, scale=0.25).rvs(n)
    +    return [
    +        UnitCommitmentData(
    +            demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),
    +            pmin=pmin,
    +            pmax=pmax,
    +            cfix=cfix,
    +            cvar=cvar,
    +        )
    +        for _ in range(samples)
    +    ]
    +
    +
    +
    +

    In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.

    +

    Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple +machines. The code below generates the files uc/train/00000.pkl.gz, uc/train/00001.pkl.gz, etc., which contain the input data in compressed (gzipped) pickle format.

    +
    +
    [5]:
    +
    +
    +
    from miplearn.io import write_pkl_gz
    +
    +data = random_uc_data(samples=500, n=500)
    +train_data = write_pkl_gz(data[0:450], "uc/train")
    +test_data = write_pkl_gz(data[450:500], "uc/test")
    +
    +
    +
    +

    Finally, we use BasicCollector to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files uc/train/00000.h5, uc/train/00001.h5, etc. The optimization models are also exported to compressed MPS files uc/train/00000.mps.gz, uc/train/00001.mps.gz, etc.

    +
    +
    [6]:
    +
    +
    +
    from miplearn.collectors.basic import BasicCollector
    +
    +bc = BasicCollector()
    +bc.collect(train_data, build_uc_model, n_jobs=4)
    +
    +
    +
    +
    +
    +

    2.5. Training and solving test instances

    +

    With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using \(k\)-nearest neighbors. More specifically, the strategy is to:

    +
      +
    1. Memorize the optimal solutions of all training instances;

    2. +
    3. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;

    4. +
    5. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.

    6. +
    7. Provide this partial solution to the solver as a warm start.

    8. +
    +

    This simple strategy can be implemented as shown below, using MemorizingPrimalComponent. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide.

    +
    +
    [7]:
    +
    +
    +
    from sklearn.neighbors import KNeighborsClassifier
    +from miplearn.components.primal.actions import SetWarmStart
    +from miplearn.components.primal.mem import (
    +    MemorizingPrimalComponent,
    +    MergeTopSolutions,
    +)
    +from miplearn.extractors.fields import H5FieldsExtractor
    +
    +comp = MemorizingPrimalComponent(
    +    clf=KNeighborsClassifier(n_neighbors=25),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_constr_rhs"],
    +    ),
    +    constructor=MergeTopSolutions(25, [0.0, 1.0]),
    +    action=SetWarmStart(),
    +)
    +
    +
    +
    +

    Having defined the ML strategy, we next construct LearningSolver, train the ML component and optimize one of the test instances.

    +
    +
    [8]:
    +
    +
    +
    from miplearn.solvers.learning import LearningSolver
    +
    +solver_ml = LearningSolver(components=[comp])
    +solver_ml.fit(train_data)
    +solver_ml.optimize(test_data[0], build_uc_model)
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xa8b70287
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.01s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
    +       1    8.2906219e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.290621916e+09
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xcf27855a
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +
    +User MIP start produced solution with objective 8.29153e+09 (0.01s)
    +User MIP start produced solution with objective 8.29153e+09 (0.01s)
    +Loaded user MIP start with objective 8.29153e+09
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2906e+09    0    1 8.2915e+09 8.2906e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    3 8.2915e+09 8.2907e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    1 8.2915e+09 8.2907e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    2 8.2915e+09 8.2907e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Gomory: 1
    +  Flow cover: 2
    +
    +Explored 1 nodes (565 simplex iterations) in 0.03 seconds (0.01 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: 8.29153e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%
    +
    +
    +
    +
    [8]:
    +
    +
    +
    +
    +{'WS: Count': 1, 'WS: Number of variables set': 482.0}
    +
    +
    +

    By examining the solve log above, specifically the line Loaded user MIP start with objective..., we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided.

    +
    +
    [9]:
    +
    +
    +
    solver_baseline = LearningSolver(components=[])
    +solver_baseline.fit(train_data)
    +solver_baseline.optimize(test_data[0], build_uc_model)
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xa8b70287
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.00s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
    +       1    8.2906219e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.290621916e+09
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x4cbbf7c7
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Found heuristic solution: objective 9.757128e+09
    +
    +Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2906e+09    0    1 9.7571e+09 8.2906e+09  15.0%     -    0s
    +H    0     0                    8.298273e+09 8.2906e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    1 8.2983e+09 8.2907e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
    +H    0     0                    8.293980e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    5 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    1 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    2 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    1 8.2940e+09 8.2908e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
    +H    0     0                    8.291465e+09 8.2908e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Gomory: 2
    +  MIR: 1
    +
    +Explored 1 nodes (1031 simplex iterations) in 0.15 seconds (0.03 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%
    +
    +
    +
    +
    [9]:
    +
    +
    +
    +
    +{}
    +
    +
    +

    In the log above, the MIP start line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems.

    +
    +
    +

    2.6. Accessing the solution

    +

    In the example above, we used LearningSolver.solve together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver.

    +
    +
    [10]:
    +
    +
    +
    data = random_uc_data(samples=1, n=500)[0]
    +model = build_uc_model(data)
    +solver_ml.optimize(model)
    +print("obj =", model.inner.objVal)
    +print("x =", [model.inner._x[i].x for i in range(3)])
    +print("y =", [model.inner._y[i].x for i in range(3)])
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x19042f12
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.00s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.5917580e+09   5.627453e+04   0.000000e+00      0s
    +       1    8.2535968e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.253596777e+09
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xf97cde91
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +
    +User MIP start produced solution with objective 8.25814e+09 (0.00s)
    +User MIP start produced solution with objective 8.25512e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25459e+09 (0.01s)
    +User MIP start produced solution with objective 8.25459e+09 (0.01s)
    +Loaded user MIP start with objective 8.25459e+09
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2536e+09    0    1 8.2546e+09 8.2536e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    3 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    1 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    4 8.2546e+09 8.2538e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    5 8.2546e+09 8.2538e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    6 8.2546e+09 8.2538e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Cover: 1
    +  MIR: 2
    +  StrongCG: 1
    +  Flow cover: 1
    +
    +Explored 1 nodes (575 simplex iterations) in 0.05 seconds (0.01 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%
    +obj = 8254590409.969726
    +x = [1.0, 1.0, 0.0]
    +y = [935662.0949262811, 1604270.0218116897, 0.0]
    +
    +
    +
    +
    [ ]:
    +
    +
    +
    
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/tutorials/getting-started-jump.ipynb b/0.4/tutorials/getting-started-jump.ipynb new file mode 100644 index 0000000..8dbf587 --- /dev/null +++ b/0.4/tutorials/getting-started-jump.ipynb @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (JuMP)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Julia/JuMP version of MIPLearn\n", + "2. Model a simple optimization problem using JuMP\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
    \n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
    \n" + ] + }, + { + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Julia in your machine. See the [official Julia website for more instructions](https://julialang.org/downloads/). After Julia is installed, launch the Julia REPL, type `]` to enter package mode, then install MIPLearn:\n", + "\n", + "```\n", + "pkg> add MIPLearn@0.3\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "e8274543", + "metadata": {}, + "source": [ + "In addition to MIPLearn itself, we will also install:\n", + "\n", + "- the JuMP modeling language\n", + "- Gurobi, a state-of-the-art commercial MILP solver\n", + "- Distributions, to generate random data\n", + "- PyCall, to access ML model from Scikit-Learn\n", + "- Suppressor, to make the output cleaner\n", + "\n", + "```\n", + "pkg> add JuMP@1, Gurobi@1, Distributions@0.25, PyCall@1, Suppressor@0.2\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Note\n", + "\n", + "- If you do not have a Gurobi license available, you can also follow the tutorial by installing an open-source solver, such as `HiGHS`, and replacing `Gurobi.Optimizer` by `HiGHS.Optimizer` in all the code examples.\n", + "- In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Julia projects.\n", + " \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
    \n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Julia and JuMP. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c62ebff1-db40-45a1-9997-d121837f067b", + "metadata": {}, + "outputs": [], + "source": [ + "struct UnitCommitmentData\n", + " demand::Float64\n", + " pmin::Vector{Float64}\n", + " pmax::Vector{Float64}\n", + " cfix::Vector{Float64}\n", + " cvar::Vector{Float64}\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete JuMP model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a JLD2 file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "79ef7775-18ca-4dfa-b438-49860f762ad0", + "metadata": {}, + "outputs": [], + "source": [ + "using MIPLearn\n", + "using JuMP\n", + "using Gurobi\n", + "\n", + "function build_uc_model(data)\n", + " if data isa String\n", + " data = read_jld2(data)\n", + " end\n", + " model = Model(Gurobi.Optimizer)\n", + " G = 1:length(data.pmin)\n", + " @variable(model, x[G], Bin)\n", + " @variable(model, y[G] >= 0)\n", + " @objective(model, Min, sum(data.cfix[g] * x[g] + data.cvar[g] * y[g] for g in G))\n", + " @constraint(model, eq_max_power[g in G], y[g] <= data.pmax[g] * x[g])\n", + " @constraint(model, eq_min_power[g in G], y[g] >= data.pmin[g] * x[g])\n", + " @constraint(model, eq_demand, sum(y[g] for g in G) == data.demand)\n", + " return JumpModel(model)\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Gurobi to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "dd828d68-fd43-4d2a-a058-3e2628d99d9e", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:01:10.993801745Z", + "start_time": "2023-06-06T20:01:10.887580927Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x55e33a07\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.00 seconds (0.00 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "\n", + "User-callback calls 371, time in user-callback 0.00 sec\n", + "objective_value(model.inner) = 1320.0\n", + "Vector(value.(model.inner[:x])) = [-0.0, 1.0, 1.0]\n", + "Vector(value.(model.inner[:y])) = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " 100.0, # demand\n", + " [10, 20, 30], # pmin\n", + " [50, 60, 70], # pmax\n", + " [700, 600, 500], # cfix\n", + " [1.5, 2.0, 2.5], # cvar\n", + " )\n", + ")\n", + "model.optimize()\n", + "@show objective_value(model.inner)\n", + "@show Vector(value.(model.inner[:x]))\n", + "@show Vector(value.(model.inner[:y]));" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Notes\n", + " \n", + "- In the example above, `JumpModel` is just a thin wrapper around a standard JuMP model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original JuMP model can be accessed through `model.inner`, as illustrated above.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1326efd7-3869-4137-ab6b-df9cb609a7e0", + "metadata": {}, + "outputs": [], + "source": [ + "using Distributions\n", + "using Random\n", + "\n", + "function random_uc_data(; samples::Int, n::Int, seed::Int=42)::Vector\n", + " Random.seed!(seed)\n", + " pmin = rand(Uniform(100_000, 500_000), n)\n", + " pmax = pmin .* rand(Uniform(2, 2.5), n)\n", + " cfix = pmin .* rand(Uniform(100, 125), n)\n", + " cvar = rand(Uniform(1.25, 1.50), n)\n", + " return [\n", + " UnitCommitmentData(\n", + " sum(pmax) * rand(Uniform(0.5, 0.75)),\n", + " pmin,\n", + " pmax,\n", + " cfix,\n", + " cvar,\n", + " )\n", + " for _ in 1:samples\n", + " ]\n", + "end;" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00001.jld2`, `uc/train/00002.jld2`, etc., which contain the input data in JLD2 format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:04.782830561Z", + "start_time": "2023-06-06T20:03:04.530421396Z" + } + }, + "outputs": [], + "source": [ + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_jld2(data[1:450], \"uc/train\")\n", + "test_data = write_jld2(data[451:500], \"uc/test\");" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00001.h5`, `uc/train/00002.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00001.mps.gz`, `uc/train/00002.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:35.571497019Z", + "start_time": "2023-06-06T20:03:25.804104036Z" + } + }, + "outputs": [], + "source": [ + "using Suppressor\n", + "@suppress_out begin\n", + " bc = BasicCollector()\n", + " bc.collect(train_data, build_uc_model)\n", + "end" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:20.497772794Z", + "start_time": "2023-06-06T20:05:20.484821405Z" + } + }, + "outputs": [], + "source": [ + "# Load kNN classifier from Scikit-Learn\n", + "using PyCall\n", + "KNeighborsClassifier = pyimport(\"sklearn.neighbors\").KNeighborsClassifier\n", + "\n", + "# Build the MIPLearn component\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ");" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:22.672002339Z", + "start_time": "2023-06-06T20:05:21.447466634Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xd2378195\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "\n", + "User MIP start produced solution with objective 1.02165e+10 (0.00s)\n", + "Loaded user MIP start with objective 1.02165e+10\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.0216e+10 0 1 1.0217e+10 1.0216e+10 0.01% - 0s\n", + "\n", + "Explored 1 nodes (510 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 1: 1.02165e+10 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.021651058978e+10, best bound 1.021567971257e+10, gap 0.0081%\n", + "\n", + "User-callback calls 169, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[1], build_uc_model);" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:46.969575966Z", + "start_time": "2023-06-06T20:05:46.420803286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0xb45c0594\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 1.071463e+10\n", + "\n", + "Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1.0216e+10 0 1 1.0715e+10 1.0216e+10 4.66% - 0s\n", + "H 0 0 1.025162e+10 1.0216e+10 0.35% - 0s\n", + " 0 0 1.0216e+10 0 1 1.0252e+10 1.0216e+10 0.35% - 0s\n", + "H 0 0 1.023090e+10 1.0216e+10 0.15% - 0s\n", + "H 0 0 1.022335e+10 1.0216e+10 0.07% - 0s\n", + "H 0 0 1.022281e+10 1.0216e+10 0.07% - 0s\n", + "H 0 0 1.021753e+10 1.0216e+10 0.02% - 0s\n", + "H 0 0 1.021752e+10 1.0216e+10 0.02% - 0s\n", + " 0 0 1.0216e+10 0 3 1.0218e+10 1.0216e+10 0.02% - 0s\n", + " 0 0 1.0216e+10 0 1 1.0218e+10 1.0216e+10 0.02% - 0s\n", + "H 0 0 1.021651e+10 1.0216e+10 0.01% - 0s\n", + "\n", + "Explored 1 nodes (764 simplex iterations) in 0.03 seconds (0.02 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 7: 1.02165e+10 1.02175e+10 1.02228e+10 ... 1.07146e+10\n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.021651058978e+10, best bound 1.021573363741e+10, gap 0.0076%\n", + "\n", + "User-callback calls 204, time in user-callback 0.00 sec\n" + ] + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[1], build_uc_model);" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a JuMP model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:06:26.913448568Z", + "start_time": "2023-06-06T20:06:26.169047914Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)\n", + "\n", + "CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]\n", + "Thread count: 16 physical cores, 32 logical processors, using up to 32 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x974a7fba\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 1e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [0e+00, 0e+00]\n", + " RHS range [2e+08, 2e+08]\n", + "\n", + "User MIP start produced solution with objective 9.86729e+09 (0.00s)\n", + "User MIP start produced solution with objective 9.86675e+09 (0.00s)\n", + "User MIP start produced solution with objective 9.86654e+09 (0.01s)\n", + "User MIP start produced solution with objective 9.8661e+09 (0.01s)\n", + "Loaded user MIP start with objective 9.8661e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 9.865344e+09, 510 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 9.8653e+09 0 1 9.8661e+09 9.8653e+09 0.01% - 0s\n", + "\n", + "Explored 1 nodes (510 simplex iterations) in 0.02 seconds (0.01 work units)\n", + "Thread count was 32 (of 32 available processors)\n", + "\n", + "Solution count 4: 9.8661e+09 9.86654e+09 9.86675e+09 9.86729e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 9.866096485614e+09, best bound 9.865343669936e+09, gap 0.0076%\n", + "\n", + "User-callback calls 182, time in user-callback 0.00 sec\n", + "objective_value(model.inner) = 9.866096485613789e9\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[1]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "@show objective_value(model.inner);" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Julia 1.9.0", + "language": "julia", + "name": "julia-1.9" + }, + "language_info": { + "file_extension": ".jl", + "mimetype": "application/julia", + "name": "julia", + "version": "1.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/tutorials/getting-started-jump/index.html b/0.4/tutorials/getting-started-jump/index.html new file mode 100644 index 0000000..877fa2b --- /dev/null +++ b/0.4/tutorials/getting-started-jump/index.html @@ -0,0 +1,755 @@ + + + + + + + + 3. Getting started (JuMP) — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    3. Getting started (JuMP)

    +
    +

    3.1. Introduction

    +

    MIPLearn is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:

    +
      +
    1. Install the Julia/JuMP version of MIPLearn

    2. +
    3. Model a simple optimization problem using JuMP

    4. +
    5. Generate training data and train the ML models

    6. +
    7. Use the ML models together Gurobi to solve new instances

    8. +
    +
    +

    Warning

    +

    MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!

    +
    +
    +
    +

    3.2. Installation

    +

    MIPLearn is available in two versions:

    +
      +
    • Python version, compatible with the Pyomo and Gurobipy modeling languages,

    • +
    • Julia version, compatible with the JuMP modeling language.

    • +
    +

    In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Julia in your machine. See the official Julia website for more instructions. After Julia is installed, launch the Julia REPL, type ] to enter package mode, then install MIPLearn:

    +
    pkg> add MIPLearn@0.3
    +
    +
    +

    In addition to MIPLearn itself, we will also install:

    +
      +
    • the JuMP modeling language

    • +
    • Gurobi, a state-of-the-art commercial MILP solver

    • +
    • Distributions, to generate random data

    • +
    • PyCall, to access ML model from Scikit-Learn

    • +
    • Suppressor, to make the output cleaner

    • +
    +
    pkg> add JuMP@1, Gurobi@1, Distributions@0.25, PyCall@1, Suppressor@0.2
    +
    +
    +
    +

    Note

    +
      +
    • If you do not have a Gurobi license available, you can also follow the tutorial by installing an open-source solver, such as HiGHS, and replacing Gurobi.Optimizer by HiGHS.Optimizer in all the code examples.

    • +
    • In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Julia projects.

    • +
    +
    +
    +
    +

    3.3. Modeling a simple optimization problem

    +

    To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the unit commitment problem, a practical optimization problem solved daily by electric grid operators around the world.

    +

    Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns \(n\) generators, denoted by \(g_1, \ldots, g_n\). Each generator can either be online or offline. An online generator \(g_i\) can produce between \(p^\text{min}_i\) to \(p^\text{max}_i\) megawatts of power, and it costs the company +\(c^\text{fix}_i + c^\text{var}_i y_i\), where \(y_i\) is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand \(d\) (in megawatts).

    +

    This simple problem can be modeled as a mixed-integer linear optimization problem as follows. For each generator \(g_i\), let \(x_i \in \{0,1\}\) be a decision variable indicating whether \(g_i\) is online, and let \(y_i \geq 0\) be a decision variable indicating how much power does \(g_i\) produce. The problem is then given by:

    +
    +\[\begin{split}\begin{align} +\text{minimize } \quad & \sum_{i=1}^n \left( c^\text{fix}_i x_i + c^\text{var}_i y_i \right) \\ +\text{subject to } \quad & y_i \leq p^\text{max}_i x_i & i=1,\ldots,n \\ +& y_i \geq p^\text{min}_i x_i & i=1,\ldots,n \\ +& \sum_{i=1}^n y_i = d \\ +& x_i \in \{0,1\} & i=1,\ldots,n \\ +& y_i \geq 0 & i=1,\ldots,n +\end{align}\end{split}\]
    +
    +

    Note

    +

    We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.

    +
    +

    Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Julia and JuMP. We start by defining a data class UnitCommitmentData, which holds all the input data.

    +
    +
    [1]:
    +
    +
    +
    struct UnitCommitmentData
    +    demand::Float64
    +    pmin::Vector{Float64}
    +    pmax::Vector{Float64}
    +    cfix::Vector{Float64}
    +    cvar::Vector{Float64}
    +end;
    +
    +
    +
    +

    Next, we write a build_uc_model function, which converts the input data into a concrete JuMP model. The function accepts UnitCommitmentData, the data structure we previously defined, or the path to a JLD2 file containing this data.

    +
    +
    [2]:
    +
    +
    +
    using MIPLearn
    +using JuMP
    +using Gurobi
    +
    +function build_uc_model(data)
    +    if data isa String
    +        data = read_jld2(data)
    +    end
    +    model = Model(Gurobi.Optimizer)
    +    G = 1:length(data.pmin)
    +    @variable(model, x[G], Bin)
    +    @variable(model, y[G] >= 0)
    +    @objective(model, Min, sum(data.cfix[g] * x[g] + data.cvar[g] * y[g] for g in G))
    +    @constraint(model, eq_max_power[g in G], y[g] <= data.pmax[g] * x[g])
    +    @constraint(model, eq_min_power[g in G], y[g] >= data.pmin[g] * x[g])
    +    @constraint(model, eq_demand, sum(y[g] for g in G) == data.demand)
    +    return JumpModel(model)
    +end;
    +
    +
    +
    +

    At this point, we can already use Gurobi to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:

    +
    +
    [3]:
    +
    +
    +
    model = build_uc_model(
    +    UnitCommitmentData(
    +        100.0,  # demand
    +        [10, 20, 30],  # pmin
    +        [50, 60, 70],  # pmax
    +        [700, 600, 500],  # cfix
    +        [1.5, 2.0, 2.5],  # cvar
    +    )
    +)
    +model.optimize()
    +@show objective_value(model.inner)
    +@show Vector(value.(model.inner[:x]))
    +@show Vector(value.(model.inner[:y]));
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)
    +
    +CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]
    +Thread count: 16 physical cores, 32 logical processors, using up to 32 threads
    +
    +Optimize a model with 7 rows, 6 columns and 15 nonzeros
    +Model fingerprint: 0x55e33a07
    +Variable types: 3 continuous, 3 integer (3 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 7e+01]
    +  Objective range  [2e+00, 7e+02]
    +  Bounds range     [0e+00, 0e+00]
    +  RHS range        [1e+02, 1e+02]
    +Presolve removed 2 rows and 1 columns
    +Presolve time: 0.00s
    +Presolved: 5 rows, 5 columns, 13 nonzeros
    +Variable types: 0 continuous, 5 integer (3 binary)
    +Found heuristic solution: objective 1400.0000000
    +
    +Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 1035.00000    0    1 1400.00000 1035.00000  26.1%     -    0s
    +     0     0 1105.71429    0    1 1400.00000 1105.71429  21.0%     -    0s
    +*    0     0               0    1320.0000000 1320.00000  0.00%     -    0s
    +
    +Explored 1 nodes (5 simplex iterations) in 0.00 seconds (0.00 work units)
    +Thread count was 32 (of 32 available processors)
    +
    +Solution count 2: 1320 1400
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%
    +
    +User-callback calls 371, time in user-callback 0.00 sec
    +objective_value(model.inner) = 1320.0
    +Vector(value.(model.inner[:x])) = [-0.0, 1.0, 1.0]
    +Vector(value.(model.inner[:y])) = [0.0, 60.0, 40.0]
    +
    +
    +

    Running the code above, we found that the optimal solution for our small problem instance costs $1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power.

    +
    +

    Notes

    +
      +
    • In the example above, JumpModel is just a thin wrapper around a standard JuMP model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as optimize. For more control, and to query the solution, the original JuMP model can be accessed through model.inner, as illustrated above.

    • +
    +
    +
    +
    +

    3.4. Generating training data

    +

    Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a trained solver, which can optimize new instances (similar to the ones it was trained on) faster.

    +

    In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a +random instance generator:

    +
    +
    [4]:
    +
    +
    +
    using Distributions
    +using Random
    +
    +function random_uc_data(; samples::Int, n::Int, seed::Int=42)::Vector
    +    Random.seed!(seed)
    +    pmin = rand(Uniform(100_000, 500_000), n)
    +    pmax = pmin .* rand(Uniform(2, 2.5), n)
    +    cfix = pmin .* rand(Uniform(100, 125), n)
    +    cvar = rand(Uniform(1.25, 1.50), n)
    +    return [
    +        UnitCommitmentData(
    +            sum(pmax) * rand(Uniform(0.5, 0.75)),
    +            pmin,
    +            pmax,
    +            cfix,
    +            cvar,
    +        )
    +        for _ in 1:samples
    +    ]
    +end;
    +
    +
    +
    +

    In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.

    +

    Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple +machines. The code below generates the files uc/train/00001.jld2, uc/train/00002.jld2, etc., which contain the input data in JLD2 format.

    +
    +
    [5]:
    +
    +
    +
    data = random_uc_data(samples=500, n=500)
    +train_data = write_jld2(data[1:450], "uc/train")
    +test_data = write_jld2(data[451:500], "uc/test");
    +
    +
    +
    +

    Finally, we use BasicCollector to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files uc/train/00001.h5, uc/train/00002.h5, etc. The optimization models are also exported to compressed MPS files uc/train/00001.mps.gz, uc/train/00002.mps.gz, etc.

    +
    +
    [6]:
    +
    +
    +
    using Suppressor
    +@suppress_out begin
    +    bc = BasicCollector()
    +    bc.collect(train_data, build_uc_model)
    +end
    +
    +
    +
    +
    +
    +

    3.5. Training and solving test instances

    +

    With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using \(k\)-nearest neighbors. More specifically, the strategy is to:

    +
      +
    1. Memorize the optimal solutions of all training instances;

    2. +
    3. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;

    4. +
    5. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.

    6. +
    7. Provide this partial solution to the solver as a warm start.

    8. +
    +

    This simple strategy can be implemented as shown below, using MemorizingPrimalComponent. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide.

    +
    +
    [7]:
    +
    +
    +
    # Load kNN classifier from Scikit-Learn
    +using PyCall
    +KNeighborsClassifier = pyimport("sklearn.neighbors").KNeighborsClassifier
    +
    +# Build the MIPLearn component
    +comp = MemorizingPrimalComponent(
    +    clf=KNeighborsClassifier(n_neighbors=25),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_constr_rhs"],
    +    ),
    +    constructor=MergeTopSolutions(25, [0.0, 1.0]),
    +    action=SetWarmStart(),
    +);
    +
    +
    +
    +

    Having defined the ML strategy, we next construct LearningSolver, train the ML component and optimize one of the test instances.

    +
    +
    [8]:
    +
    +
    +
    solver_ml = LearningSolver(components=[comp])
    +solver_ml.fit(train_data)
    +solver_ml.optimize(test_data[1], build_uc_model);
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)
    +
    +CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]
    +Thread count: 16 physical cores, 32 logical processors, using up to 32 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xd2378195
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [0e+00, 0e+00]
    +  RHS range        [2e+08, 2e+08]
    +
    +User MIP start produced solution with objective 1.02165e+10 (0.00s)
    +Loaded user MIP start with objective 1.02165e+10
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 1.0216e+10    0    1 1.0217e+10 1.0216e+10  0.01%     -    0s
    +
    +Explored 1 nodes (510 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 32 (of 32 available processors)
    +
    +Solution count 1: 1.02165e+10
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 1.021651058978e+10, best bound 1.021567971257e+10, gap 0.0081%
    +
    +User-callback calls 169, time in user-callback 0.00 sec
    +
    +
    +

    By examining the solve log above, specifically the line Loaded user MIP start with objective..., we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided.

    +
    +
    [9]:
    +
    +
    +
    solver_baseline = LearningSolver(components=[])
    +solver_baseline.fit(train_data)
    +solver_baseline.optimize(test_data[1], build_uc_model);
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)
    +
    +CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]
    +Thread count: 16 physical cores, 32 logical processors, using up to 32 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0xb45c0594
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [0e+00, 0e+00]
    +  RHS range        [2e+08, 2e+08]
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Found heuristic solution: objective 1.071463e+10
    +
    +Root relaxation: objective 1.021568e+10, 510 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 1.0216e+10    0    1 1.0715e+10 1.0216e+10  4.66%     -    0s
    +H    0     0                    1.025162e+10 1.0216e+10  0.35%     -    0s
    +     0     0 1.0216e+10    0    1 1.0252e+10 1.0216e+10  0.35%     -    0s
    +H    0     0                    1.023090e+10 1.0216e+10  0.15%     -    0s
    +H    0     0                    1.022335e+10 1.0216e+10  0.07%     -    0s
    +H    0     0                    1.022281e+10 1.0216e+10  0.07%     -    0s
    +H    0     0                    1.021753e+10 1.0216e+10  0.02%     -    0s
    +H    0     0                    1.021752e+10 1.0216e+10  0.02%     -    0s
    +     0     0 1.0216e+10    0    3 1.0218e+10 1.0216e+10  0.02%     -    0s
    +     0     0 1.0216e+10    0    1 1.0218e+10 1.0216e+10  0.02%     -    0s
    +H    0     0                    1.021651e+10 1.0216e+10  0.01%     -    0s
    +
    +Explored 1 nodes (764 simplex iterations) in 0.03 seconds (0.02 work units)
    +Thread count was 32 (of 32 available processors)
    +
    +Solution count 7: 1.02165e+10 1.02175e+10 1.02228e+10 ... 1.07146e+10
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 1.021651058978e+10, best bound 1.021573363741e+10, gap 0.0076%
    +
    +User-callback calls 204, time in user-callback 0.00 sec
    +
    +
    +

    In the log above, the MIP start line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems.

    +
    +
    +

    3.6. Accessing the solution

    +

    In the example above, we used LearningSolver.solve together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a JuMP model entirely in-memory, using our trained solver.

    +
    +
    [10]:
    +
    +
    +
    data = random_uc_data(samples=1, n=500)[1]
    +model = build_uc_model(data)
    +solver_ml.optimize(model)
    +@show objective_value(model.inner);
    +
    +
    +
    +
    +
    +
    +
    +
    +Gurobi Optimizer version 10.0.1 build v10.0.1rc0 (linux64)
    +
    +CPU model: AMD Ryzen 9 7950X 16-Core Processor, instruction set [SSE2|AVX|AVX2|AVX512]
    +Thread count: 16 physical cores, 32 logical processors, using up to 32 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x974a7fba
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 1e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [0e+00, 0e+00]
    +  RHS range        [2e+08, 2e+08]
    +
    +User MIP start produced solution with objective 9.86729e+09 (0.00s)
    +User MIP start produced solution with objective 9.86675e+09 (0.00s)
    +User MIP start produced solution with objective 9.86654e+09 (0.01s)
    +User MIP start produced solution with objective 9.8661e+09 (0.01s)
    +Loaded user MIP start with objective 9.8661e+09
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 9.865344e+09, 510 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 9.8653e+09    0    1 9.8661e+09 9.8653e+09  0.01%     -    0s
    +
    +Explored 1 nodes (510 simplex iterations) in 0.02 seconds (0.01 work units)
    +Thread count was 32 (of 32 available processors)
    +
    +Solution count 4: 9.8661e+09 9.86654e+09 9.86675e+09 9.86729e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 9.866096485614e+09, best bound 9.865343669936e+09, gap 0.0076%
    +
    +User-callback calls 182, time in user-callback 0.00 sec
    +objective_value(model.inner) = 9.866096485613789e9
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/0.4/tutorials/getting-started-pyomo.ipynb b/0.4/tutorials/getting-started-pyomo.ipynb new file mode 100644 index 0000000..e109ddb --- /dev/null +++ b/0.4/tutorials/getting-started-pyomo.ipynb @@ -0,0 +1,858 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6b8983b1", + "metadata": { + "tags": [] + }, + "source": [ + "# Getting started (Pyomo)\n", + "\n", + "## Introduction\n", + "\n", + "**MIPLearn** is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:\n", + "\n", + "1. Install the Python/Pyomo version of MIPLearn\n", + "2. Model a simple optimization problem using Pyomo\n", + "3. Generate training data and train the ML models\n", + "4. Use the ML models together Gurobi to solve new instances\n", + "\n", + "
    \n", + "Note\n", + " \n", + "The Python/Pyomo version of MIPLearn is currently only compatible with Pyomo persistent solvers (Gurobi, CPLEX and XPRESS). For broader solver compatibility, see the Julia/JuMP version of the package.\n", + "
    \n", + "\n", + "
    \n", + "Warning\n", + " \n", + "MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!\n", + " \n", + "
    \n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "02f0a927", + "metadata": {}, + "source": [ + "## Installation\n", + "\n", + "MIPLearn is available in two versions:\n", + "\n", + "- Python version, compatible with the Pyomo and Gurobipy modeling languages,\n", + "- Julia version, compatible with the JuMP modeling language.\n", + "\n", + "In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Python 3.8+ in your computer. See the [official Python website for more instructions](https://www.python.org/downloads/). After Python is installed, we proceed to install MIPLearn using `pip`:\n", + "\n", + "```\n", + "$ pip install MIPLearn==0.3\n", + "```\n", + "\n", + "In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.\n", + "\n", + "```\n", + "$ pip install 'gurobipy>=10,<10.1'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a14e4550", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Note\n", + " \n", + "In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.\n", + " \n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "16b86823", + "metadata": {}, + "source": [ + "## Modeling a simple optimization problem\n", + "\n", + "To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the **unit commitment problem,** a practical optimization problem solved daily by electric grid operators around the world. \n", + "\n", + "Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns $n$ generators, denoted by $g_1, \\ldots, g_n$. Each generator can either be online or offline. An online generator $g_i$ can produce between $p^\\text{min}_i$ to $p^\\text{max}_i$ megawatts of power, and it costs the company $c^\\text{fix}_i + c^\\text{var}_i y_i$, where $y_i$ is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand $d$ (in megawatts).\n", + "\n", + "This simple problem can be modeled as a *mixed-integer linear optimization* problem as follows. For each generator $g_i$, let $x_i \\in \\{0,1\\}$ be a decision variable indicating whether $g_i$ is online, and let $y_i \\geq 0$ be a decision variable indicating how much power does $g_i$ produce. The problem is then given by:" + ] + }, + { + "cell_type": "markdown", + "id": "f12c3702", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{align}\n", + "\\text{minimize } \\quad & \\sum_{i=1}^n \\left( c^\\text{fix}_i x_i + c^\\text{var}_i y_i \\right) \\\\\n", + "\\text{subject to } \\quad & y_i \\leq p^\\text{max}_i x_i & i=1,\\ldots,n \\\\\n", + "& y_i \\geq p^\\text{min}_i x_i & i=1,\\ldots,n \\\\\n", + "& \\sum_{i=1}^n y_i = d \\\\\n", + "& x_i \\in \\{0,1\\} & i=1,\\ldots,n \\\\\n", + "& y_i \\geq 0 & i=1,\\ldots,n\n", + "\\end{align}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "id": "be3989ed", + "metadata": {}, + "source": [ + "
    \n", + "\n", + "Note\n", + "\n", + "We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.\n", + "\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "a5fd33f6", + "metadata": {}, + "source": [ + "Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class `UnitCommitmentData`, which holds all the input data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "22a67170-10b4-43d3-8708-014d91141e73", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:00:03.278853343Z", + "start_time": "2023-06-06T20:00:03.123324067Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "import numpy as np\n", + "\n", + "\n", + "@dataclass\n", + "class UnitCommitmentData:\n", + " demand: float\n", + " pmin: List[float]\n", + " pmax: List[float]\n", + " cfix: List[float]\n", + " cvar: List[float]" + ] + }, + { + "cell_type": "markdown", + "id": "29f55efa-0751-465a-9b0a-a821d46a3d40", + "metadata": {}, + "source": [ + "Next, we write a `build_uc_model` function, which converts the input data into a concrete Pyomo model. The function accepts `UnitCommitmentData`, the data structure we previously defined, or the path to a compressed pickle file containing this data." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2f67032f-0d74-4317-b45c-19da0ec859e9", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:00:45.890126754Z", + "start_time": "2023-06-06T20:00:45.637044282Z" + } + }, + "outputs": [], + "source": [ + "import pyomo.environ as pe\n", + "from typing import Union\n", + "from miplearn.io import read_pkl_gz\n", + "from miplearn.solvers.pyomo import PyomoModel\n", + "\n", + "\n", + "def build_uc_model(data: Union[str, UnitCommitmentData]) -> PyomoModel:\n", + " if isinstance(data, str):\n", + " data = read_pkl_gz(data)\n", + "\n", + " model = pe.ConcreteModel()\n", + " n = len(data.pmin)\n", + " model.x = pe.Var(range(n), domain=pe.Binary)\n", + " model.y = pe.Var(range(n), domain=pe.NonNegativeReals)\n", + " model.obj = pe.Objective(\n", + " expr=sum(\n", + " data.cfix[i] * model.x[i] + data.cvar[i] * model.y[i] for i in range(n)\n", + " )\n", + " )\n", + " model.eq_max_power = pe.ConstraintList()\n", + " model.eq_min_power = pe.ConstraintList()\n", + " for i in range(n):\n", + " model.eq_max_power.add(model.y[i] <= data.pmax[i] * model.x[i])\n", + " model.eq_min_power.add(model.y[i] >= data.pmin[i] * model.x[i])\n", + " model.eq_demand = pe.Constraint(\n", + " expr=sum(model.y[i] for i in range(n)) == data.demand,\n", + " )\n", + " return PyomoModel(model, \"gurobi_persistent\")" + ] + }, + { + "cell_type": "markdown", + "id": "c22714a3", + "metadata": {}, + "source": [ + "At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2a896f47", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:01:10.993801745Z", + "start_time": "2023-06-06T20:01:10.887580927Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Restricted license - for non-production use only - expires 2024-10-28\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 7 rows, 6 columns and 15 nonzeros\n", + "Model fingerprint: 0x15c7a953\n", + "Variable types: 3 continuous, 3 integer (3 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 7e+01]\n", + " Objective range [2e+00, 7e+02]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [1e+02, 1e+02]\n", + "Presolve removed 2 rows and 1 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 5 rows, 5 columns, 13 nonzeros\n", + "Variable types: 0 continuous, 5 integer (3 binary)\n", + "Found heuristic solution: objective 1400.0000000\n", + "\n", + "Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 1035.00000 0 1 1400.00000 1035.00000 26.1% - 0s\n", + " 0 0 1105.71429 0 1 1400.00000 1105.71429 21.0% - 0s\n", + "* 0 0 0 1320.0000000 1320.00000 0.00% - 0s\n", + "\n", + "Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 2: 1320 1400 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n", + "obj = 1320.0\n", + "x = [-0.0, 1.0, 1.0]\n", + "y = [0.0, 60.0, 40.0]\n" + ] + } + ], + "source": [ + "model = build_uc_model(\n", + " UnitCommitmentData(\n", + " demand=100.0,\n", + " pmin=[10, 20, 30],\n", + " pmax=[50, 60, 70],\n", + " cfix=[700, 600, 500],\n", + " cvar=[1.5, 2.0, 2.5],\n", + " )\n", + ")\n", + "\n", + "model.optimize()\n", + "print(\"obj =\", model.inner.obj())\n", + "print(\"x =\", [model.inner.x[i].value for i in range(3)])\n", + "print(\"y =\", [model.inner.y[i].value for i in range(3)])" + ] + }, + { + "cell_type": "markdown", + "id": "41b03bbc", + "metadata": {}, + "source": [ + "Running the code above, we found that the optimal solution for our small problem instance costs \\$1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power." + ] + }, + { + "cell_type": "markdown", + "id": "01f576e1-1790-425e-9e5c-9fa07b6f4c26", + "metadata": {}, + "source": [ + "
    \n", + " \n", + "Notes\n", + " \n", + "- In the example above, `PyomoModel` is just a thin wrapper around a standard Pyomo model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as `optimize`. For more control, and to query the solution, the original Pyomo model can be accessed through `model.inner`, as illustrated above. \n", + "- To use CPLEX or XPRESS, instead of Gurobi, replace `gurobi_persistent` by `cplex_persistent` or `xpress_persistent` in the `build_uc_model`. Note that only persistent Pyomo solvers are currently supported. Pull requests adding support for other types of solver are very welcome.\n", + "
    " + ] + }, + { + "cell_type": "markdown", + "id": "cf60c1dd", + "metadata": {}, + "source": [ + "## Generating training data\n", + "\n", + "Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a **trained** solver, which can optimize new instances (similar to the ones it was trained on) faster.\n", + "\n", + "In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a random instance generator:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5eb09fab", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:02:27.324208900Z", + "start_time": "2023-06-06T20:02:26.990044230Z" + } + }, + "outputs": [], + "source": [ + "from scipy.stats import uniform\n", + "from typing import List\n", + "import random\n", + "\n", + "\n", + "def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:\n", + " random.seed(seed)\n", + " np.random.seed(seed)\n", + " pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)\n", + " pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)\n", + " cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)\n", + " cvar = uniform(loc=1.25, scale=0.25).rvs(n)\n", + " return [\n", + " UnitCommitmentData(\n", + " demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),\n", + " pmin=pmin,\n", + " pmax=pmax,\n", + " cfix=cfix,\n", + " cvar=cvar,\n", + " )\n", + " for _ in range(samples)\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "3a03a7ac", + "metadata": {}, + "source": [ + "In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.\n", + "\n", + "Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple machines. The code below generates the files `uc/train/00000.pkl.gz`, `uc/train/00001.pkl.gz`, etc., which contain the input data in compressed (gzipped) pickle format." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6156752c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:04.782830561Z", + "start_time": "2023-06-06T20:03:04.530421396Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.io import write_pkl_gz\n", + "\n", + "data = random_uc_data(samples=500, n=500)\n", + "train_data = write_pkl_gz(data[0:450], \"uc/train\")\n", + "test_data = write_pkl_gz(data[450:500], \"uc/test\")" + ] + }, + { + "cell_type": "markdown", + "id": "b17af877", + "metadata": {}, + "source": [ + "Finally, we use `BasicCollector` to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files `uc/train/00000.h5`, `uc/train/00001.h5`, etc. The optimization models are also exported to compressed MPS files `uc/train/00000.mps.gz`, `uc/train/00001.mps.gz`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7623f002", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:03:35.571497019Z", + "start_time": "2023-06-06T20:03:25.804104036Z" + } + }, + "outputs": [], + "source": [ + "from miplearn.collectors.basic import BasicCollector\n", + "\n", + "bc = BasicCollector()\n", + "bc.collect(train_data, build_uc_model, n_jobs=4)" + ] + }, + { + "cell_type": "markdown", + "id": "c42b1be1-9723-4827-82d8-974afa51ef9f", + "metadata": {}, + "source": [ + "## Training and solving test instances" + ] + }, + { + "cell_type": "markdown", + "id": "a33c6aa4-f0b8-4ccb-9935-01f7d7de2a1c", + "metadata": {}, + "source": [ + "With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using $k$-nearest neighbors. More specifically, the strategy is to:\n", + "\n", + "1. Memorize the optimal solutions of all training instances;\n", + "2. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;\n", + "3. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.\n", + "4. Provide this partial solution to the solver as a warm start.\n", + "\n", + "This simple strategy can be implemented as shown below, using `MemorizingPrimalComponent`. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "435f7bf8-4b09-4889-b1ec-b7b56e7d8ed2", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:20.497772794Z", + "start_time": "2023-06-06T20:05:20.484821405Z" + } + }, + "outputs": [], + "source": [ + "from sklearn.neighbors import KNeighborsClassifier\n", + "from miplearn.components.primal.actions import SetWarmStart\n", + "from miplearn.components.primal.mem import (\n", + " MemorizingPrimalComponent,\n", + " MergeTopSolutions,\n", + ")\n", + "from miplearn.extractors.fields import H5FieldsExtractor\n", + "\n", + "comp = MemorizingPrimalComponent(\n", + " clf=KNeighborsClassifier(n_neighbors=25),\n", + " extractor=H5FieldsExtractor(\n", + " instance_fields=[\"static_constr_rhs\"],\n", + " ),\n", + " constructor=MergeTopSolutions(25, [0.0, 1.0]),\n", + " action=SetWarmStart(),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9536e7e4-0b0d-49b0-bebd-4a848f839e94", + "metadata": {}, + "source": [ + "Having defined the ML strategy, we next construct `LearningSolver`, train the ML component and optimize one of the test instances." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9d13dd50-3dcf-4673-a757-6f44dcc0dedf", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:22.672002339Z", + "start_time": "2023-06-06T20:05:21.447466634Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x5e67c6ee\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x4a7cfe2b\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.29153e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.29153e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 8.2915e+09 8.2906e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 3 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2915e+09 8.2907e+09 0.01% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2915e+09 8.2907e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 1\n", + " Flow cover: 2\n", + "\n", + "Explored 1 nodes (565 simplex iterations) in 0.04 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 1: 8.29153e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from miplearn.solvers.learning import LearningSolver\n", + "\n", + "solver_ml = LearningSolver(components=[comp])\n", + "solver_ml.fit(train_data)\n", + "solver_ml.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "61da6dad-7f56-4edb-aa26-c00eb5f946c0", + "metadata": {}, + "source": [ + "By examining the solve log above, specifically the line `Loaded user MIP start with objective...`, we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ff391ed-e855-4228-aa09-a7641d8c2893", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:05:46.969575966Z", + "start_time": "2023-06-06T20:05:46.420803286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x5e67c6ee\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.6166537e+09 5.648803e+04 0.000000e+00 0s\n", + " 1 8.2906219e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.290621916e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x8a0f9587\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Found heuristic solution: objective 9.757128e+09\n", + "\n", + "Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2906e+09 0 1 9.7571e+09 8.2906e+09 15.0% - 0s\n", + "H 0 0 8.298273e+09 8.2906e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2983e+09 8.2907e+09 0.09% - 0s\n", + " 0 0 8.2907e+09 0 4 8.2983e+09 8.2907e+09 0.09% - 0s\n", + "H 0 0 8.293980e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 5 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 1 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2907e+09 0 2 8.2940e+09 8.2907e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 1 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + " 0 0 8.2908e+09 0 4 8.2940e+09 8.2908e+09 0.04% - 0s\n", + "H 0 0 8.291465e+09 8.2908e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Gomory: 2\n", + " MIR: 1\n", + "\n", + "Explored 1 nodes (1025 simplex iterations) in 0.12 seconds (0.03 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n" + ] + }, + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver_baseline = LearningSolver(components=[])\n", + "solver_baseline.fit(train_data)\n", + "solver_baseline.optimize(test_data[0], build_uc_model)" + ] + }, + { + "cell_type": "markdown", + "id": "b6d37b88-9fcc-43ee-ac1e-2a7b1e51a266", + "metadata": {}, + "source": [ + "In the log above, the `MIP start` line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems." + ] + }, + { + "cell_type": "markdown", + "id": "eec97f06", + "metadata": { + "tags": [] + }, + "source": [ + "## Accessing the solution\n", + "\n", + "In the example above, we used `LearningSolver.solve` together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "67a6cd18", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-06T20:06:26.913448568Z", + "start_time": "2023-06-06T20:06:26.169047914Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x2dfe4e1c\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "Presolve removed 1000 rows and 500 columns\n", + "Presolve time: 0.00s\n", + "Presolved: 1 rows, 500 columns, 500 nonzeros\n", + "\n", + "Iteration Objective Primal Inf. Dual Inf. Time\n", + " 0 6.5917580e+09 5.627453e+04 0.000000e+00 0s\n", + " 1 8.2535968e+09 0.000000e+00 0.000000e+00 0s\n", + "\n", + "Solved in 1 iterations and 0.01 seconds (0.00 work units)\n", + "Optimal objective 8.253596777e+09\n", + "Set parameter QCPDual to value 1\n", + "Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)\n", + "\n", + "CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]\n", + "Thread count: 10 physical cores, 20 logical processors, using up to 20 threads\n", + "\n", + "Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros\n", + "Model fingerprint: 0x0f0924a1\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "Coefficient statistics:\n", + " Matrix range [1e+00, 2e+06]\n", + " Objective range [1e+00, 6e+07]\n", + " Bounds range [1e+00, 1e+00]\n", + " RHS range [3e+08, 3e+08]\n", + "\n", + "User MIP start produced solution with objective 8.25814e+09 (0.00s)\n", + "User MIP start produced solution with objective 8.25512e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25483e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "User MIP start produced solution with objective 8.25459e+09 (0.01s)\n", + "Loaded user MIP start with objective 8.25459e+09\n", + "\n", + "Presolve time: 0.00s\n", + "Presolved: 1001 rows, 1000 columns, 2500 nonzeros\n", + "Variable types: 500 continuous, 500 integer (500 binary)\n", + "\n", + "Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)\n", + "\n", + " Nodes | Current Node | Objective Bounds | Work\n", + " Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time\n", + "\n", + " 0 0 8.2536e+09 0 1 8.2546e+09 8.2536e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 3 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 1 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2537e+09 0 4 8.2546e+09 8.2537e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 4 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 5 8.2546e+09 8.2538e+09 0.01% - 0s\n", + " 0 0 8.2538e+09 0 6 8.2546e+09 8.2538e+09 0.01% - 0s\n", + "\n", + "Cutting planes:\n", + " Cover: 1\n", + " MIR: 2\n", + " StrongCG: 1\n", + " Flow cover: 1\n", + "\n", + "Explored 1 nodes (575 simplex iterations) in 0.09 seconds (0.01 work units)\n", + "Thread count was 20 (of 20 available processors)\n", + "\n", + "Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09 \n", + "\n", + "Optimal solution found (tolerance 1.00e-04)\n", + "Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%\n", + "WARNING: Cannot get reduced costs for MIP.\n", + "WARNING: Cannot get duals for MIP.\n", + "obj = 8254590409.96973\n", + " x = [1.0, 1.0, 0.0, 1.0, 1.0]\n", + " y = [935662.0949262811, 1604270.0218116897, 0.0, 1369560.835229226, 602828.5321028307]\n" + ] + } + ], + "source": [ + "data = random_uc_data(samples=1, n=500)[0]\n", + "model = build_uc_model(data)\n", + "solver_ml.optimize(model)\n", + "print(\"obj =\", model.inner.obj())\n", + "print(\" x =\", [model.inner.x[i].value for i in range(5)])\n", + "print(\" y =\", [model.inner.y[i].value for i in range(5)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5593d23a-83bd-4e16-8253-6300f5e3f63b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/0.4/tutorials/getting-started-pyomo/index.html b/0.4/tutorials/getting-started-pyomo/index.html new file mode 100644 index 0000000..da776df --- /dev/null +++ b/0.4/tutorials/getting-started-pyomo/index.html @@ -0,0 +1,909 @@ + + + + + + + + 1. Getting started (Pyomo) — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + +
    + + +
    +
    + +
    + +
    +

    1. Getting started (Pyomo)

    +
    +

    1.1. Introduction

    +

    MIPLearn is an open source framework that uses machine learning (ML) to accelerate the performance of mixed-integer programming solvers (e.g. Gurobi, CPLEX, XPRESS). In this tutorial, we will:

    +
      +
    1. Install the Python/Pyomo version of MIPLearn

    2. +
    3. Model a simple optimization problem using Pyomo

    4. +
    5. Generate training data and train the ML models

    6. +
    7. Use the ML models together Gurobi to solve new instances

    8. +
    +
    +

    Note

    +

    The Python/Pyomo version of MIPLearn is currently only compatible with Pyomo persistent solvers (Gurobi, CPLEX and XPRESS). For broader solver compatibility, see the Julia/JuMP version of the package.

    +
    +
    +

    Warning

    +

    MIPLearn is still in early development stage. If run into any bugs or issues, please submit a bug report in our GitHub repository. Comments, suggestions and pull requests are also very welcome!

    +
    +
    +
    +

    1.2. Installation

    +

    MIPLearn is available in two versions:

    +
      +
    • Python version, compatible with the Pyomo and Gurobipy modeling languages,

    • +
    • Julia version, compatible with the JuMP modeling language.

    • +
    +

    In this tutorial, we will demonstrate how to use and install the Python/Pyomo version of the package. The first step is to install Python 3.8+ in your computer. See the official Python website for more instructions. After Python is installed, we proceed to install MIPLearn using pip:

    +
    $ pip install MIPLearn==0.3
    +
    +
    +

    In addition to MIPLearn itself, we will also install Gurobi 10.0, a state-of-the-art commercial MILP solver. This step also install a demo license for Gurobi, which should able to solve the small optimization problems in this tutorial. A license is required for solving larger-scale problems.

    +
    $ pip install 'gurobipy>=10,<10.1'
    +
    +
    +
    +

    Note

    +

    In the code above, we install specific version of all packages to ensure that this tutorial keeps running in the future, even when newer (and possibly incompatible) versions of the packages are released. This is usually a recommended practice for all Python projects.

    +
    +
    +
    +

    1.3. Modeling a simple optimization problem

    +

    To illustrate how can MIPLearn be used, we will model and solve a small optimization problem related to power systems optimization. The problem we discuss below is a simplification of the unit commitment problem, a practical optimization problem solved daily by electric grid operators around the world.

    +

    Suppose that a utility company needs to decide which electrical generators should be online at each hour of the day, as well as how much power should each generator produce. More specifically, assume that the company owns \(n\) generators, denoted by \(g_1, \ldots, g_n\). Each generator can either be online or offline. An online generator \(g_i\) can produce between \(p^\text{min}_i\) to \(p^\text{max}_i\) megawatts of power, and it costs the company +\(c^\text{fix}_i + c^\text{var}_i y_i\), where \(y_i\) is the amount of power produced. An offline generator produces nothing and costs nothing. The total amount of power to be produced needs to be exactly equal to the total demand \(d\) (in megawatts).

    +

    This simple problem can be modeled as a mixed-integer linear optimization problem as follows. For each generator \(g_i\), let \(x_i \in \{0,1\}\) be a decision variable indicating whether \(g_i\) is online, and let \(y_i \geq 0\) be a decision variable indicating how much power does \(g_i\) produce. The problem is then given by:

    +
    +\[\begin{split}\begin{align} +\text{minimize } \quad & \sum_{i=1}^n \left( c^\text{fix}_i x_i + c^\text{var}_i y_i \right) \\ +\text{subject to } \quad & y_i \leq p^\text{max}_i x_i & i=1,\ldots,n \\ +& y_i \geq p^\text{min}_i x_i & i=1,\ldots,n \\ +& \sum_{i=1}^n y_i = d \\ +& x_i \in \{0,1\} & i=1,\ldots,n \\ +& y_i \geq 0 & i=1,\ldots,n +\end{align}\end{split}\]
    +
    +

    Note

    +

    We use a simplified version of the unit commitment problem in this tutorial just to make it easier to follow. MIPLearn can also handle realistic, large-scale versions of this problem.

    +
    +

    Next, let us convert this abstract mathematical formulation into a concrete optimization model, using Python and Pyomo. We start by defining a data class UnitCommitmentData, which holds all the input data.

    +
    +
    [1]:
    +
    +
    +
    from dataclasses import dataclass
    +from typing import List
    +
    +import numpy as np
    +
    +
    +@dataclass
    +class UnitCommitmentData:
    +    demand: float
    +    pmin: List[float]
    +    pmax: List[float]
    +    cfix: List[float]
    +    cvar: List[float]
    +
    +
    +
    +

    Next, we write a build_uc_model function, which converts the input data into a concrete Pyomo model. The function accepts UnitCommitmentData, the data structure we previously defined, or the path to a compressed pickle file containing this data.

    +
    +
    [2]:
    +
    +
    +
    import pyomo.environ as pe
    +from typing import Union
    +from miplearn.io import read_pkl_gz
    +from miplearn.solvers.pyomo import PyomoModel
    +
    +
    +def build_uc_model(data: Union[str, UnitCommitmentData]) -> PyomoModel:
    +    if isinstance(data, str):
    +        data = read_pkl_gz(data)
    +
    +    model = pe.ConcreteModel()
    +    n = len(data.pmin)
    +    model.x = pe.Var(range(n), domain=pe.Binary)
    +    model.y = pe.Var(range(n), domain=pe.NonNegativeReals)
    +    model.obj = pe.Objective(
    +        expr=sum(
    +            data.cfix[i] * model.x[i] + data.cvar[i] * model.y[i] for i in range(n)
    +        )
    +    )
    +    model.eq_max_power = pe.ConstraintList()
    +    model.eq_min_power = pe.ConstraintList()
    +    for i in range(n):
    +        model.eq_max_power.add(model.y[i] <= data.pmax[i] * model.x[i])
    +        model.eq_min_power.add(model.y[i] >= data.pmin[i] * model.x[i])
    +    model.eq_demand = pe.Constraint(
    +        expr=sum(model.y[i] for i in range(n)) == data.demand,
    +    )
    +    return PyomoModel(model, "gurobi_persistent")
    +
    +
    +
    +

    At this point, we can already use Pyomo and any mixed-integer linear programming solver to find optimal solutions to any instance of this problem. To illustrate this, let us solve a small instance with three generators:

    +
    +
    [3]:
    +
    +
    +
    model = build_uc_model(
    +    UnitCommitmentData(
    +        demand=100.0,
    +        pmin=[10, 20, 30],
    +        pmax=[50, 60, 70],
    +        cfix=[700, 600, 500],
    +        cvar=[1.5, 2.0, 2.5],
    +    )
    +)
    +
    +model.optimize()
    +print("obj =", model.inner.obj())
    +print("x =", [model.inner.x[i].value for i in range(3)])
    +print("y =", [model.inner.y[i].value for i in range(3)])
    +
    +
    +
    +
    +
    +
    +
    +
    +Restricted license - for non-production use only - expires 2024-10-28
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 7 rows, 6 columns and 15 nonzeros
    +Model fingerprint: 0x15c7a953
    +Variable types: 3 continuous, 3 integer (3 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 7e+01]
    +  Objective range  [2e+00, 7e+02]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [1e+02, 1e+02]
    +Presolve removed 2 rows and 1 columns
    +Presolve time: 0.00s
    +Presolved: 5 rows, 5 columns, 13 nonzeros
    +Variable types: 0 continuous, 5 integer (3 binary)
    +Found heuristic solution: objective 1400.0000000
    +
    +Root relaxation: objective 1.035000e+03, 3 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 1035.00000    0    1 1400.00000 1035.00000  26.1%     -    0s
    +     0     0 1105.71429    0    1 1400.00000 1105.71429  21.0%     -    0s
    +*    0     0               0    1320.0000000 1320.00000  0.00%     -    0s
    +
    +Explored 1 nodes (5 simplex iterations) in 0.01 seconds (0.00 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 2: 1320 1400
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 1.320000000000e+03, best bound 1.320000000000e+03, gap 0.0000%
    +WARNING: Cannot get reduced costs for MIP.
    +WARNING: Cannot get duals for MIP.
    +obj = 1320.0
    +x = [-0.0, 1.0, 1.0]
    +y = [0.0, 60.0, 40.0]
    +
    +
    +

    Running the code above, we found that the optimal solution for our small problem instance costs $1320. It is achieve by keeping generators 2 and 3 online and producing, respectively, 60 MW and 40 MW of power.

    +
    +

    Notes

    +
      +
    • In the example above, PyomoModel is just a thin wrapper around a standard Pyomo model. This wrapper allows MIPLearn to be solver- and modeling-language-agnostic. The wrapper provides only a few basic methods, such as optimize. For more control, and to query the solution, the original Pyomo model can be accessed through model.inner, as illustrated above.

    • +
    • To use CPLEX or XPRESS, instead of Gurobi, replace gurobi_persistent by cplex_persistent or xpress_persistent in the build_uc_model. Note that only persistent Pyomo solvers are currently supported. Pull requests adding support for other types of solver are very welcome.

    • +
    +
    +
    +
    +

    1.4. Generating training data

    +

    Although Gurobi could solve the small example above in a fraction of a second, it gets slower for larger and more complex versions of the problem. If this is a problem that needs to be solved frequently, as it is often the case in practice, it could make sense to spend some time upfront generating a trained solver, which can optimize new instances (similar to the ones it was trained on) faster.

    +

    In the following, we will use MIPLearn to train machine learning models that is able to predict the optimal solution for instances that follow a given probability distribution, then it will provide this predicted solution to Gurobi as a warm start. Before we can train the model, we need to collect training data by solving a large number of instances. In real-world situations, we may construct these training instances based on historical data. In this tutorial, we will construct them using a +random instance generator:

    +
    +
    [4]:
    +
    +
    +
    from scipy.stats import uniform
    +from typing import List
    +import random
    +
    +
    +def random_uc_data(samples: int, n: int, seed: int = 42) -> List[UnitCommitmentData]:
    +    random.seed(seed)
    +    np.random.seed(seed)
    +    pmin = uniform(loc=100_000.0, scale=400_000.0).rvs(n)
    +    pmax = pmin * uniform(loc=2.0, scale=2.5).rvs(n)
    +    cfix = pmin * uniform(loc=100.0, scale=25.0).rvs(n)
    +    cvar = uniform(loc=1.25, scale=0.25).rvs(n)
    +    return [
    +        UnitCommitmentData(
    +            demand=pmax.sum() * uniform(loc=0.5, scale=0.25).rvs(),
    +            pmin=pmin,
    +            pmax=pmax,
    +            cfix=cfix,
    +            cvar=cvar,
    +        )
    +        for _ in range(samples)
    +    ]
    +
    +
    +
    +

    In this example, for simplicity, only the demands change from one instance to the next. We could also have randomized the costs, production limits or even the number of units. The more randomization we have in the training data, however, the more challenging it is for the machine learning models to learn solution patterns.

    +

    Now we generate 500 instances of this problem, each one with 50 generators, and we use 450 of these instances for training. After generating the instances, we write them to individual files. MIPLearn uses files during the training process because, for large-scale optimization problems, it is often impractical to hold in memory the entire training data, as well as the concrete Pyomo models. Files also make it much easier to solve multiple instances simultaneously, potentially on multiple +machines. The code below generates the files uc/train/00000.pkl.gz, uc/train/00001.pkl.gz, etc., which contain the input data in compressed (gzipped) pickle format.

    +
    +
    [5]:
    +
    +
    +
    from miplearn.io import write_pkl_gz
    +
    +data = random_uc_data(samples=500, n=500)
    +train_data = write_pkl_gz(data[0:450], "uc/train")
    +test_data = write_pkl_gz(data[450:500], "uc/test")
    +
    +
    +
    +

    Finally, we use BasicCollector to collect the optimal solutions and other useful training data for all training instances. The data is stored in HDF5 files uc/train/00000.h5, uc/train/00001.h5, etc. The optimization models are also exported to compressed MPS files uc/train/00000.mps.gz, uc/train/00001.mps.gz, etc.

    +
    +
    [6]:
    +
    +
    +
    from miplearn.collectors.basic import BasicCollector
    +
    +bc = BasicCollector()
    +bc.collect(train_data, build_uc_model, n_jobs=4)
    +
    +
    +
    +
    +
    +

    1.5. Training and solving test instances

    +

    With training data in hand, we can now design and train a machine learning model to accelerate solver performance. In this tutorial, for illustration purposes, we will use ML to generate a good warm start using \(k\)-nearest neighbors. More specifically, the strategy is to:

    +
      +
    1. Memorize the optimal solutions of all training instances;

    2. +
    3. Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;

    4. +
    5. Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.

    6. +
    7. Provide this partial solution to the solver as a warm start.

    8. +
    +

    This simple strategy can be implemented as shown below, using MemorizingPrimalComponent. For more advanced strategies, and for the usage of more advanced classifiers, see the user guide.

    +
    +
    [7]:
    +
    +
    +
    from sklearn.neighbors import KNeighborsClassifier
    +from miplearn.components.primal.actions import SetWarmStart
    +from miplearn.components.primal.mem import (
    +    MemorizingPrimalComponent,
    +    MergeTopSolutions,
    +)
    +from miplearn.extractors.fields import H5FieldsExtractor
    +
    +comp = MemorizingPrimalComponent(
    +    clf=KNeighborsClassifier(n_neighbors=25),
    +    extractor=H5FieldsExtractor(
    +        instance_fields=["static_constr_rhs"],
    +    ),
    +    constructor=MergeTopSolutions(25, [0.0, 1.0]),
    +    action=SetWarmStart(),
    +)
    +
    +
    +
    +

    Having defined the ML strategy, we next construct LearningSolver, train the ML component and optimize one of the test instances.

    +
    +
    [8]:
    +
    +
    +
    from miplearn.solvers.learning import LearningSolver
    +
    +solver_ml = LearningSolver(components=[comp])
    +solver_ml.fit(train_data)
    +solver_ml.optimize(test_data[0], build_uc_model)
    +
    +
    +
    +
    +
    +
    +
    +
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x5e67c6ee
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.00s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
    +       1    8.2906219e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.290621916e+09
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x4a7cfe2b
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +
    +User MIP start produced solution with objective 8.29153e+09 (0.01s)
    +User MIP start produced solution with objective 8.29153e+09 (0.01s)
    +Loaded user MIP start with objective 8.29153e+09
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2906e+09    0    1 8.2915e+09 8.2906e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    3 8.2915e+09 8.2907e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    1 8.2915e+09 8.2907e+09  0.01%     -    0s
    +     0     0 8.2907e+09    0    2 8.2915e+09 8.2907e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Gomory: 1
    +  Flow cover: 2
    +
    +Explored 1 nodes (565 simplex iterations) in 0.04 seconds (0.01 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 1: 8.29153e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.291528276179e+09, best bound 8.290733258025e+09, gap 0.0096%
    +WARNING: Cannot get reduced costs for MIP.
    +WARNING: Cannot get duals for MIP.
    +
    +
    +
    +
    [8]:
    +
    +
    +
    +
    +{}
    +
    +
    +

    By examining the solve log above, specifically the line Loaded user MIP start with objective..., we can see that MIPLearn was able to construct an initial solution which turned out to be very close to the optimal solution to the problem. Now let us repeat the code above, but a solver which does not apply any ML strategies. Note that our previously-defined component is not provided.

    +
    +
    [9]:
    +
    +
    +
    solver_baseline = LearningSolver(components=[])
    +solver_baseline.fit(train_data)
    +solver_baseline.optimize(test_data[0], build_uc_model)
    +
    +
    +
    +
    +
    +
    +
    +
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x5e67c6ee
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.00s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.6166537e+09   5.648803e+04   0.000000e+00      0s
    +       1    8.2906219e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.290621916e+09
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x8a0f9587
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Found heuristic solution: objective 9.757128e+09
    +
    +Root relaxation: objective 8.290622e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2906e+09    0    1 9.7571e+09 8.2906e+09  15.0%     -    0s
    +H    0     0                    8.298273e+09 8.2906e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    1 8.2983e+09 8.2907e+09  0.09%     -    0s
    +     0     0 8.2907e+09    0    4 8.2983e+09 8.2907e+09  0.09%     -    0s
    +H    0     0                    8.293980e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    5 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    1 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2907e+09    0    2 8.2940e+09 8.2907e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    1 8.2940e+09 8.2908e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
    +     0     0 8.2908e+09    0    4 8.2940e+09 8.2908e+09  0.04%     -    0s
    +H    0     0                    8.291465e+09 8.2908e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Gomory: 2
    +  MIR: 1
    +
    +Explored 1 nodes (1025 simplex iterations) in 0.12 seconds (0.03 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 4: 8.29147e+09 8.29398e+09 8.29827e+09 9.75713e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.291465302389e+09, best bound 8.290781665333e+09, gap 0.0082%
    +WARNING: Cannot get reduced costs for MIP.
    +WARNING: Cannot get duals for MIP.
    +
    +
    +
    +
    [9]:
    +
    +
    +
    +
    +{}
    +
    +
    +

    In the log above, the MIP start line is missing, and Gurobi had to start with a significantly inferior initial solution. The solver was still able to find the optimal solution at the end, but it required using its own internal heuristic procedures. In this example, because we solve very small optimization problems, there was almost no difference in terms of running time, but the difference can be significant for larger problems.

    +
    +
    +

    1.6. Accessing the solution

    +

    In the example above, we used LearningSolver.solve together with data files to solve both the training and the test instances. In the following example, we show how to build and solve a Pyomo model entirely in-memory, using our trained solver.

    +
    +
    [10]:
    +
    +
    +
    data = random_uc_data(samples=1, n=500)[0]
    +model = build_uc_model(data)
    +solver_ml.optimize(model)
    +print("obj =", model.inner.obj())
    +print(" x =", [model.inner.x[i].value for i in range(5)])
    +print(" y =", [model.inner.y[i].value for i in range(5)])
    +
    +
    +
    +
    +
    +
    +
    +
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x2dfe4e1c
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +Presolve removed 1000 rows and 500 columns
    +Presolve time: 0.00s
    +Presolved: 1 rows, 500 columns, 500 nonzeros
    +
    +Iteration    Objective       Primal Inf.    Dual Inf.      Time
    +       0    6.5917580e+09   5.627453e+04   0.000000e+00      0s
    +       1    8.2535968e+09   0.000000e+00   0.000000e+00      0s
    +
    +Solved in 1 iterations and 0.01 seconds (0.00 work units)
    +Optimal objective  8.253596777e+09
    +Set parameter QCPDual to value 1
    +Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (linux64)
    +
    +CPU model: 13th Gen Intel(R) Core(TM) i7-13800H, instruction set [SSE2|AVX|AVX2]
    +Thread count: 10 physical cores, 20 logical processors, using up to 20 threads
    +
    +Optimize a model with 1001 rows, 1000 columns and 2500 nonzeros
    +Model fingerprint: 0x0f0924a1
    +Variable types: 500 continuous, 500 integer (500 binary)
    +Coefficient statistics:
    +  Matrix range     [1e+00, 2e+06]
    +  Objective range  [1e+00, 6e+07]
    +  Bounds range     [1e+00, 1e+00]
    +  RHS range        [3e+08, 3e+08]
    +
    +User MIP start produced solution with objective 8.25814e+09 (0.00s)
    +User MIP start produced solution with objective 8.25512e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25483e+09 (0.01s)
    +User MIP start produced solution with objective 8.25459e+09 (0.01s)
    +User MIP start produced solution with objective 8.25459e+09 (0.01s)
    +Loaded user MIP start with objective 8.25459e+09
    +
    +Presolve time: 0.00s
    +Presolved: 1001 rows, 1000 columns, 2500 nonzeros
    +Variable types: 500 continuous, 500 integer (500 binary)
    +
    +Root relaxation: objective 8.253597e+09, 512 iterations, 0.00 seconds (0.00 work units)
    +
    +    Nodes    |    Current Node    |     Objective Bounds      |     Work
    + Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    +
    +     0     0 8.2536e+09    0    1 8.2546e+09 8.2536e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    3 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    1 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2537e+09    0    4 8.2546e+09 8.2537e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    4 8.2546e+09 8.2538e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    5 8.2546e+09 8.2538e+09  0.01%     -    0s
    +     0     0 8.2538e+09    0    6 8.2546e+09 8.2538e+09  0.01%     -    0s
    +
    +Cutting planes:
    +  Cover: 1
    +  MIR: 2
    +  StrongCG: 1
    +  Flow cover: 1
    +
    +Explored 1 nodes (575 simplex iterations) in 0.09 seconds (0.01 work units)
    +Thread count was 20 (of 20 available processors)
    +
    +Solution count 4: 8.25459e+09 8.25483e+09 8.25512e+09 8.25814e+09
    +
    +Optimal solution found (tolerance 1.00e-04)
    +Best objective 8.254590409970e+09, best bound 8.253768093811e+09, gap 0.0100%
    +WARNING: Cannot get reduced costs for MIP.
    +WARNING: Cannot get duals for MIP.
    +obj = 8254590409.96973
    + x = [1.0, 1.0, 0.0, 1.0, 1.0]
    + y = [935662.0949262811, 1604270.0218116897, 0.0, 1369560.835229226, 602828.5321028307]
    +
    +
    +
    +
    [ ]:
    +
    +
    +
    
    +
    +
    +
    +
    +
    + + +
    + + + + +
    +
    +
    +
    +

    + + © Copyright 2020-2023, UChicago Argonne, LLC.
    +

    +
    +
    +
    + + +
    +
    + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index 60f3e8e..3270ba9 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ - +

    wQH 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 literal 0 HcmV?d00001 diff --git a/0.4/_static/webpack-macros.html b/0.4/_static/webpack-macros.html new file mode 100644 index 0000000..144f188 --- /dev/null +++ b/0.4/_static/webpack-macros.html @@ -0,0 +1,25 @@ + +{% macro head_pre_icons() %} + + + +{% endmacro %} + +{% macro head_pre_fonts() %} +{% endmacro %} + +{% macro head_pre_bootstrap() %} + + +{% endmacro %} + +{% macro head_js_preload() %} + +{% endmacro %} + +{% macro body_post() %} + +{% endmacro %} \ No newline at end of file diff --git a/0.4/api/collectors/index.html b/0.4/api/collectors/index.html new file mode 100644 index 0000000..6d91abd --- /dev/null +++ b/0.4/api/collectors/index.html @@ -0,0 +1,771 @@ + + + + + + + + 11. Collectors & Extractors — MIPLearn 0.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

w`Ao`B8FfP>nd2WTiT*&L> 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�d261qc>pomd%U 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 literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.eot b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..38cf2517a4da71a05f8e83723cdffce45ebc3319 GIT binary patch literal 34390 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 HJNN#7)2yBs literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.svg b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.svg new file mode 100644 index 0000000..48634a9 --- /dev/null +++ b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.ttf b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..abe99e20c38a23e95295917d6a5cdb49a8489eb8 GIT binary patch literal 34092 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 bMX1cpc?scf@6G-KdBTf-{AinZ?ft(1jXHOb literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff new file mode 100644 index 0000000000000000000000000000000000000000..24de566a5c97289a86ce0c238a195da0d3251263 GIT binary patch literal 16800 zcmZ5{V~{9K%Y465lccMrGSxMmiJP35 z7yux^uL$M?!2UOe&;UvU!TdM-|3yqp`40d9Aj2<@{R^DfaxMjN5mB*UPUcs~`yWsN z|y066~s+A#lu4Fgm4(EOJK0N}*<)$xBp0>TQkU~c2= z{>yRv>a_p>K*~t<0M6ESM*rEJ{jSUQAI<=$#B2=Qf7j(E_&>e`02cstwgxsPzns9Y z9})lns22A+9iF|NlQRGSuQC7th$H|2c-5zcV3&Kgk-omZF+gC}MkT-CV~vLnmb@lD zfK)tK-*5Qei2pq~v|j-IYQO*)zaz;1Xup^Ld=0#(I06Dv0sb-df-1mO~!FWfRVF(2b#Nu&^P>&P|O8WaD0K8b?*xa*M z7H6cB9T33p_V{d}=9-v|5PA+Mc_ic!bhum?&S9g^c3S&|&np)?BExBb z(G8~PEHDlP36-hlbxCL_yo;jkO>@5_@iwmottP&JBf(5V>8B5zFpOkkGcJLUp-LTg zK`2?WhGfm^kxOU)W~OxeKe$8I?F;G7Ty_Lcy;7+CbKsk3(H?v94ybcLW_P0IKjQly z*4sF}%}xwy)P^#yNgfy5pxQDH!-*eKq*p8X^L|{?ourVFY>}a$)@EKI_LQGHpRT(+ z^{$?GrgXiC=Tn0Jn05Oq>bo5G5j62>tmU|V(~!buO24tiX1irmcHQ7}-pq(I+jujh zE;rNE3bMNEv0CD%E^X2!avYzXHrl_LW3q#y?gqMQq{(b`{c3W(8W8QGm)bI#+rVxe z<@2~DWg)eI60?NSQ-;1Qjq4njc@D?A@TXH2);VNWfp(-k^H~WKb%{$;xT-uHO^FY2 zb|3vS`iLWvm?OHb0T+4Qzl0RFxUK!}r49s!1gK#TfdDSS7B)o2_19?~ zht0v?8U~uzYp?3!<{~#YLGo{IrDvUPPv=YT$Ib1I+R5uop3OIG+`?EpL)sBXvIs}G zy(!k80?#5_?x^t!wJ6YX8d<`?8UthY`87vIue%pv5i0dl`=5!CQZT|sU9l#oC<$AVow9J<{p^O zqO_x@>R-SUjE*ea)ePXN&@Q}QJw4LxWR^S^?O)?du-#xGC?9Zd8I%V5bwX`<8`Llr zo)eXBHMG6wC{GK*;z!VHfl@4=o~2aRQ)F(C8dLE3>O0^2FEUa#Hc~d6#;_cy6Y39Rj0)z(c(fox zs5V|hR5t$xH8n$$+uxr+pP4*j#@Y+orXLO7VZqcv4_TdE788Y7!li`y^D&6vzn&b$ z`91uL^ZES#;P>SpT$A*a+N+Ca5X|$)-nICC`+8K>R~YWAxxA%DWUU42;KaP^9dq_Jv?SV4aYb`2`@|ywqRKS##51f?Zvy3TC1ol^u_xl*5zkDL`ug4F2_D+*bEiNA7vML?kS`9gp2w-{MAc* zM2lh6|0KN)!7lv|I5ZTI?8ZK^JOt3SzeT=kHHXBpx?J?EUfi+T#oBty$YPqI{rFM_nkU4hHKAZq)3iC+T4qF1n8J}R}tKcfA-!5RERCkVQ%k|r}s z$*iigKLRmsb&9&BIR!3kr5d)Drw!II(Mnd!VTfN*&)9`Jm}3(h_u({gJOoqZ3tTMi z!&t@p$LI_)s8CB&a5gV{QZ=sB+1zcfJq%^Y3{9mSVAHf|kGTGeCX>TQ<1%ZNux^dS z>1X4u5{%(k35I|tUPZCF5)qbJkb{X7rS1pf=GVeAdAs_g?x=`OD0u9*TVFgIA3vie zOx<4T*|HHtzTG3NR6GmY?P{-{4`oz&^|t=x(+*Ch%^z<)AQeZ!yn|2EegA46=;hX% zivlgM7#EtsJ^nUzog6M77SeDxHnx}dOWshq$jy#@u>O;BmS{_XE| zng+;FOEyD&6B7-nJ>4az-tDq;Iu0cIi7nh7*KNPk)d*mG=(9@Ix)27O+CRrwlmuq> zO|YYZlHtLk`-A1&+8iu^E=lD&NVb?P^0&Tns*wl=gjGo`JW<}>x1!$cR=`xBVChe` z8#g5}iBSZCZI*CVe=5E)^NsnQ_{US*?8K}Wk1@@_)ACVwE-`8?n zoi9h8-|Oel+xiN7Ari|rH!4(Ovk^$~mH=AyHk(KF=xp29L&Dcb9L@%R1N)R@Ru%)e zZ^(HXAVwFITl^6Tx8*>%39~3j|3JCQWUNTV2}6dmwXpP0uY+5j2~q7}G3JSZ$8X{pX?dqDsj|Rx1T{Cy>>&z1aIU{BFP^bh#>}*| zCaheQgcq;16KreWY_NHEnZm4XV^Wvet)R@Xi8MNV{Dqgq@0q<{=Y?vPkO@dAs|R() zq*61ml?rUzXq`7X2QTHfn_oV4MPZelx+Q)QC&=d2Z)xdA>_2&BO2pa&S?mm>&dXQ~ z()#73Zcw5F-hmD^w*i2@_uO)P9&3-wyH&!Sli;|D!rKwx_}6 zDhOlP9}YHx$iHuCI*AT2|B5twHo<>8uJ``^`?P3LKTWrLA~g#-a|e= zfTfNO)ei13G^NyS8fsZG>2(5!NbO{C1AUA1@ln%VL^RrV8k7p^CdCa_gCOw5N36kE z7lYwRkp@tW>OE4QI~qWgt!l3r!4zPAb{D%CxY`!IPZOk>8(hYGh`W27*M9hpq7O<= zN}gO~@6Q_z_N#%nsk*mX#!q;zcmS8_%&2)y%%D*Pu-QDZ3SOidX*M43(yptjw$%Eh zQ{0+R!Wn$}=4cnIs!{@4VA#;CToET8E)xWH2?fF_Up`nO!N8(A)&E%h3O);;*4bw1N zMj6^KV89U2Dv&%u>Qa!gP;>9DyGi>9b@=u&5+C}oUvIIe;6|1}5RN{|v$2;=e2$|c zD`G}v0;cG%+J|pGQj8emr^UU~yh$Z%{-Zc%-=Vc^zO}%S$GLQz8I^&EGh};6B(+?~Y_{kq6Y8{YsxBzWeU=YE!U6;2@Lr0wM5$XY8 z1@p0ln#?C&$cwQ6Oh^-LX?NFmbtR$SdZJ}zbs1!#etHW6R|FEnN)9w4PPyNf4^3rv zSHaXtlJ5t3IO+|J=HpgvJN_KMx(XNxB%AEUm~UO~CgZ8Kgn+I920y(-MV5*Tm*h$? z5M}l+Zcz|D26jB+3j|P#*f^K&rzX6MR@@G@8UT`ho#7&94%UA*nYjm&60eaOGK=Nz zwc)>IFG`exib1fJZp6Q-%vN$3^i#y|CDmcZnj~xjedyjQhJ}!^D{kc?hhAghP z1kxKZOq}qF==vFp_sI&iSO@yFC-1C-$>Wte8A1G*`%=_6nbc=;!*yf2_6}-ff@5Gf zG_T~r82f1m9IR$k3im7>(nx~i$vkGX zAk7%YWhHkvDpa&0{23v4hmu0X>AB}(N?V;*1=_MNZO zqfm5c9yw=Pn2HOUcvooVIBP6>cibSa4IdSWsX}ohqC|r3_2f>*r6Qaf{-$kOio$9e zT#$mQqZ2-wS~HMbB1x!{jq!;9NrAyTgYddF9bI}_I+TFJM#QK=gjLD{U_cWG zvLZx~P6Q459hlxUbYGv*LKa>Oma9+lc;|MV5Xt?G7|Mwy)GCb29m%3SG;s{79|(pK z!t7r#9{!i5ver+or?*`6n*Oz}F+86%L+H`W>vfMcsUgbMs{6116)Z+X7Gld!IBF*t z%uZ590G=nN1S1MQIOwuI%Y0Z!^|3lUxP#8?oc&k_Hs@>shoXsM6&+N*yy*q>c-?3J zlSxy`K2fQyeFF@vKUC?qaznjSs7>H<82 ze46$yq)3ou{tQn|#6zUUSh#2O(3>Gg(7xHt;q$<>ne8oy<;QdF+xSU0fDn^3Ws-|9 z*3O7T4HO>?Ped*=(rTm`%D|h(R6Bf-KsT0>(Z6EL?T~+NZ%FX7r&};pZ^#HKGNyD5 z&_x~LWBUf%pIo{_dd$m7k5CR&@NqOAj=1`}0D7cQzNVnoX7HAIc31J%%}51&AI{Jz zXxX&4JI~!rV726}E>j$xz25{Uaab3iD;PtsI$@P7!YkAn)NyD!_BwC2#;Te01|;{N z3rdv+=Cvkrq%ih|xWo=rTiU|d8$qA`h;bueV9|iz z`mNm73Rr(|MB!G5;kYti+fI>TjjIHOge*~5k#ahk!FW(XX25Vz9cnxn`qITU&@E9< zh6mCC`b5&d0b&wLb~Scl(8Oq|_zO2EdnNMP1!xD{0Zm`8#zPU3l8YiykX38Bcnx5Z zknl0(aBEf4l#UR4NknNp4p+`-nrVm6jfk6O++yRT4RhnW!*zLlR(c)FPY@azlBieU z^mOQtP0;{8KjSrb-Hxp&K9_Gr^gv!8`n9^sbz_+(Wd~RQ5CZa55N=s`*n<3p$>yeUChyxZG7D}nS#6@e9T>eWCq;SpfAalajpO6 zS64|uj6Ta<@fC=k7lUE8*D*BkqoIs%Q?h`X5_3J3y$S9U$6Eixhj(gouminaaJnl$ zP(0^I+@PahP`edbdW75~Y0GfKV5L6x-qK=e%As5ySJVToE~WX@aAN4lp8?_l&Z+G} zn>CtV?3kmaX&N+|Ax+o3kQC;v!Xf~q>G8Os+2-gPwoJ{93(`RzTj%xJF{7(#bXuY1 zNuD}CqmASfI^7DdW!Z~(zI^v}wu)9Amb%7UpIExI{)P=K&R1ZuT=X!_IW=$7ITULH z0fg^hWnVIBF3nq3x;xET{X}m1E4!-lSy&h1d+%Ka>f+o}0ll<}Privnd;#_0JmSQY z^$hx$_)0_uEqMZrw$T!$4ya4_98%THt4;eBH3jFw)-OBRzB{7YzdIgV=eI?15&?7M zX_Gy$4&Ny0+HDrlrX>B9Ao&|p-S%CBKovFRPOnf><;OVu|AlvE0bkw9K7b9bu)T*TFBzwBU>3s21ov=|tgcAiH4>tzhoN84| zp1*KPRUJW=0je;X5y%Io_`;9`RXQdUIE?&MY|&JHJtcBuhM&jnCYm4%xaePxD0g0c z5Y@rLDJTsR({71SCK**xs+vxzk!M4c}?#lc3?9}b;$Bk2so1W*$ZD}_seW|fH zclU=-l^M##$4QotGv>BI?ZVR>eIUK@K>Ew8AdS0DG?c}X%hG(0m~^o34pV8Wo=63A zWkY~1VB3jOEjNJeOwZJma(kfgkaJmAuhjLlf^A@x2U{|^c(|FBP(|XcPm>M^`m|R| zgyK6h*H;5@qql!G*5wot=wDE>I5nMyPY-;Fs!?y~(i2T=yL>M8ks3#Kg$~IhWpCH$ z8hMi~1t@^v+Qxsj$&sNfh0AzU?}I8zLaCwbp!LvGt=M%)S`C$U3Gv3nWr_$wgMU$% zO7$(L_j-k6VJidqSU5Bk92*cn>=7(T%)@432qhyh8WD*gz2sNGZYckzDeKty#%=)O z%aS_{MKl0TDaJ_bmk0zGH%7L5b!s|g2NdG+W8tV6g(5=VMwV-;ryNrYlklT>_14E3 z*IHpB`EFmbs1AH4IShBBbn<}rS<98%BC5D5HSGd2MbtP$l$Y$P_Q)6Wy8{GG$)wHh zi|>qD7KHYiBs+1IFmZAeLu*QsTCT^ipGBf&cRC`CGjT^*Q!aOJz|L&fh7Mpa%gcjR zr`?BmfwAy16NSNSL{EOs?+qVjR30r?E_I38#331q#M4$WF=k8W8n9rk4n`s>i3b(H zY^b^OX{8CPO;(zK5Oodic5(pu;u}y`hw;Qfx;MBS%Wz+oOah%KcO{<>z}?3f2BAnU z5VAm)9-w#@0sNrA&0!ldVDvbWFx19rWB!R8<*AYuO5rOA^K{mEPG4hf&KI9CjsP6O{WqK^d82*hD*dwFvbGTW1@*>j?47!p8x}mt`FB_ zOUMIT(}i;jtAJQLq(&=EKrf?O6hKKTAuz$MC)g-S5?~g{x3T}xl^E{k>aSu;S2#R<2mk5ZzJPSx0!C|rnF(up( z(pamQ?#z9FC(?z61uX2qIaxPM{0is!(}f?;#r9t#&J-DrPQ#!+Kzly)$@VtcwVNH&Fg9z~@qz>XU46Os)f`+)Mfvs6= ztBL}fewl^h`lLpjkD~+;Ly`tw>(f+C+BH3?`Bs7fEqh1;1=jcq3%aox|5fc1Re^7n z!VNJNEn@pny_yB+24SvM?CrTs*s)SbfqudkC=96dO?jpLN&nb<+?p~)=?WBH@2NPc z#0=Mxf&DTcu9W+YGP>(B!BL&qXXOGDtx?4ARto7k9cSjy($*vNJ77o1OsSM{$pHl+ zWztfuRJ6U^iLAN6$tgq&)nq4(sWs%N@*wq?n^;T7X?8vO*5ilcld8*{_f=+1z%3j7 zZ!bfQ;?gNI;~d;p&?oH1>YNqhj?5R$3&-3co9_aT8PWX()EB&v!`N!! zjnW*WQ+HX+s&c^t$>=|VD8V|xS48W>9A8m2(@p;p(ur8yLvWYwK*(~Q-SmNcU2u=M z2n~Y&&Fj6G6`gF(LDYHcBAzMmfyp5rdPM?zb3caxwyq)Q^TG}N6$K;h+NL-xJQYr( zT=Jh1G)j}*AzXJ9kSko8Cb95!f4_3B(e~wT65i!yQH~M|vR(Nx4AC$x#nE3|WBeHT zxA(l+*b;tnf`518R8_#Hr;>lqfu-9n6PJAN5dJa$f1^ zx{dK?Ckx?d?(c&iIs>BIB=U|gl@=Y_>bjcSXjoZe1%vwKU@o>C7L5+!Z_ z)oHS8_R0)4&P#Rg(RPgwk>@@pB%01xu>xh(>ygoZ&uaLb)MH&uv)00Rd|j@Y;DPD5 zB=$u+3xYOO$b88OmZm=Ac$XTUJ+mm?h1p{+zGm1dUAfq%AmJJD_GdIrU-v|T@&kC` zE5N$nofamhM>MHbS08-i5f3*|k?pthL(^wnVO?OgT<2REQjWQ(+leTZY$mDmDt46N z%m9tjVqf$z$_^<_0Ie=&xSzb5iptLj`MSl|Qq&ASh5_imMkK{#%Hx7*d4u--T60XN z!QKjYgB=+zOMx|&M;lkmTzXL;`9oAzJp3pV_)i~WbAg*`8&;^L*qG91*0uW})5YyW zVC9d$cV1Ta`_o*uROSgKb7B?DAfNm)JNq}&8wPUU05J9%MP*IC_i-t(nJ2p&Ha^Z$Tm2z?_ zA%OKw4H!|T_p|xI;B6IUPGeI+1x=+uTo$q1qdNztrGYec%^UnXkU$wZ(JRhYRP!B4 zq5Ms$pg$DYw2fj{gsYG&)hJszE?oI@ob$Q0iv@Y}`U`|T9toxz9c$*0aMBfI>R4xn zP$;O|!G7|%Mc4$Y=LgKEtENvT&CL^Uxeq$b)Fl(qv83f;epy%U30&qXuHiYnUI$l= zvMVf2McX`$Uj(9Xz`U`*yt-3gR4;fpYZk?fn*~y>!YP_-F4zN7;_4#QzLt=8Sst z!`YQIG^6}D%XA|iPnus9nbwnO1W(a>qHYb-O^h @5E5^jow38oGT;HT{*=6I5BP zPeN~)y@whdfIBsRmq+Rdg0wTnQ7nPHr0W78U5CX2223}%1mx1n&83U#f08@zJkueT z$DgUg@4aOBobTYZlOfw{A6tR{zC7-_)wQi%FtW;tntgzGD%#dCRe@ntYfE`#rbn&Z z|6)Q;ve5_j8Y|`QupfJ-(bpq$$OCFZ)G9H^2@|7fmRV!La<CzlFK6uux zDViThsLzm$UXb}MFKscAfY?#%;oqe!RS;J>Pli? z*=6TyI2eY>)`{x{|H)5~wzZk*nbk;8Hum(bGtPLHh3P+DL69&UjNAU=?vWHz(U@W^ zUWs|p+FYcOaUAECH@kQEWn}}C)F?%Ei+KbACvL|Q`}9O?5fS=&J62rx!~y-dp>JBk z9bs^&ugytSsijMeNwVztda@V=S^OgOrVFdK{}6%-?Z4bPjb(}DCsyL>a=l4)9R z(gKn4BhFvdE(c|mpR9yec6#HJ3)t3S<+*%tyQA#oa#3(OUJRh5vB0trS( z>ro`qUC3E5kdgtD`q)qi3z)$myT_u(3-(|@_>f6d?~FpNyU9p22Eds)B~he0(bnY7 zOl<)$uus~sv1J|G{86+|@wQx3l|pNVc+xPwos)G^%kJxYR+n>6tlx4|Ja15QTw_tC zYeOqmD*AOhuBG$2m%3gBDQ|hw8g2|63)U%sOvtnNNfiQb2PT~a4G=GK^j55H3&}66N<55Hdr-s1T_QQbQC0td=>|8^UaFv2te*S16MGw8D8{J ztmA2B5z1QG)O-k6fjwPRETi!V@jb~R8^#}Wbp;34NzKL7U;YwfSe#HkW7!a#f=`AB zS{#%@>k_3(1>pQAdnf`zSIAvH4Lk7}R!V0Wk=P2~m}PRCkNNv`zFMp}V7x)Ae3*+e zT@J7Kw{BCI>a56*fpRrJ1{xsI`g<4(Xz0lM0kRQ!fkr>p%2R3FWyGrxwLZcG+Z8~% zbbPYx0SJ90LQxW~jAEj}<9Z6w$4wd{9JC`Vi{_7b2V2FG`qU^=TeUuDt_eFXz2h+- zr2ls7cl_qaea#?D{m#@P;C$=s`VnJ{|H!(OmbYOrmX5C)V*+TGwbh7h4d0Nn1aAYs z09^a_sKy<*7-n1i!>87%{1$o6W<}-LLq}aIBj>1Q;o}uwcd@oh&8kO<$MRG0Z<0CR zb@KFlHXO@0j|ks1f)}~YEP|K2RN`ym!}yp%`d@~3PS)YjVQHTH`eLA96!Q?=ajMxY))FcVAiA-D?unhc4w)sti3#;i4%UAqQ4@@C1qKAw`T^iHG8pdJn&GcPOO?1KFg;4 z_V6UC&hRkaxH{43Q2(6lP(^%VbPJsBo?WQqXRqO{5kPflhO^QRL$us|P;W{PLr%b? z8gv}f!kP3`dwBA1c;|eWL>_n%jTe<}5^0E`C>PxN53ElcwaUGngH+w{-*5U(JQX*y zbJ>g1zIJ>YmvE`x^*#H4kUGKYC+dn^=BbaincT=lbjLTAZ`jdW|QNpj#{J1uQFz>VKj&5Cg1HyxjRZUxOcTfLz+9#IArCj3VVo3j@Cbp z{|zLl27BsZ8cp8_OPd5iUP&-yScnW?qclaE62-E&7z~9Jpz%v( z7$q>07k%bF>XKZA5kV4>6+PijngVDWn3UrtjqKZgInx(k9Ys6m^$EIwWjUe0^bwlvSy7QpsZoEcF zJuZ`N2c;-1L^LLX!Xsp^=NhGkP4Kp9B_D(SutIskq@adHu(=W@_+vgA zf)K1|>G8>2Isl<7gO55yC73#wqdX+kSf-x&{b#R&_=*c@Dt~w|*>|E8nM)huPD}q~ zk_afZJ`q3+N#O(+emX&Or9Sc359U+m+WC@y8FIw>D)qFs3?0Uoz$qWip(qMQKA}r zTVI!Hs~!f_Owx`Lp?WVBa<*=x_{_F{W`(rgS)pnv2s|>sM_M!5@hd8l^^Ef}G)$|i zVaY6gZ$GWAsWNb|a&ZLScqDQ@&l2P3jzdVD6MY;`NVLqpV#({JKy_6V7}a#Re1E>i z(kfL{j=e9FoMFmkb8h8+AWD94J}!jNZpS&Pqi1#N+dL|aOtyU9P7KY;;9>tLmIeQhilgss4^1`ahGWdT zq`sStoA0>~ujyJe@xHi_dr+{mXsrp(Kve?`5nmy5seYdK=30v^0mb^TK-sDE+-fw5 zY_f%aV67{>UiM_OTlPup-P0+8TqF^G!5wy}V$kUv4kVZoQxN1q3sOHV3Of-1wA}O$ z6c)h*u%U?Tf*19G1&2IIA%d%a#*}eKq*8;!)f~XmUfiJzo$fqj49Jy#oWIGFz(!n+qW83J%7cKFIiVxKypPJ8*&EqQCih&hT&OEq+mci1q+-;o6$1y$nBT7!2cs_G<$J{Wg(zg@;83tE|TdtLl0h zP9{2rQ)h@yWB_ZomASbDfMD}tx4h$)>-BGmV7mecPmSqkiJiUBdJa>1D6V)bZAEiV z+17_Gs=3K}@eHZIR~8Vm0;A~T4t8d2J>!q7>k(k9nNw)#LUzKj@QgGiSw4dJ1BPE< zL!|22IXQ;uD%o163-uPpyZL1Ws$X`V>A~FNzk>4Ds8&9IG{!1E_$}$ON60r+1U;8j z?GFIFrM!jni=dUJhF~se##G*|ZfuN?xq!F^X48|dL=)&x0oNZ{=s#6 zqw{2%yu|wMzO9R&%Gy12=UdQ^?y4J~0h-ei8J2BzA?m#3UKP`w0*lohg*oT3=DqD!qZ#Yi^I%pP_# z+}U@B?^b-6SmLa{^-p5oJ~Mg_{T5P+ zw4QKLchQHiKspvb2s;!AuEr|Q{5QK5dX>J`Q-P;tU~sCo9aBDeQS_3BU=}}dlm(aI z>@+E>tG$kx82lf1qo9Y?&I&ov{;^sIHGzh&8RmVpR?y@cbU<}f3=KPGbIwN5szFgK zszQ5Woth$yrY1bl9%}>~6b}sqP`t;-#80M`jJ|v>=KeIW2Np_whB&tSauNOv$E6&? zw_=*g!10Grf2?^xAieU5P2%~snKZUD;H&+QTwtB5J*=^_O%=86gD&HpRsWBJo`=_) z5Kz$u6HwX48Va2fU9ACW`)~fQO;1v+C|jSO@fM>oi`k>qTY%2fLF#v|Meg*S1Aacg zja$;5zgtJ=-@V>%@Dwgv`^%#{Eh@8rJ6-eHEB$#$TD9I9#>B1XYF5=;Julj};G-d=_p;1kY#+htJ_@kX12Dl!`p6*a<7QS;4#;ZqDm`Rl^ zYP@nYBNeq;S%2hQZv>CvC~{Pf7#BtlqY%Xn8(!vlF~(Zxq}&x!8+Lc~|xAut0`nU&E1)lv=VSHS<&@kY|$W5O-FZx;-) zYuL@dpB|7^tC&YOO+A1_=pzuVutIZ|vpb{mF`<53!%M)pqW3qPGQOf6yR{WHq7rUc z7n-AL;i40tBRYd=I8-Bw0s7!DDKivD^2;C~u@Z|HY2<-|kjh7p4=#f|Yy#CAfg9!@ zK;D59R+A($Tx$I@{XoLGN!j}EooY*o6;6Fe;DxP0StfIcqB+6A&2>e~ADveQtJc<_ ztDm7DPuze~aK(^w&2p6t@cm@?4uw$Z2dfTUH1vRC$h{^6>e-&VL@8PJcFzXlcxQ~# zrrG@=OgD-yF<;q~z#*hmCjrC!J1NDDVii!3IHN)iA`$;6;&a?W>OzJnyD*q57Y}W_ zix0}>;gsRw0nR^fZNzEmrtQSsGZ34iAGXq{p^ISd9_>Q1H_dM0S_jAdS${mfxE)Z| zc^V?4aOB5r0s=SLLz+ccN*%rbh0Xa%obUP71)rZTbfiGvzm-X-8zBrF9wcm-fgT@b z1T+{n88RJ;RM3pWvK@@h@t$+IK7- zij4UmeQ?(txJhDSM|mp9PMqoJf3%=vW6TKr|p&Pr)|u5ToMILn;aTAMrT zogd!_0$!xd*T8)qpmWV$5cXU^evZ_bE$~^g^8msGocUe>#U#R zd$>9RR7sT-HM?@EKoHbQH98s5;e3n+*E%>O5h&?!i8V2yjC1+o*olC$3pH3V;DDx^mDF|=aY^DAdO@5Y0b9R+AzgQpAgPB#ma zu77s_JR26iMY?{bu07iNiHyH*+l%;U77kzM4%!JMHVEKPd{mKwIm8+{cxWaioTY)# z)ECxj@$1@=_b07ol;c@w2H+_TX{aT=DTbxRH3Z`k3P~%LB7M`$otz#(ztsTzI`~yEr!{@`=di z%7r`H)nfmA)3%cXYX#i4>t*#&q=~5KFt>Hn0fg>+ef8Glw^fLboRXj``NsbaITD5Y zY`^D5>+>W93~8SzQ(?c! zphUAFsSwp!Y0rSHVP|_89AuJ)fd5#cxd%+~wsC90T0QC>$nLHiVP){mox_66^u!XiS{&})$b?D=C)`G8f zyOPI4xA3kT{!qR3=Tf+o+~Kb?@GO>D`Y1{Y7%E{@D)~93f(ywFKm1jSZk2sc*6bHt z`ZPGQ-`YTjf-Tr5ODA`R&*0`QhTUoXCwTa?G6CL!j+~~_fdS#q0`2Z}n6KnLp-zkQ z+{$PqPOM5B2dCfbEn%^rEJ%iRMsbFcw%cd^?x91O^r-FIp`=SOs*pWmUjXCDlFs5I zW?%k_kpbpify9Iq$5^}j3ONmLTwflpz?d`o43W*hHX}sKg@o!VxGK7(MrQS3$pZAv z)VlX9m~peljRlfHosKb{*SYVpB)o5a@Xw`bMc&b!KQGi*{<@@;ESkZH zRJkA55jqtn!*rcf`p$TpS4)p7c3;i$pepK4N5?5lRM-Bmf^lvhyv7x_B3CAC)@NXb zp>b7##PRjEWFmrauzu=CWmWpwd3;~(6%!=oOdsC&jNIny1A@6=X`9Rb%qngI(IzS- z2TY{J255Xs;KT_7cjAK<6DKK0oydo85)ua``_m8xyRYXJkc9cLW$FA2KU-G4>7b|A zCDmq~M%m7Komm{vO>0rD`9sZ9AM~%w^WqQ~OlmTiN^1TYOAq`u9Y5fFHcr|DNe>T6 z>Kj;9#vdVG2aqrInGQ=EH6IZy z({XUf@;^T94lON-BI==y>@<;Wx`E4(Rr`8V-CyK?wy0e+nrcOM=%(?im$kfkFW-AD z2yEW1y6F0|A#MgZtl@t2Hz&#d(-?uelbn$F9{|L76EBxvf=}a^@SUD(o&1!M$x9E) z=N<;eE37A-7nNRtG+4u3x*sB;cRw`GQ7xm4BdBfV9+X5;Ph$UHA8LekL;OKQLF>Pt zMo)*{fW}QY0}Vg`&1)}D6K)3C*yAvdByMsOXrnxsKu|Pqe8L?>Q_P6WXw1Y~Ci*`d zT665eQ6MkDo1LLW>&QA{J$p!BXR7E_^6PhF>*ZZ5F*4nk)J3<~Pl;FFZqqe=y_7c$ zPF+UFzOR09H8R28jQ6(lC+q8dCRy!P(sXmr8DqZupjvzjSI{eR1$dS>@Xp?eFD}}N zsNeIxkQF3ecB;uGyW?MJpSj{Ar|!wMwdfiIdFU$IpFhfP^%?)W@6K3X|IzdiGK;}Q zkf(1(4a}Ad456L1P8wM1^2hJNk6elSNRl8e2#^LAz|tDl_kVizetmfEetQyum4<%U z1sZbzDFT7W0081n!61L{$0!WI4l=+v-(SR$Upk|R{o|2Y2#J3e0bw{@m&An%lpr5V ztFp>te&a+oNlY~3Di*zE0mPzg5bb8&&F9>ep?}F26p7W`$4{~1$@s=i;lFODtZCJT`2aq_s-I0F0!(X57-LmVf8j(8%&B@l<`RyqCzajwO4KNJw3TO-13wQ`b z1r!W43k(8m2wV+<1yTsI4+;Y+0on>i1ttqN4UPqF2!04*2yqCh3)v0@2gL;y4s{02 z1w9Lc0mB9p2s00h0(%072PX>`4mS!f0v`-NhyZ}VhhU4)iO7N2ibR2Aid2j=i!6gY ziz0$jjtYP(gW8LRgJzBP7o85>8hso?1fvw=5z`d&4l5mN5*r0u6Z;Cs9OnX81a})R z86N}R6u+AQh`^Mfoe+Xhf-siwcSk0=B9Bt;>WB@-YkCWj!GB@ZU= zB)_8|qp+lyq$HyBr`)EJp&F)Uq4uW!q>-R0rbVDlrvsxirU#=BWuRinWW-}^W`bg} zWx8RGWnN|BU`b@TV8vsNX9HkUXB%Y)Vb}kExsyIp9~uw`2$Jrfo&ZR40&w!%h)NOy znE9IrEs27*O9CjNKMhN&HYJs`mV&_}{X~CA4%eY#E3YbrRISUQ41p6bWs$2Q#v03F zCSliy5IF##Q)$*6K;Z%qzy%IH?j;~Cse{dFX1tjnPQPy3!25Ck!2l)g**K?zR=Uqz z=>y`Y>ue*_If1JDOZ0$a{g>f^*OuKiE!}?GZJ;~)=al<_%C;Tw0Sj%rZ2Gdi(=LNu z;vvK%iG_b$M;XoHsozzxF+Vnsfm?lm+Y}xCUqVMV*;F^o z+}m(>WMz%&1)n5f5QHlAIX<07q~cvY6j2Xjr?sA*kFB8=VTS2H}n2@3bmX=T0`@-UTgJjA19AyuSTdi*W7vp zgP~f1=ib^)y+6>2OXgaKJNJA;=Q5&`0(k4>tasH2?T?(wf+r6aZT*FiMWF z%$NjD?^fxVkhtJ!SxKHwQW0t5E?S%GCEcX6gx>4><^NU3wMKO;h9+jtNeN|&aG`=W zF;2#lDtfx(U@WGqt0t@p)Rm6PC6>jQA$hF+3jaH5EZVlhRj9o{l7@b{u;+Ui#drSG zo2RCiWp(yI-bM9R@n6o^D4V$*u^}vHlVl|uPdy*gbv*87?0Vhyynv9Xk;oDV0_U4> z_K>@sUkiV0`pN+LH(T?I-$HI{_)iTOg5?n7{(QXT%|^`re0{h2Imhlpbe@Ud1A@$NOCW- zN0;2e<_ti@w?+Ez@$6i754w=1U>F-E;2iIP~_71tdZ8>T#wWen>ivqWBSp# z@0v}aRj(zFCu4ypEzPjL)8-Q~ z2B*IlygQ5pwY|@Um)T8cXp;uFHJ);Q(RqccmxUacSRv34^^7ilL@WoQ-T21XpUN!A8_jHb`4o2SK9npyLtA`fTI-C_k1)jA8^I}9y74X>_sU6ol;p;M!0 z#gsy$>}fcvOa9n(mCWHm9V}qj*OkwgC=7Zq0ct&7+GdDZS0HNSJ(EZ9Q}rg zi+WS5%FqoBF(0{!hD~3vI?D%s^b1B!*wk(FzM{ReqSzX3y5=G!g|Ie??DegeHM6=< f^T0hF-L{XeT%c=JDXyt+xV@DB10tt^kN^Mx@BjzA literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff2 b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7e0118e526eb53511cb57e7cfaf515784fee4345 GIT binary patch literal 13584 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 W8!VVrwhAGt$p&j$lnnbyiU9!e_&Ztv literal 0 HcmV?d00001 diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.eot b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.eot new file mode 100644 index 0000000000000000000000000000000000000000..d3b77c223afc9a0a1af914936219da2b001a010f GIT binary patch literal 202902 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