From 120c6e4d2cce13d29a8d755e729407a975b67b98 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 24 Oct 2023 12:36:54 +0200 Subject: [PATCH 1/9] Updates for Gurobi 11 Mainly use the new FuncNonLinear attribute for logistic regressions. Update the code to add logistic regression, documentation and examples. Also has to limit the number of students in the admission example to the new limited license limit. --- docs/notebooks/ipynb/student_admission.ipynb | 157 ++++++------------- docs/notebooks/myst/student_admission.md | 79 +++------- docs/requirements.txt | 2 +- docs/source/mlm-mip-models.rst | 6 +- requirements.tox.txt | 2 +- requirements.txt | 2 +- src/gurobi_ml/sklearn/logistic_regression.py | 32 ++-- 7 files changed, 105 insertions(+), 175 deletions(-) diff --git a/docs/notebooks/ipynb/student_admission.ipynb b/docs/notebooks/ipynb/student_admission.ipynb index dd167c79..22befc45 100644 --- a/docs/notebooks/ipynb/student_admission.ipynb +++ b/docs/notebooks/ipynb/student_admission.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "7eba6234", + "id": "6ce1fcdb", "metadata": {}, "source": [ "# Student Enrollment\n", @@ -80,7 +80,17 @@ { "cell_type": "code", "execution_count": null, - "id": "c9961b42", + "id": "44e78a62", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U ../../.." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d8067c7", "metadata": {}, "outputs": [], "source": [ @@ -102,7 +112,7 @@ }, { "cell_type": "markdown", - "id": "81a96d64", + "id": "03a40109", "metadata": {}, "source": [ "We now retrieve the historical data used to build the regression from Janos\n", @@ -115,7 +125,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24154c54", + "id": "c29d537f", "metadata": {}, "outputs": [], "source": [ @@ -133,7 +143,7 @@ }, { "cell_type": "markdown", - "id": "9a87740e", + "id": "dcdb4ba6", "metadata": {}, "source": [ "## Fit the logistic regression\n", @@ -145,7 +155,7 @@ { "cell_type": "code", "execution_count": null, - "id": "bd8a43ce", + "id": "d6524441", "metadata": {}, "outputs": [], "source": [ @@ -158,7 +168,7 @@ }, { "cell_type": "markdown", - "id": "4236c46e", + "id": "85b6498d", "metadata": {}, "source": [ "### Optimization Model\n", @@ -172,7 +182,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4830ff33", + "id": "86f0976c", "metadata": {}, "outputs": [], "source": [ @@ -183,19 +193,19 @@ { "cell_type": "code", "execution_count": null, - "id": "47f1ab07", + "id": "7de4d8e6", "metadata": {}, "outputs": [], "source": [ - "nstudents = 250\n", + "nstudents = 25\n", "\n", "# Select randomly nstudents in the data\n", - "studentsdata = studentsdata.sample(nstudents)" + "studentsdata = studentsdata.sample(nstudents, random_state=1)" ] }, { "cell_type": "markdown", - "id": "861392fc", + "id": "b037eac7", "metadata": {}, "source": [ "We can now create the our model.\n", @@ -206,7 +216,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b6222f9f", + "id": "37a642f0", "metadata": {}, "outputs": [], "source": [ @@ -216,25 +226,28 @@ "# The y variables are modeling the probability of enrollment of each student. They are indexed by students data\n", "y = gppd.add_vars(m, studentsdata, name='enroll_probability')\n", "\n", - "# We add to studentsdata a column of variables to model the \"merit\" feature. Those variable are between 0 and 2.5.\n", - "# They are added directly to the data frame using the gppd extension.\n", - "studentsdata = studentsdata.gppd.add_vars(m, lb=0.0, ub=2.5, name='merit')\n", + "\n", + "# We want to complete studentsdata with a column of decision variables to model the \"merit\" feature.\n", + "# Those variable are between 0 and 2.5.\n", + "# They are added using the gppd extension and the resulting dataframe is stored in\n", + "# students_opt_data.\n", + "students_opt_data = studentsdata.gppd.add_vars(m, lb=0.0, ub=2.5, name='merit')\n", "\n", "# We denote by x the (variable) \"merit\" feature\n", - "x = studentsdata.loc[:, \"merit\"]\n", + "x = students_opt_data.loc[:, \"merit\"]\n", "\n", "# Make sure that studentsdata contains only the features column and in the right order\n", - "studentsdata = studentsdata.loc[:, features]\n", + "students_opt_data = students_opt_data.loc[:, features]\n", "\n", "m.update()\n", "\n", "# Let's look at our features dataframe for the optimization\n", - "studentsdata[:10]" + "students_opt_data[:10]" ] }, { "cell_type": "markdown", - "id": "83a83662", + "id": "9b91307e", "metadata": {}, "source": [ "We add the objective and the budget constraint:" @@ -243,7 +256,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1a21833e", + "id": "1df88255", "metadata": {}, "outputs": [], "source": [ @@ -255,7 +268,7 @@ }, { "cell_type": "markdown", - "id": "aba0719c", + "id": "418664ec", "metadata": {}, "source": [ "Finally, we insert the constraints from the regression. In this model we want to\n", @@ -270,12 +283,12 @@ { "cell_type": "code", "execution_count": null, - "id": "5868b7ef", + "id": "c5354d86", "metadata": {}, "outputs": [], "source": [ "pred_constr = add_predictor_constr(\n", - " m, pipe, studentsdata, y, output_type=\"probability_1\"\n", + " m, pipe, students_opt_data, y, output_type=\"probability_1\"\n", ")\n", "\n", "pred_constr.print_stats()" @@ -283,16 +296,21 @@ }, { "cell_type": "markdown", - "id": "1eb51817", + "id": "0c7a14f1", "metadata": {}, "source": [ - "We can now optimize the problem." + "We can now optimize the problem.\n", + "With Gurobi ≥ 11.0, the attribute `FuncNonLinear` is automatically set to 1 by Gurobi machine learning on the nonlinear constraints it adds\n", + "in order to deal algorithmically with the logistic function.\n", + "\n", + "Older versions of Gurobi would make a piece-wise linear approximation of the logistic function. You can refer to [older versions\n", + "of this documentation](https://gurobi-machinelearning.readthedocs.io/en/v1.3.0/mlm-examples/student_admission.html) for dealing with those approximations." ] }, { "cell_type": "code", "execution_count": null, - "id": "efffd1f0", + "id": "f141099d", "metadata": {}, "outputs": [], "source": [ @@ -301,14 +319,9 @@ }, { "cell_type": "markdown", - "id": "1f3d5c78", + "id": "cb742bf5", "metadata": {}, "source": [ - "Remember that for the logistic regression, Gurobi does a piecewise-linear\n", - "approximation of the logistic function. We can therefore get some significant\n", - "errors when comparing the results of the Gurobi model with what is predicted by\n", - "the regression.\n", - "\n", "We print the error using [get_error](../api/AbstractPredictorConstr.rst#gurobi_ml.modeling.base_predictor_constr.AbstractPredictorConstr.get_error) (note that we take the maximal error\n", "over all input vectors)." ] @@ -316,73 +329,7 @@ { "cell_type": "code", "execution_count": null, - "id": "af9a2820", - "metadata": {}, - "outputs": [], - "source": [ - "print(\n", - " \"Maximum error in approximating the regression {:.6}\".format(\n", - " np.max(pred_constr.get_error())\n", - " )\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "de8fa4d3", - "metadata": {}, - "source": [ - "The error we get might be considered too large, but we can use Gurobi parameters\n", - "to tune the piecewise-linear approximation made by Gurobi (at the expense of a\n", - "harder models).\n", - "\n", - "The specific parameters are explained in the documentation of [Functions\n", - "Constraints](https://www.gurobi.com/documentation/9.1/refman/constraints.html#subsubsection:GenConstrFunction)\n", - "in Gurobi's manual.\n", - "\n", - "We can pass those parameters to the\n", - "[add_predictor_constr](../api/AbstractPredictorConstr.rst#gurobi_ml.add_predictor_constr)\n", - "function in the form of a dictionary with the keyword parameter\n", - "`pwd_attributes`.\n", - "\n", - "Now we want a more precise solution, so we remove the current constraint, add a\n", - "new one that does a tighter approximation and resolve the model." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a8706d13", - "metadata": {}, - "outputs": [], - "source": [ - "pred_constr.remove()\n", - "\n", - "pwl_attributes = {\n", - " \"FuncPieces\": -1,\n", - " \"FuncPieceLength\": 0.01,\n", - " \"FuncPieceError\": 1e-5,\n", - " \"FuncPieceRatio\": -1.0,\n", - "}\n", - "pred_constr = add_predictor_constr(\n", - " m, pipe, studentsdata, y, output_type=\"probability_1\", pwl_attributes=pwl_attributes\n", - ")\n", - "\n", - "m.optimize()" - ] - }, - { - "cell_type": "markdown", - "id": "4b1c7700", - "metadata": {}, - "source": [ - "We can see that the error has been reduced." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "606d2ba8", + "id": "ac8c7e09", "metadata": {}, "outputs": [], "source": [ @@ -395,16 +342,16 @@ }, { "cell_type": "markdown", - "id": "e802a077", + "id": "22c9c454", "metadata": {}, "source": [ - "Finally note that we can directly get the input values for the regression in a solution as a pandas dataframe using input_values." + "Finally, note that we can directly get the input values for the regression in a solution as a pandas dataframe using input_values." ] }, { "cell_type": "code", "execution_count": null, - "id": "c6b5c180", + "id": "aa7a19dc", "metadata": {}, "outputs": [], "source": [ @@ -413,7 +360,7 @@ }, { "cell_type": "markdown", - "id": "766c54d0", + "id": "a7548099", "metadata": { "nbsphinx": "hidden" }, @@ -441,7 +388,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.10.0" }, "license": { "full_text": "# Copyright © 2023 Gurobi Optimization, LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================" diff --git a/docs/notebooks/myst/student_admission.md b/docs/notebooks/myst/student_admission.md index 116ee01b..458f5632 100644 --- a/docs/notebooks/myst/student_admission.md +++ b/docs/notebooks/myst/student_admission.md @@ -83,6 +83,10 @@ We import the necessary packages. Besides the usual (`numpy`, `gurobipy`, `pandas`), for this we will use Scikit-learn's Pipeline, StandardScaler and LogisticRegression. +```{code-cell} ipython3 +!pip install -U ../../.. +``` + ```{code-cell} ipython3 import numpy as np import pandas as pd @@ -145,10 +149,10 @@ studentsdata = pd.read_csv(janos_data_url + "college_applications6000.csv", inde ``` ```{code-cell} ipython3 -nstudents = 250 +nstudents = 25 # Select randomly nstudents in the data -studentsdata = studentsdata.sample(nstudents) +studentsdata = studentsdata.sample(nstudents, random_state=1) ``` We can now create the our model. @@ -162,20 +166,23 @@ m = gp.Model() # The y variables are modeling the probability of enrollment of each student. They are indexed by students data y = gppd.add_vars(m, studentsdata, name='enroll_probability') -# We add to studentsdata a column of variables to model the "merit" feature. Those variable are between 0 and 2.5. -# They are added directly to the data frame using the gppd extension. -studentsdata = studentsdata.gppd.add_vars(m, lb=0.0, ub=2.5, name='merit') + +# We want to complete studentsdata with a column of decision variables to model the "merit" feature. +# Those variable are between 0 and 2.5. +# They are added using the gppd extension and the resulting dataframe is stored in +# students_opt_data. +students_opt_data = studentsdata.gppd.add_vars(m, lb=0.0, ub=2.5, name='merit') # We denote by x the (variable) "merit" feature -x = studentsdata.loc[:, "merit"] +x = students_opt_data.loc[:, "merit"] # Make sure that studentsdata contains only the features column and in the right order -studentsdata = studentsdata.loc[:, features] +students_opt_data = students_opt_data.loc[:, features] m.update() # Let's look at our features dataframe for the optimization -studentsdata[:10] +students_opt_data[:10] ``` We add the objective and the budget constraint: @@ -197,23 +204,23 @@ With the `print_stats` function we display what was added to the model. ```{code-cell} ipython3 pred_constr = add_predictor_constr( - m, pipe, studentsdata, y, output_type="probability_1" + m, pipe, students_opt_data, y, output_type="probability_1" ) pred_constr.print_stats() ``` We can now optimize the problem. +With Gurobi ≥ 11.0, the attribute `FuncNonLinear` is automatically set to 1 by Gurobi machine learning on the nonlinear constraints it adds +in order to deal algorithmically with the logistic function. + +Older versions of Gurobi would make a piece-wise linear approximation of the logistic function. You can refer to [older versions +of this documentation](https://gurobi-machinelearning.readthedocs.io/en/v1.3.0/mlm-examples/student_admission.html) for dealing with those approximations. ```{code-cell} ipython3 m.optimize() ``` -Remember that for the logistic regression, Gurobi does a piecewise-linear -approximation of the logistic function. We can therefore get some significant -errors when comparing the results of the Gurobi model with what is predicted by -the regression. - We print the error using [get_error](../api/AbstractPredictorConstr.rst#gurobi_ml.modeling.base_predictor_constr.AbstractPredictorConstr.get_error) (note that we take the maximal error over all input vectors). @@ -225,49 +232,7 @@ print( ) ``` -The error we get might be considered too large, but we can use Gurobi parameters -to tune the piecewise-linear approximation made by Gurobi (at the expense of a -harder models). - -The specific parameters are explained in the documentation of [Functions -Constraints](https://www.gurobi.com/documentation/9.1/refman/constraints.html#subsubsection:GenConstrFunction) -in Gurobi's manual. - -We can pass those parameters to the -[add_predictor_constr](../api/AbstractPredictorConstr.rst#gurobi_ml.add_predictor_constr) -function in the form of a dictionary with the keyword parameter -`pwd_attributes`. - -Now we want a more precise solution, so we remove the current constraint, add a -new one that does a tighter approximation and resolve the model. - -```{code-cell} ipython3 -pred_constr.remove() - -pwl_attributes = { - "FuncPieces": -1, - "FuncPieceLength": 0.01, - "FuncPieceError": 1e-5, - "FuncPieceRatio": -1.0, -} -pred_constr = add_predictor_constr( - m, pipe, studentsdata, y, output_type="probability_1", pwl_attributes=pwl_attributes -) - -m.optimize() -``` - -We can see that the error has been reduced. - -```{code-cell} ipython3 -print( - "Maximum error in approximating the regression {:.6}".format( - np.max(pred_constr.get_error()) - ) -) -``` - -Finally note that we can directly get the input values for the regression in a solution as a pandas dataframe using input_values. +Finally, note that we can directly get the input values for the regression in a solution as a pandas dataframe using input_values. ```{code-cell} ipython3 pred_constr.input_values diff --git a/docs/requirements.txt b/docs/requirements.txt index c9710275..d51fce5a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -gurobipy==10.0.3 +gurobipy==11.0.0beta1 matplotlib-inline==0.1.6 matplotlib==3.6.0 sphinxcontrib_bibtex==2.5.0 diff --git a/docs/source/mlm-mip-models.rst b/docs/source/mlm-mip-models.rst index a3594852..81bb3305 100644 --- a/docs/source/mlm-mip-models.rst +++ b/docs/source/mlm-mip-models.rst @@ -47,7 +47,11 @@ First an intermediate free variable :math:`\omega = \sum_{i=1}^p \beta_i x_i + \beta_0` is created, and then we can express :math:`y = f(\omega)` using the general constraint. -Internally, Gurobi then makes a piecewise linear approximation of the logistic +With version 11, Gurobi introduced direct algorithmic support of nonlinear functions. +We enable it by setting the attribute `FuncNonLinear` to 1 for the logistic functions +created by Gurobi Machine Learning. + +Older versions of Gurobi make a piecewise linear approximation of the logistic function. By default, the approximation guarantees a maximal error of :math:`10^{-2}`. Those parameters can be tuned by setting the `pwl_attributes` keyword argument when the constraints is added (see diff --git a/requirements.tox.txt b/requirements.tox.txt index f5856fe3..5999090d 100644 --- a/requirements.tox.txt +++ b/requirements.tox.txt @@ -1,3 +1,3 @@ -gurobipy==10.0.3 +gurobipy==11.0.0beta1 numpy==1.26.0 scipy==1.11.3 diff --git a/requirements.txt b/requirements.txt index faa26b69..cd08364b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -gurobipy==10.0.3 +gurobipy==11.0.0beta1 numpy==1.26.0 pandas==2.1.1 scikit-learn==1.3.1 diff --git a/src/gurobi_ml/sklearn/logistic_regression.py b/src/gurobi_ml/sklearn/logistic_regression.py index 847c2529..dcdf4477 100644 --- a/src/gurobi_ml/sklearn/logistic_regression.py +++ b/src/gurobi_ml/sklearn/logistic_regression.py @@ -18,6 +18,8 @@ :gurobipy:`model`. """ +import warnings + import gurobipy as gp import numpy as np @@ -40,10 +42,16 @@ def add_logistic_regression_constr( The formulation predicts the values of output_vars using input_vars according to logistic_regression. - Note that the model uses a piecewise linear approximation of the logistic function. + For users of Gurobi ≥ 11.0, the attribute FuncNonLinear is set to 1 to + deal directly with the logistic function in an algorithmic fashion. + + For older versions, Gurobi makes a piecewise linear approximation of the logistic + function. The quality of the approximation can be controlled with the parameter pwl_attributes. By default, it is parametrized so that the maximal error of the - approximation is `1e-2`. See our :ref:`Users Guide ` for + approximation is `1e-2`. + + See our :ref:`Users Guide ` for details on the mip formulation used. Parameters @@ -166,15 +174,21 @@ def default_pwl_attributes(): """Default attributes for approximating the logistic function with Gurobi. See `Gurobi's User Manual - `_ + `_ for the meaning of the attributes. """ - return { - "FuncPieces": -1, - "FuncPieceLength": 0.01, - "FuncPieceError": 0.01, - "FuncPieceRatio": -1.0, - } + if gp.gurobi.version()[0] < 11: + message = """ +Gurobi ≥ 11 can deal directly with nonlinear functions with 'FuncNonlinear'. +Upgrading to version 11 is recommended when using logistic regressions.""" + warnings.warn(message) + return { + "FuncPieces": -1, + "FuncPieceLength": 0.01, + "FuncPieceError": 0.01, + "FuncPieceRatio": -1.0, + } + return {"FuncNonlinear": 1} def _mip_model(self, **kwargs): """Add the prediction constraints to Gurobi.""" From 3cc0fe7016989c1a47a0999a1a6bf88788192230 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 24 Oct 2023 13:50:36 +0200 Subject: [PATCH 2/9] Need to convert dict_values to list to create MVar --- src/gurobi_ml/modeling/_var_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gurobi_ml/modeling/_var_utils.py b/src/gurobi_ml/modeling/_var_utils.py index 35a1136f..37ef2720 100644 --- a/src/gurobi_ml/modeling/_var_utils.py +++ b/src/gurobi_ml/modeling/_var_utils.py @@ -184,7 +184,7 @@ def validate_output_vars(gp_vars): return gp_vars raise ParameterError("Variables should be an MVar of dimension 1 or 2") if isinstance(gp_vars, dict): - gp_vars = gp_vars.values() + gp_vars = list(gp_vars.values()) if isinstance(gp_vars, list): return gp.MVar.fromlist(gp_vars) if isinstance(gp_vars, gp.Var): @@ -220,7 +220,7 @@ def validate_input_vars(model, gp_vars): return (gp_vars, None, None) raise ParameterError("Variables should be an MVar of dimension 1 or 2") if isinstance(gp_vars, dict): - gp_vars = gp_vars.values() + gp_vars = list(gp_vars.values()) if isinstance(gp_vars, list): return (gp.MVar.fromlist(gp_vars).reshape(1, -1), None, None) if isinstance(gp_vars, gp.Var): From d75b706f4de059362a6fffde8f960609275d6374 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 24 Oct 2023 13:51:06 +0200 Subject: [PATCH 3/9] Convert to float in assertion --- tests/test_sklearn/test_sklearn_formulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sklearn/test_sklearn_formulations.py b/tests/test_sklearn/test_sklearn_formulations.py index e1589cc5..47876d73 100644 --- a/tests/test_sklearn/test_sklearn_formulations.py +++ b/tests/test_sklearn/test_sklearn_formulations.py @@ -38,7 +38,7 @@ def additional_test(self, predictor, pred_constr): ) self.assertLessEqual( np.max(pred_constr[i].get_error().astype(float)), - np.max(pred_constr.get_error()), + np.max(pred_constr.get_error().astype(float) + 1e-10), ) def test_diabetes_sklearn(self): From 810a5706a747bf7ead6e1a5801964da7817d6665 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 24 Oct 2023 14:58:21 +0200 Subject: [PATCH 4/9] Fix notebook Remove call to pip --- docs/notebooks/ipynb/student_admission.ipynb | 10 ---------- docs/notebooks/myst/student_admission.md | 4 ---- 2 files changed, 14 deletions(-) diff --git a/docs/notebooks/ipynb/student_admission.ipynb b/docs/notebooks/ipynb/student_admission.ipynb index 22befc45..28257d00 100644 --- a/docs/notebooks/ipynb/student_admission.ipynb +++ b/docs/notebooks/ipynb/student_admission.ipynb @@ -77,16 +77,6 @@ "LogisticRegression." ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "44e78a62", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install -U ../../.." - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/docs/notebooks/myst/student_admission.md b/docs/notebooks/myst/student_admission.md index 458f5632..91bda250 100644 --- a/docs/notebooks/myst/student_admission.md +++ b/docs/notebooks/myst/student_admission.md @@ -83,10 +83,6 @@ We import the necessary packages. Besides the usual (`numpy`, `gurobipy`, `pandas`), for this we will use Scikit-learn's Pipeline, StandardScaler and LogisticRegression. -```{code-cell} ipython3 -!pip install -U ../../.. -``` - ```{code-cell} ipython3 import numpy as np import pandas as pd From a49dbbc9012c9b7fd06486ecf4052cf873432164 Mon Sep 17 00:00:00 2001 From: Thomas Braam Date: Tue, 24 Oct 2023 16:04:40 +0200 Subject: [PATCH 5/9] Test with both gurobi 10.0.3 and 11.0.0 --- docs/source/conf.py | 3 ++- requirements.tox.txt | 1 - tox.ini | 25 +++++++++++++++---------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index cb17f45e..196288e3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -53,7 +53,8 @@ def get_versions(file: Path, acc=None): root_path = Path().resolve().parent.parent -dep_versions = get_versions(root_path / "requirements.tox.txt") +dep_versions = {k: v for k, v in get_versions(root_path / "requirements.txt").items() if k == 'gurobipy'} # get only gurobipy from requirements.txt +dep_versions = get_versions(root_path / "requirements.tox.txt", dep_versions) dep_versions = get_versions(root_path / "requirements.keras.txt", dep_versions) dep_versions = get_versions(root_path / "requirements.pytorch.txt", dep_versions) dep_versions = get_versions(root_path / "requirements.sklearn.txt", dep_versions) diff --git a/requirements.tox.txt b/requirements.tox.txt index a3357c57..5d79dabe 100644 --- a/requirements.tox.txt +++ b/requirements.tox.txt @@ -1,3 +1,2 @@ -gurobipy==11.0.0beta1 numpy==1.26.1 scipy==1.11.3 diff --git a/tox.ini b/tox.ini index 0a03ce6e..aa787a3a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,14 +4,14 @@ # and then run "tox" from this directory. [tox] -envlist = {py39,py310,py311}-{keras,pytorch,sklearn,xgboost,no_deps,all_deps},pre-commit,docs,examples +envlist = {py39,py310,py311}-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11},pre-commit,docs,examples-{gurobi10,gurobi11} isolated_build = True [gh-actions] python = - 3.9: pre-commit,py39-{keras,pytorch,sklearn,xgboost,no_deps,all_deps} - 3.10: py310-{keras,pytorch,sklearn,xgboost,no_deps,all_deps} - 3.11: docs,py311-{keras,pytorch,sklearn,xgboost,no_deps,all_deps} + 3.9: pre-commit,py39-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} + 3.10: py310-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} + 3.11: docs,py311-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} [testenv:docs] deps= @@ -21,7 +21,7 @@ allowlist_externals = /usr/bin/make commands = make html -[testenv:examples] +[testenv:examples-{gurobi10,gurobi11}] deps = -r{toxinidir}/requirements.tox.txt -r{toxinidir}/requirements.keras.txt @@ -33,6 +33,9 @@ deps = matplotlib ipywidgets seaborn + gurobi10: gurobipy==10.0.3 + gurobi11: gurobipy==11.0.0beta1 + changedir = {toxinidir}/notebooks allowlist_externals = /usr/bin/make commands = @@ -68,29 +71,31 @@ deps = pytest-subtests pytest-cov joblib + gurobi10: gurobipy==10.0.3 + gurobi11: gurobipy==11.0.0beta1 -[testenv:{py39,py310,py311}-keras] +[testenv:{py39,py310,py311}-keras-{gurobi10,gurobi11}] deps = {[base]deps} -r{toxinidir}/requirements.keras.txt commands = pytest tests/test_keras -[testenv:{py39,py310,py311}-pytorch] +[testenv:{py39,py310,py311}-pytorch-{gurobi10,gurobi11}] deps = {[base]deps} -r{toxinidir}/requirements.pytorch.txt commands = pytest tests/test_pytorch -[testenv:{py39,py310,py311}-sklearn] +[testenv:{py39,py310,py311}-sklearn-{gurobi10,gurobi11}] deps = {[base]deps} -r{toxinidir}/requirements.sklearn.txt commands = pytest tests/test_sklearn -[testenv:{py39,py310,py311}-xgboost] +[testenv:{py39,py310,py311}-xgboost-{gurobi10,gurobi11}] deps = {[base]deps} -r{toxinidir}/requirements.sklearn.txt @@ -104,7 +109,7 @@ deps = commands = pytest tests/test_no_deps -[testenv:{py39,py310,py311}-all_deps] +[testenv:{py39,py310,py311}-all_deps-{gurobi10,gurobi11}] deps = {[base]deps} -r{toxinidir}/requirements.keras.txt From 4fbb77e435e8cdfbed7391fd0224724277ac4bfe Mon Sep 17 00:00:00 2001 From: Thomas Braam Date: Tue, 24 Oct 2023 16:37:07 +0200 Subject: [PATCH 6/9] Black --- docs/source/conf.py | 6 +++++- tests/test_sklearn/sklearn_cases.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 196288e3..c8546241 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -53,7 +53,11 @@ def get_versions(file: Path, acc=None): root_path = Path().resolve().parent.parent -dep_versions = {k: v for k, v in get_versions(root_path / "requirements.txt").items() if k == 'gurobipy'} # get only gurobipy from requirements.txt +dep_versions = { + k: v + for k, v in get_versions(root_path / "requirements.txt").items() + if k == "gurobipy" +} # get only gurobipy from requirements.txt dep_versions = get_versions(root_path / "requirements.tox.txt", dep_versions) dep_versions = get_versions(root_path / "requirements.keras.txt", dep_versions) dep_versions = get_versions(root_path / "requirements.pytorch.txt", dep_versions) diff --git a/tests/test_sklearn/sklearn_cases.py b/tests/test_sklearn/sklearn_cases.py index 15189e0f..811eea18 100644 --- a/tests/test_sklearn/sklearn_cases.py +++ b/tests/test_sklearn/sklearn_cases.py @@ -293,7 +293,8 @@ def load_data(self): class WageCase(Cases): """Wage case - We use it for testing column_transformer and OneHotEncoding of fixed categorical features.""" + We use it for testing column_transformer and OneHotEncoding of fixed categorical features. + """ def __init__(self): self.categorical_features = ["OCCUPATION", "SECTOR"] From 06851dc8dc4bb0d346de1e5b58e3a19d0d137db3 Mon Sep 17 00:00:00 2001 From: Thomas Braam Date: Tue, 24 Oct 2023 17:07:56 +0200 Subject: [PATCH 7/9] Safely grab versions from requirements.txt --- docs/source/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c8546241..a29c47b1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,7 +48,14 @@ def get_versions(file: Path, acc=None): if acc is None: acc = dict() - new_dict = {x.split("==")[0]: x.split("==")[1] for x in file.read_text().split()} + new_dict = {} + for line in file.read_text().splitlines(): + try: + package, version = line.split("==") + new_dict[package] = version + except ValueError: + pass # Skip lines that don't split into exactly two items + return {**new_dict, **acc} From 2065770e4db4693cec7722311ec38c8f6d9d168f Mon Sep 17 00:00:00 2001 From: Thomas Braam Date: Tue, 24 Oct 2023 17:36:11 +0200 Subject: [PATCH 8/9] Reduce tox testset --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index aa787a3a..dce8739d 100644 --- a/tox.ini +++ b/tox.ini @@ -9,8 +9,8 @@ isolated_build = True [gh-actions] python = - 3.9: pre-commit,py39-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} - 3.10: py310-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} + 3.9: pre-commit,py39-all_deps-gurobi11 + 3.10: py310-all_deps-gurobi11 3.11: docs,py311-{keras,pytorch,sklearn,xgboost,no_deps,all_deps}-{gurobi10,gurobi11} [testenv:docs] From 53edf782befff97ca05513bbdd8c69884ae8b201 Mon Sep 17 00:00:00 2001 From: Pierre Bonami Date: Tue, 24 Oct 2023 17:43:31 +0200 Subject: [PATCH 9/9] Test everything in the all_deps target --- tox.ini | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index dce8739d..2f93d07d 100644 --- a/tox.ini +++ b/tox.ini @@ -116,11 +116,13 @@ deps = -r{toxinidir}/requirements.pytorch.txt -r{toxinidir}/requirements.sklearn.txt -r{toxinidir}/requirements.pandas.txt + -r{toxinidir}/requirements.xgboost.txt commands = - pytest tests/test_sklearn/test_sklearn_exceptions.py \ - tests/test_keras/test_keras_exceptions.py \ - tests/test_pytorch/test_pytorch_exceptions.py \ - tests/test_pandas/test_pandas_formulations.py + pytest tests/test_sklearn \ + tests/test_keras \ + tests/test_xgboost \ + tests/test_pytorch \ + tests/test_pandas [testenv] setenv =