diff --git a/0.4/.buildinfo b/0.4/.buildinfo
new file mode 100644
index 00000000..d064bf89
--- /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 00000000..ca2072fc
--- /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 00000000..64b6c5bd
--- /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 00000000..d83450ff
--- /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 00000000..a60a9689
--- /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 00000000..2337d924
--- /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 00000000..443802ed
--- /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 00000000..495e8eaf
--- /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 00000000..26464ce6
--- /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 00000000..acc35fb2
--- /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",
+ "
"
+ ]
+ },
+ {
+ "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 00000000..c4ee9bc9
--- /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 00000000..ebecd362
--- /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 00000000..ffdc13db
--- /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 00000000..110e3f43
--- /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 00000000..8dbf587e
--- /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 00000000..e109ddb5
--- /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",
+ "
"
+ )
+ );
+ },
+
+ /**
+ * 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 00000000..f31bef92
--- /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 00000000..3d28ab20
--- /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 00000000..a1bc094a
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.eot differ
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 00000000..46ad237a
--- /dev/null
+++ b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.svg
@@ -0,0 +1,3570 @@
+
+
+
+
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 00000000..948a2a6c
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.ttf differ
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 00000000..2a89d521
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff differ
diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff2 b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff2
new file mode 100644
index 00000000..141a90a9
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-brands-400.woff2 differ
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 00000000..38cf2517
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.eot differ
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 00000000..48634a9a
--- /dev/null
+++ b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.svg
@@ -0,0 +1,803 @@
+
+
+
+
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 00000000..abe99e20
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.ttf differ
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 00000000..24de566a
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff differ
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 00000000..7e0118e5
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-regular-400.woff2 differ
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 00000000..d3b77c22
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.eot differ
diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.svg b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.svg
new file mode 100644
index 00000000..7742838b
--- /dev/null
+++ b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.svg
@@ -0,0 +1,4938 @@
+
+
+
+
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 00000000..5b979039
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.ttf differ
diff --git a/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff
new file mode 100644
index 00000000..beec7917
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff differ
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 00000000..978a681a
Binary files /dev/null and b/0.4/_static/vendor/fontawesome/5.13.0/webfonts/fa-solid-900.woff2 differ
diff --git a/0.4/_static/webpack-macros.html b/0.4/_static/webpack-macros.html
new file mode 100644
index 00000000..144f1885
--- /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 00000000..6d91abda
--- /dev/null
+++ b/0.4/api/collectors/index.html
@@ -0,0 +1,771 @@
+
+
+
+
+
+
+
+ 11. Collectors & Extractors — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
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.
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.
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.
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.
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.
Alvarez, A. M., Louveaux, Q., & Wehenkel, L. (2017). A machine learning-based
+approximation of strong branching. INFORMS Journal on Computing, 29(1),
+185-195.
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.
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.
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.
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]foriinrange(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]/mforjinrange(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.
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.
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.
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
+
+
+
+
+
+
\ 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 00000000..443802ed
--- /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 00000000..443f6f56
--- /dev/null
+++ b/0.4/guide/collectors/index.html
@@ -0,0 +1,595 @@
+
+
+
+
+
+
+
+ 6. Training Data Collectors — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
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.
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.
importnumpyasnp
+importscipy.sparse
+
+frommiplearn.h5importH5File
+
+# Set random seed to make example reproducible
+np.random.seed(42)
+
+# Create a new empty HDF5 file
+withH5File("test.h5","w")ash5:
+ # 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
+withH5File("test.h5","r")ash5:
+ 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"))
+
BasicCollector is the most fundamental collector, and performs the following steps:
+
+
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;
+
Solves the linear relaxation of the problem and stores its optimal solution, basis status and sensitivity information, among other information;
+
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.
+
+
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.
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]:
+
+
+
importrandom
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+fromglobimportglob
+
+frommiplearn.problems.tspimport(
+ TravelingSalesmanGenerator,
+ build_tsp_model_gurobipy,
+)
+frommiplearn.ioimportwrite_pkl_gz
+frommiplearn.h5importH5File
+frommiplearn.collectors.basicimportBasicCollector
+
+# 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.
+withH5File("data/tsp/00000.h5","r")ash5:
+ print("lp_obj_value = ",h5.get_scalar("lp_obj_value"))
+ print("mip_obj_value = ",h5.get_scalar("mip_obj_value"))
+
+
+
+
+
+
+
\ 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 00000000..495e8eaf
--- /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",
+ "
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.
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.
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.
The example below demonstrates the usage of H5FieldsExtractor in a randomly generated instance of the multi-dimensional knapsack problem.
+
+
[1]:
+
+
+
fromglobimportglob
+fromshutilimportrmtree
+
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+
+frommiplearn.collectors.basicimportBasicCollector
+frommiplearn.extractors.fieldsimportH5FieldsExtractor
+frommiplearn.h5importH5File
+frommiplearn.ioimportwrite_pkl_gz
+frommiplearn.problems.multiknapsackimport(
+ 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",
+ ],
+)
+
+withH5File("data/multiknapsack/00000.h5")ash5:
+ # 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)
+
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.
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.
frommiplearn.extractors.AlvLouWeh2017importAlvLouWeh2017Extractor
+frommiplearn.h5importH5File
+
+# Build the extractor
+ext=AlvLouWeh2017Extractor()
+
+# Open previously-created multiknapsack training data
+withH5File("data/multiknapsack/00000.h5")ash5:
+ # Extract and print variable features
+ x1=ext.get_var_features(h5)
+ print("x1",x1.shape,"\n",x1.round(1))
+
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.
+
+
+
+
+
+
\ 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 00000000..26464ce6
--- /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 00000000..bfe31b78
--- /dev/null
+++ b/0.4/guide/primal/index.html
@@ -0,0 +1,541 @@
+
+
+
+
+
+
+
+ 8. Primal Components — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
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
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.
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:
+
+
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.
+
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\):
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.
+
+
+
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.
fromsklearn.dummyimportDummyClassifier
+fromsklearn.neighborsimportKNeighborsClassifier
+
+frommiplearn.components.primal.actionsimport(
+ SetWarmStart,
+ FixVariables,
+ EnforceProximity,
+)
+frommiplearn.components.primal.memimport(
+ MemorizingPrimalComponent,
+ SelectTopSolutions,
+ MergeTopSolutions,
+)
+frommiplearn.extractors.dummyimportDummyExtractor
+frommiplearn.extractors.fieldsimportH5FieldsExtractor
+
+# 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),
+)
+
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:
+
+
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).
+
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.
+
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.
fromsklearn.linear_modelimportLogisticRegression
+frommiplearn.classifiers.minprobimportMinProbabilityClassifier
+frommiplearn.classifiers.singleclassimportSingleClassFix
+frommiplearn.components.primal.indepimportIndependentVarsPrimalComponent
+frommiplearn.extractors.AlvLouWeh2017importAlvLouWeh2017Extractor
+frommiplearn.components.primal.actionsimportSetWarmStart
+
+# 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(),
+)
+
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.
fromsklearn.multioutputimportClassifierChain
+fromsklearn.neural_networkimportMLPClassifier
+frommiplearn.components.primal.jointimportJointVarsPrimalComponent
+frommiplearn.extractors.fieldsimportH5FieldsExtractor
+frommiplearn.components.primal.actionsimportSetWarmStart
+
+# 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(),
+)
+
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.
frommiplearn.components.primal.expertimportExpertPrimalComponent
+frommiplearn.components.primal.actionsimportSetWarmStart
+
+# 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())
+
+
+
+
+
+
+
\ 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 00000000..acc35fb2
--- /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",
+ "
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.
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.
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:
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.
importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.binpackimportBinPackGenerator,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
+foriinrange(10):
+ print(i,data[i].sizes,data[i].capacity)
+print()
+
+# Optimize first instance
+model=build_binpack_model_gurobipy(data[0])
+model.optimize()
+
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.
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:
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.
importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.multiknapsackimport(
+ 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
+
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.
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:
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.
importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.pmedianimportPMedianGenerator,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()
+
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.
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:
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:
+
+
The density \(d\) of \(M\) is decided by sampling the provided probability distribution density.
+
Each entry of \(M\) is then sampled from the Bernoulli distribution, with probability \(d\).
+
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).
+
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).
+
+
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.
importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.setcoverimportSetCoverGenerator,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
+
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.
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:
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.
importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.setpackimportSetPackGenerator,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
+
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.
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.
importrandom
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.stabimport(
+ 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
+
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.
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:
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.
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
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.
importrandom
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.tspimport(
+ 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()
+
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.
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:
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.
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.
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.
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:
importrandom
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+frommiplearn.problems.vertexcoverimport(
+ 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
+
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.
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:
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.
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)
+
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]:
+
+
+
importrandom
+
+importnumpyasnp
+fromscipy.statsimportuniform,randint
+fromsklearn.linear_modelimportLogisticRegression
+
+frommiplearn.classifiers.minprobimportMinProbabilityClassifier
+frommiplearn.classifiers.singleclassimportSingleClassFix
+frommiplearn.collectors.basicimportBasicCollector
+frommiplearn.components.primal.actionsimportSetWarmStart
+frommiplearn.components.primal.indepimportIndependentVarsPrimalComponent
+frommiplearn.extractors.AlvLouWeh2017importAlvLouWeh2017Extractor
+frommiplearn.ioimportwrite_pkl_gz
+frommiplearn.problems.tspimport(
+ TravelingSalesmanGenerator,
+ build_tsp_model_gurobipy,
+)
+frommiplearn.solvers.learningimportLearningSolver
+
+# 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}
+
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.
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.
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
\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 00000000..a3393772
--- /dev/null
+++ b/0.4/tutorials/cuts-gurobipy/index.html
@@ -0,0 +1,714 @@
+
+
+
+
+
+
+
+ 4. User cuts and lazy constraints — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
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:
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]:
+
+
+
importgurobipyasgp
+fromgurobipyimportquicksum,GRB,tuplelist
+frommiplearn.solvers.gurobiimportGurobiModel
+importnetworkxasnx
+importnumpyasnp
+frommiplearn.problems.tspimport(
+ TravelingSalesmanData,
+ TravelingSalesmanGenerator,
+)
+fromscipy.statsimportuniform,randint
+frommiplearn.ioimportwrite_pkl_gz,read_pkl_gz
+frommiplearn.collectors.basicimportBasicCollector
+frommiplearn.solvers.learningimportLearningSolver
+frommiplearn.components.lazy.memimportMemorizingLazyComponent
+frommiplearn.extractors.fieldsimportH5FieldsExtractor
+fromsklearn.neighborsimportKNeighborsClassifier
+
+# Set up random seed to make example more reproducible
+np.random.seed(42)
+
+# Set up Python logging
+importlogging
+
+logging.basicConfig(level=logging.WARNING)
+
+
+defbuild_tsp_model_gurobipy_simplified(data):
+ # Read data from file if a filename is provided
+ ifisinstance(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)foriinrange(data.n_cities)forjinrange(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)inedges))
+
+ # Add constraint: must choose two edges adjacent to each city
+ model.addConstrs(
+ (
+ quicksum(x[min(i,j),max(i,j)]forjinrange(data.n_cities)ifi!=j)
+ ==2
+ foriinrange(data.n_cities)
+ ),
+ name="eq_degree",
+ )
+
+ deflazy_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=[eforeinedgesifx_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
+ forcomponentinlist(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.
+ iflen(component)<data.n_cities:
+ cut_edges=[
+ [e[0],e[1]]
+ foreinedges
+ if(e[0]incomponentande[1]notincomponent)
+ or(e[0]notincomponentande[1]incomponent)
+ ]
+ violations.append(cut_edges)
+
+ # Return the list of violations
+ returnviolations
+
+ deflazy_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")
+ forviolationinviolations:
+ m.add_constr(quicksum(x[e[0],e[1]]foreinviolation)>=2)
+
+ returnGurobiModel(
+ 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.
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)
+
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.
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
+
+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
+
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.
+
+
+
+
+
+
\ 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 00000000..110e3f43
--- /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 00000000..dc6702cf
--- /dev/null
+++ b/0.4/tutorials/getting-started-gurobipy/index.html
@@ -0,0 +1,888 @@
+
+
+
+
+
+
+
+ 2. Getting started (Gurobipy) — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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:
+
+
Install the Python/Gurobipy version of MIPLearn
+
Model a simple optimization problem using Gurobipy
+
Generate training data and train the ML models
+
Use the ML models together Gurobi to solve new instances
+
+
+
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!
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.
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:
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.
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.
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:
+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.
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:
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.
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.
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:
+
+
Memorize the optimal solutions of all training instances;
+
Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;
+
Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.
+
Provide this partial solution to the solver as a warm start.
+
+
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.
+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 LoadeduserMIPstartwithobjective..., 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.
+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 MIPstart 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.
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.
+
+
+
+
+
+
\ 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 00000000..8dbf587e
--- /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 00000000..877fa2b0
--- /dev/null
+++ b/0.4/tutorials/getting-started-jump/index.html
@@ -0,0 +1,755 @@
+
+
+
+
+
+
+
+ 3. Getting started (JuMP) — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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:
+
+
Install the Julia/JuMP version of MIPLearn
+
Model a simple optimization problem using JuMP
+
Generate training data and train the ML models
+
Use the ML models together Gurobi to solve new instances
+
+
+
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!
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:
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.
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:
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.
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.
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:
+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.
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:
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.
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.
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:
+
+
Memorize the optimal solutions of all training instances;
+
Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;
+
Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.
+
Provide this partial solution to the solver as a warm start.
+
+
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.
+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 LoadeduserMIPstartwithobjective..., 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.
In the log above, the MIPstart 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.
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.
+
+
+
+
+
+
\ 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 00000000..e109ddb5
--- /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 00000000..da776dfe
--- /dev/null
+++ b/0.4/tutorials/getting-started-pyomo/index.html
@@ -0,0 +1,909 @@
+
+
+
+
+
+
+
+ 1. Getting started (Pyomo) — MIPLearn 0.4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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:
+
+
Install the Python/Pyomo version of MIPLearn
+
Model a simple optimization problem using Pyomo
+
Generate training data and train the ML models
+
Use the ML models together Gurobi to solve new instances
+
+
+
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!
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.
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:
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.
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.
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:
+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.
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:
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.
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.
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:
+
+
Memorize the optimal solutions of all training instances;
+
Given a test instance, find the 25 most similar training instances, based on constraint right-hand sides;
+
Merge their optimal solutions into a single partial solution; specifically, only assign values to the binary variables that agree unanimously.
+
Provide this partial solution to the solver as a warm start.
+
+
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.
+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 LoadeduserMIPstartwithobjective..., 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.
+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 MIPstart 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.
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].valueforiinrange(5)])
+print(" y =",[model.inner.y[i].valueforiinrange(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]
+