From 27910da0bbbf470c016ccb480602d8cbafb8e06b Mon Sep 17 00:00:00 2001 From: Franco Peschiera Date: Sun, 5 Jul 2020 00:21:39 +0200 Subject: [PATCH] updated contribution file and docs. (#316) * updated contribution file and docs. * added more docstrings to documentation. --- .github/CONTRIBUTING.md | 11 +- INSTALL | 34 ++-- README.rst | 41 ++-- .../CaseStudies/a_transportation_problem.rst | 20 +- doc/source/_templates/layout.html | 9 - doc/source/conf.py | 7 +- .../guides/how_to_configure_solvers.rst | 64 ++++++ doc/source/guides/how_to_export_models.rst | 185 ++++++++++++++++++ doc/source/guides/how_to_mip_start.rst | 4 +- doc/source/guides/index.rst | 1 + doc/source/index.rst | 9 - doc/source/main/includeme.rst | 1 + doc/source/main/index.rst | 5 +- doc/source/technical/constants.rst | 66 +++---- doc/source/technical/pulp.rst | 10 +- .../Two_stage_Stochastic_GemstoneTools.py | 1 - pulp/apis/__init__.py | 31 +++ pulp/apis/xpress_api.py | 14 +- pulp/constants.py | 10 +- pulp/pulp.py | 146 ++++++++++---- pulp/tests/test_pulp.py | 8 +- 21 files changed, 514 insertions(+), 163 deletions(-) delete mode 100644 doc/source/_templates/layout.html create mode 100644 doc/source/guides/how_to_export_models.rst create mode 100644 doc/source/main/includeme.rst diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 23202303..141bfbd5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,6 +2,11 @@ When contributing to this repository, please first discuss the change you wish to make with the owners of this repository by creating an issue before making a change. +## Contributor License Agreement + +This project belongs to the [COIN-OR](coinor) community and thus follows its guidelines, listed [here](coinor_guidelines). +In order to sign the contribution agreement, we use [cla-assistant](_cla_assistant). When a new PR is created, a link will appear that asks the submitter to sign it virtually, in case the github user has not done so already. The only requisite is to have a github account. The link to pulp's CLA is [here](cla). + ## Pull Request Process 1. Create a Fork of the project to your own repository. @@ -16,7 +21,7 @@ When contributing to this repository, please first discuss the change you wish t ## Want to contribute but do not know where to start? -Check the [Roadmap][roadmap] for the project to see what you can help with. We're always looking for more examples and better documentation. +Check the [Roadmap][roadmap] for the project to see what you can help with. We're always looking for more examples and better documentation. Or check the issues that have the "help wanted" or "bug" tag. ### Attribution @@ -26,3 +31,7 @@ available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ [roadmap]: https://github.com/coin-or/pulp/projects/1 +[coinor]: https://www.coin-or.org/ +[coinor_guidelines]: https://www.coin-or.org/contributing/code/ +[cla]: https://cla-assistant.io/coin-or/pulp +[_cla_assistant]: https://github.com/cla-assistant/cla-assistant diff --git a/INSTALL b/INSTALL index d110ca6a..a4425357 100644 --- a/INSTALL +++ b/INSTALL @@ -4,32 +4,27 @@ Installation Note that to install PuLP you must first have a working python installation as described in `installing python`_. -PuLP requires Python >= 2.7. +PuLP requires Python >= 2.7 or Python >= 3.4. The latest version of PuLP can be freely obtained from github_. -Please note that this version of PuLP has not been tested with operating systems -other than Microsoft Windows and Ubuntu Linux. Pip and pypi installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -By far the easiest way to install pulp is through the use of pip_ and -CheeseShop_. +By far the easiest way to install pulp is through the use of pip_. -* Install pip - * In windows (please make sure pip is on your path):: +* In windows (please make sure pip is on your path):: - c:\Python34\Scripts\> pip install pulp + c:\Python34\Scripts\> pip install pulp - * In Linux:: +* In Linux:: - $ sudo pip install pulp - $ sudo pulptest #needed to get the default solver to work + $ sudo pip install pulp + $ sudo pulptest #needed to get the default solver to work * Then follow the instructions below to test your installation -To access the examples and pulp source code use the instructions below -to install from source +To access the examples and pulp source code use the instructions below to install from source. Windows installation from source @@ -37,7 +32,8 @@ Windows installation from source * Install python (`installing python`_) * Download the `PuLP zipfile`_ -* Extract the zipfile to a suitable location (such as the desktop - the folder will be no longer required after installation) +* Extract the zipfile to a suitable location (such as the desktop: the folder will be no longer required after installation) + * Open a command prompt by clicking "Run" in the Start Menu, and type 'cmd' in the window and push enter. * Navigate to the extracted folder with the setup file in it. [Do this by typing 'cd foldername' at the prompt, where 'cd' stands for current directory and the 'foldername' is the name of the folder to open in the path already listed to the left of the prompt. To return back to a root drive, type 'cd C:\'] * Type 'setup.py install' at the command prompt. This will install all the PuLP functions into Python's site-packages directory. @@ -46,9 +42,7 @@ The PuLP function library is now able to be imported from any python command lin >>> from pulp import * -to load in the functions. (You need to re-import the functions each time after -you close the GUI) PuLP is written in a programming language called Python, and -to use PuLP you must write Python code to describe your optimization problem. +to load in the functions. (You need to re-import the functions each time after you close the GUI) PuLP is written in a programming language called Python, and to use PuLP you must write Python code to describe your optimization problem. Linux Installation ~~~~~~~~~~~~~~~~~~ @@ -62,6 +56,7 @@ Linux Installation $ sudo python setup.py install * install a solver for pulp to use either + * use the included 64 or 32-bit binaries cbc-32 and cbc-64 * install glpk_ debain based distributions may use the following @@ -123,8 +118,7 @@ Solver pulp.solvers.PYGLPK unavailable Solver pulp.solvers.YAPOSIB unavailable .. _`installing python`: http://www.diveintopython.org/installing_python/index.html -.. _github: https://github.com/stumitchell/pulp-or +.. _github: https://github.com/coin-or/pulp-or .. _pip: https://pypi.python.org/pypi/pip -.. _CheeseShop: http://pypi.python.org -.. _`PuLP zipfile`: https://github.com/stumitchell/pulp-or/archive/master.zip +.. _`PuLP zipfile`: https://github.com/coi-nor/pulp-or/archive/master.zip diff --git a/README.rst b/README.rst index 05b524d3..9bc3a287 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ pulp :target: https://travis-ci.org/coin-or/pulp PuLP is an LP modeler written in Python. PuLP can generate MPS or LP files -and call GLPK[1], COIN-OR CLP/CBC[2], CPLEX[3], and GUROBI[4] to solve linear +and call GLPK_, COIN-OR CLP/`CBC`_, CPLEX_, GUROBI_, MOSEK_, XPRESS_, CHOCO_, MIPCL_, SCIP_ to solve linear problems. Installation @@ -17,6 +17,13 @@ If pip is available on your system:: pip install pulp Otherwise follow the download instructions on the PyPi page. + + +If you want to install the latest version from github you can run the following:: + + pip install -U git+https://github.com/coin-or/pulp + + On Linux and OSX systems the tests must be run to make the default solver executable. @@ -81,19 +88,19 @@ You can get the value of the variables using value(). ex:: Exported Classes: -* LpProblem -- Container class for a Linear programming problem -* LpVariable -- Variables that are added to constraints in the LP -* LpConstraint -- A constraint of the general form +* `LpProblem` -- Container class for a Linear programming problem +* `LpVariable` -- Variables that are added to constraints in the LP +* `LpConstraint` -- A constraint of the general form a1x1+a2x2 ...anxn (<=, =, >=) b -* LpConstraintVar -- Used to construct a column of the model in column-wise modelling +* `LpConstraintVar` -- Used to construct a column of the model in column-wise modelling Exported Functions: -* value() -- Finds the value of a variable or expression -* lpSum() -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct a linear expression to be used as a constraint or variable -* lpDot() --given two lists of the form [a1, a2, ..., an] and [ x1, x2, ..., xn] will construct a linear epression to be used as a constraint or variable +* `value()` -- Finds the value of a variable or expression +* `lpSum()` -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct a linear expression to be used as a constraint or variable +* `lpDot()` --given two lists of the form [a1, a2, ..., an] and [ x1, x2, ..., xn] will construct a linear epression to be used as a constraint or variable Comments, bug reports, patches and suggestions are welcome. pulp-or-discuss@googlegroups.com @@ -102,10 +109,14 @@ pulp-or-discuss@googlegroups.com Copyright Stuart A. Mitchell (stu@stuartmitchell.com) See the LICENSE file for copyright information. -References: - -[1] http://www.gnu.org/software/glpk/glpk.html -[2] https://github.com/coin-or/Cbc -[3] http://www.cplex.com/ -[4] http://www.gurobi.com/ - +.. _Python: http://www.python.org/ + +.. _GLPK: http://www.gnu.org/software/glpk/glpk.html +.. _CBC: https://github.com/coin-or/Cbc +.. _CPLEX: http://www.cplex.com/ +.. _GUROBI: http://www.gurobi.com/ +.. _MOSEK: https://www.mosek.com/ +.. _XPRESS: https://www.fico.com/es/products/fico-xpress-solver +.. _CHOCO: https://choco-solver.org/ +.. _MIPCL: http://mipcl-cpp.appspot.com/ +.. _SCIP: https://www.scipopt.org/ diff --git a/doc/source/CaseStudies/a_transportation_problem.rst b/doc/source/CaseStudies/a_transportation_problem.rst index df95a80d..d3a4ca16 100644 --- a/doc/source/CaseStudies/a_transportation_problem.rst +++ b/doc/source/CaseStudies/a_transportation_problem.rst @@ -140,12 +140,12 @@ problem is found in the examples directory BeerDistributionProblem.py First, start your Python file with a heading and the import PuLP statement: .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 1:8 + :lines: 1-8 The start of the formulation is a simple definition of the nodes and their limits/capacities. The node names are put into lists, and their associated capacities are put into dictionaries with the node names as the reference keys: .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 10:25 + :lines: 10-25 The cost data is then inputted into a list, with two sub lists: the first containing the costs of shipping from Warehouse A, and the second containing the @@ -160,18 +160,18 @@ warehouse A to bar 1, 2. If `costs["C"]["2"]` is called, it will return 0, since this is the defined default. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 27:35 + :lines: 27-35 The `prob` variable is created using the `LpProblem` function, with the usual input parameters. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 37:38 + :lines: 37-38 A list of tuples is created containing all the arcs. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 40:41 + :lines: 40-41 A dictionary called `route_var` is created which contains the LP variables. The reference keys to the dictionary are the warehouse name, then the bar @@ -180,7 +180,7 @@ Route_A_2). The lower limit of zero is set, the upper limit of `None` is set, and the variables are defined to be Integers. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 43:44 + :lines: 43-44 The objective function is added to the variable `prob` using a list comprehension. Since `route_vars` and `costs` are now dictionaries (with further @@ -189,7 +189,7 @@ in Routes` will cycle through all the combinations/arcs. Note that `i` and `j` could have been used, but `w` and `b` are more meaningful. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 46:47 + :lines: 46-47 The supply and demand constraints are added using a normal `for` loop and a list comprehension. Supply Constraints: For each warehouse in turn, the values of the @@ -200,7 +200,7 @@ variables (number on arc) from each of the warehouses is summed, and then constrained to being greater than or equal to the demand minimum. .. literalinclude:: ../../../examples/BeerDistributionProblem.py - :lines: 49:55 + :lines: 49-55 Following this is the `prob.writeLP` line, and the rest as explained in previous examples. @@ -242,7 +242,7 @@ constraints all operated on the original supply, demand and cost lists/dictionaries, the only changes that must be made to include another demand node are: .. literalinclude:: ../../../examples/BeerDistributionProblemWarehouseExtension.py - :lines: 11:31 + :lines: 11-31 The `Bars` list is expanded and the `Demand` dictionary is expanded to make the @@ -271,7 +271,7 @@ list, `Supply` dictionary, and `costs` list. The Supply value is chosen to balance the problem, and cost of transport is zero to all demand nodes. .. literalinclude:: ../../../examples/BeerDistributionProblemCompetitorExtension.py - :lines:8:32 + :lines: 8-32 The code for this example is found in `BeerDistributionProblemCompetitorExtension.py `_ diff --git a/doc/source/_templates/layout.html b/doc/source/_templates/layout.html deleted file mode 100644 index d3085f2c..00000000 --- a/doc/source/_templates/layout.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends '!layout.html' %} - -{% block footer %} -{{ super() }} -
-Creative Commons License
PuLP documentation by Pulp documentation team is licensed under a Creative Commons Attribution-Share Alike 3.0 New Zealand License. -
-{% endblock %} - diff --git a/doc/source/conf.py b/doc/source/conf.py index f11ea565..7d045b16 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -104,14 +104,19 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme = 'default' -html_theme = 'alabaster' +# html_theme = 'sphinx-glpi-theme' +import sphinx_glpi_theme +html_theme = "glpi" + +html_theme_path = sphinx_glpi_theme.get_html_themes_path() # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} html_theme_options = { 'fixed_sidebar': True, + 'stickysidebar': True, 'github_banner': True, 'show_powered_by': False, 'github_user': 'coin-or', diff --git a/doc/source/guides/how_to_configure_solvers.rst b/doc/source/guides/how_to_configure_solvers.rst index 71d6d54f..b01a930b 100644 --- a/doc/source/guides/how_to_configure_solvers.rst +++ b/doc/source/guides/how_to_configure_solvers.rst @@ -3,6 +3,33 @@ How to configure a solver in PuLP A typical problem PuLP users have is trying to connect to a solver that is installed in their pc. Here, we show the main concepts and ways to be sure PuLP can talk to the solver in question. +Checking which solvers PuLP has acces to +------------------------------------------------ + +PuLP has some helper functions that permit a user to query which solvers are available and initialize a solver from its name. + +.. code-block:: python + + import pulp as pl + solver_list = pl.list_solvers() + # ['GLPK_CMD', 'PYGLPK', 'CPLEX_CMD', 'CPLEX_PY', 'CPLEX_DLL', 'GUROBI', 'GUROBI_CMD', 'MOSEK', 'XPRESS', 'PULP_CBC_CMD', 'COIN_CMD', 'COINMP_DLL', 'CHOCO_CMD', 'PULP_CHOCO_CMD', 'MIPCL_CMD', 'SCIP_CMD'] + +If passed the `only_available=True` argument, PuLP lists the solvers that are currently available:: + + import pulp as pl + solver_list = pl.list_solvers(available_only=True) + # ['GLPK_CMD', 'CPLEX_CMD', 'CPLEX_PY', 'GUROBI', 'GUROBI_CMD', 'PULP_CBC_CMD', 'COIN_CMD', 'PULP_CHOCO_CMD'] + +Also, it's possible to get a solver object by using the name of the solver. Any arguments passes to this function are passed to the constructor: + +.. code-block:: python + + import pulp as pl + solver = pl.get_solver('CPLEX_CMD') + solver = pl.get_solver('CPLEX_CMD', timeLimite=10) + +In the next sections, we will explain how to configure a solver to be accesible by pulp. + What is an environment variable -------------------------------------- @@ -208,3 +235,40 @@ Also, you can access the python api object before solving by using the lower-lev For more information on how to use the `solverModel`, one needs to check the official documentation depending on the solver. + +Importing and exporting a solver +----------------------------------- + +Exporting a solver can be useful to backup the configuration that was used to solve a model. + +In order to export it one needs can export it to a dictionary or a json file:: + + import pulp + solver = pulp.PULP_CBC_CMD() + solver_dict = solver.to_dict() + +The structure of the produce dictionary is quite simple:: + + {'keepFiles': 0, + 'mip': True, + 'msg': True, + 'options': [], + 'solver': 'PULP_CBC_CMD', + 'timeLimit': None, + 'warmStart': False} + +It's also possible to export it directly to a json file:: + + solver.to_json("some_file_name.json") + +In order to import it, one needs to do:: + + import pulp + solver = pulp.get_solver_from_dict(solver_dict) + +Or from a file:: + + import pulp + solver = pulp.get_solver_from_json("some_file_name.json") + +For json, we use the base `json` package. But if `ujson` is available, we use that so the import / export can be really fast. \ No newline at end of file diff --git a/doc/source/guides/how_to_export_models.rst b/doc/source/guides/how_to_export_models.rst new file mode 100644 index 00000000..ba55d9df --- /dev/null +++ b/doc/source/guides/how_to_export_models.rst @@ -0,0 +1,185 @@ +How to export models in PuLP +====================================== + +Warning! This is experimental. Use at your own risk. And write an issue if you see anything weird. + +Exporting a model can be useful when the building time takes too long or when the model needs to be passed to another computer to solve. Or many other reasons. +PuLP offers a way to export a model into a dictionary of a json file. The json file saves enough data to be able to rebuild a new model on reading it. + +Considerations +------------------ + +The following considerations need to be taken into account: + +#. Variable names need to be unique. PuLP permits having variable names because it uses an internal code for each one. But we do not export that code. So we identify variables by their name only. +#. Variables are not exported in a grouped way. This means that if you had several `dictionaries of many variables each` you will end up with a very long list of variables. This can be seen in the Example 2. +#. Output information is also written. This means that the status, solution status, the values of variables and shadow prices / reduced costs are exported too. This means that it is possible to export a model that has been solved and then read it again only to see the values of the variables. +#. For json, we use the base `json` package. But if `ujson` is available, we use that so the import / export can be really fast. + +Example 1 +---------------- + +A very simple example taken from the internal tests. Imagine the following problem:: + + from pulp import * + prob = LpProblem("test_export_dict_MIP", const.LpMinimize) + x = LpVariable("x", 0, 4) + y = LpVariable("y", -1, 1) + z = LpVariable("z", 0, None, const.LpInteger) + prob += x + 4 * y + 9 * z, "obj" + prob += x + y <= 5, "c1" + prob += x + z >= 10, "c2" + prob += -y + z == 7.5, "c3" + +We can now export the problem into a dictionary:: + + data = prob.to_dict() + +We now have a dictionary with a lot of data:: + + {'constraints': [{'coefficients': [{'name': 'x', 'value': 1}, + {'name': 'y', 'value': 1}], + 'constant': -5, + 'name': 'c1', + 'pi': None, + 'sense': -1}, + {'coefficients': [{'name': 'x', 'value': 1}, + {'name': 'z', 'value': 1}], + 'constant': -10, + 'name': 'c2', + 'pi': None, + 'sense': 1}, + {'coefficients': [{'name': 'y', 'value': -1}, + {'name': 'z', 'value': 1}], + 'constant': -7.5, + 'name': 'c3', + 'pi': None, + 'sense': 0}], + 'objective': {'coefficients': [{'name': 'x', 'value': 1}, + {'name': 'y', 'value': 4}, + {'name': 'z', 'value': 9}], + 'name': 'obj'}, + 'parameters': {'name': 'test_export_dict_MIP', + 'sense': 1, + 'sol_status': 0, + 'status': 0}, + 'sos1': {}, + 'sos2': {}, + 'variables': [{'cat': 'Continuous', + 'dj': None, + 'lowBound': 0, + 'name': 'x', + 'upBound': 4, + 'varValue': None}, + {'cat': 'Continuous', + 'dj': None, + 'lowBound': -1, + 'name': 'y', + 'upBound': 1, + 'varValue': None}, + {'cat': 'Integer', + 'dj': None, + 'lowBound': 0, + 'name': 'z', + 'upBound': None, + 'varValue': None}]} + + +We can now import this dictionary:: + + var1, prob1 = LpProblem.from_dict(data) + var1 + # {'x': x, 'y': y, 'z': z} + prob1 + # test_export_dict_MIP: + # MINIMIZE + # 1*x + 4*y + 9*z + 0 + # SUBJECT TO + # c1: x + y <= 5 + # c2: x + z >= 10 + # c3: - y + z = 7.5 + # VARIABLES + # x <= 4 Continuous + # -1 <= y <= 1 Continuous + # 0 <= z Integer + +As you can see we need get a tuple with a variables dictionary and a PuLP model object. +We can now solve that problem:: + + prob1.solve() + +And the result will be available in our *new* variables:: + + var1['x'].value() + # 3.0 + + +Example 2 +---------------- + +We will use as example the model in :ref:`set-partitioning-problem`:: + + import pulp + + max_tables = 5 + max_table_size = 4 + guests = 'A B C D E F G I J K L M N O P Q R'.split() + + def happiness(table): + """ + Find the happiness of the table + - by calculating the maximum distance between the letters + """ + return abs(ord(table[0]) - ord(table[-1])) + + #create list of all possible tables + possible_tables = [tuple(c) for c in pulp.allcombinations(guests, + max_table_size)] + + #create a binary variable to state that a table setting is used + x = pulp.LpVariable.dicts('table', possible_tables, + lowBound = 0, + upBound = 1, + cat = pulp.LpInteger) + + seating_model = pulp.LpProblem("Wedding_Seating_Model", pulp.LpMinimize) + + seating_model += sum([happiness(table) * x[table] for table in possible_tables]) + + #specify the maximum number of tables + seating_model += sum([x[table] for table in possible_tables]) <= max_tables, \ + "Maximum_number_of_tables" + + #A guest must seated at one and only one table + for guest in guests: + seating_model += sum([x[table] for table in possible_tables + if guest in table]) == 1, "Must_seat_%s"%guest + + +Right now, we could directly solve the model doing:: + + seating_model.solve() + +Instead, we are going to export it to a json file:: + + seating_model.to_json("seating_model.json") + +And re-import it:: + + wedding_vars, wedding_model = LpProblem.from_json("seating_model.json") + +We can inspect the variables:: + + wedding_vars + {"table_('A',)": table_('A',), "table_('A',_'B')": table_('A',_'B'), "table_('A',_'B',_'C')": table_('A',_'B',_'C'), "table_('A',_'B',_'C',_'D')": table_('A',_'B',_'C',_'D'), "table_('A',_'B',_'C',_'E')": table_('A',_'B',_'C',_'E'), ...} + +As can be seen, it is no longer a dictionary indexed by the original tuples. Sadly, it has become a dictionary of concatenated names. + +We can still solve the model though:: + + wedding_model.solve() + +And inspect some of the values:: + + wedding_vars["table_('M',_'N')"].value() + # 1.0 diff --git a/doc/source/guides/how_to_mip_start.rst b/doc/source/guides/how_to_mip_start.rst index 7473dc38..4cebcdc0 100644 --- a/doc/source/guides/how_to_mip_start.rst +++ b/doc/source/guides/how_to_mip_start.rst @@ -7,12 +7,12 @@ Many solvers permit the possibility of giving a valid (or parcially valid in som Supported solver APIs ----------------------- -The present solver APIs that work with PuLP mip-start are the following: ``CPLEX_CMD``, ``GUROBI_CMD``, ``PULP_CBC_CMD``, ``CBC_CMD``. +The present solver APIs that work with PuLP mip-start are the following: ``CPLEX_CMD``, ``GUROBI_CMD``, ``PULP_CBC_CMD``, ``CBC_CMD``. ``CPLEX_PY``. Example problem ---------------- -We will use as example the model in :ref:`set-partitioning-problem`. Below is the complete modified code. +We will use as example the model in :ref:`set-partitioning-problem`. At the end is the complete modified code. Filling a variable with a value diff --git a/doc/source/guides/index.rst b/doc/source/guides/index.rst index a6383721..44635442 100644 --- a/doc/source/guides/index.rst +++ b/doc/source/guides/index.rst @@ -8,3 +8,4 @@ how_to_configure_solvers how_to_mip_start how_to_elastic_constraints + how_to_export_models diff --git a/doc/source/index.rst b/doc/source/index.rst index 97366e4b..82487460 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -32,12 +32,3 @@ Authors The authors of this documentation (the pulp documentation team) include: .. include:: AUTHORS.txt - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/doc/source/main/includeme.rst b/doc/source/main/includeme.rst new file mode 100644 index 00000000..ec7f5245 --- /dev/null +++ b/doc/source/main/includeme.rst @@ -0,0 +1 @@ +.. include:: ../../../README.rst \ No newline at end of file diff --git a/doc/source/main/index.rst b/doc/source/main/index.rst index 2c2de030..979598fb 100644 --- a/doc/source/main/index.rst +++ b/doc/source/main/index.rst @@ -2,8 +2,6 @@ Main Topics ===================== - - .. toctree:: :maxdepth: 2 @@ -12,5 +10,4 @@ Main Topics basic_python_coding installing_pulp_at_home amply - - + README diff --git a/doc/source/technical/constants.rst b/doc/source/technical/constants.rst index 697ced7b..dd9fb257 100644 --- a/doc/source/technical/constants.rst +++ b/doc/source/technical/constants.rst @@ -46,43 +46,43 @@ LpStatusNotSolved = 0 - .. data:: LpStatusInfeasible - +.. data:: LpStatusInfeasible + LpStatusInfeasible = -1 - - .. data:: LpStatusUnbounded - + +.. data:: LpStatusUnbounded + LpStatusUnbounded = -2 - - .. data:: LpStatusUndefined - + +.. data:: LpStatusUndefined + LpStatusUndefined = -3 + +.. data:: LpSolution + +Return solution status from solver: + + +----------------------------------------+------------------------------+-----------------+ + | LpStatus key | string value | numerical value | + +========================================+==============================+=================+ + | :data:`LpSolutionOptimal` | "Optimal Solution Found" | 1 | + +----------------------------------------+------------------------------+-----------------+ + | :data:`LpSolutionNoSolutionFound` | "No Solution Found" | 0 | + +----------------------------------------+------------------------------+-----------------+ + | :data:`LpSolutionStatusInfeasible` |"No Solution Exists" | -1 | + +----------------------------------------+------------------------------+-----------------+ + | :data:`LpSolutionStatusUnbounded` | "Solution is Unbounded" | -2 | + +----------------------------------------+------------------------------+-----------------+ + | :data:`LpSolutionIntegerFeasible` | "Solution Found" | 2 | + +----------------------------------------+------------------------------+-----------------+ - .. data:: LpSolution - - Return solution status from solver: - - +-----------------------------+---------------+-----------------+ - | LpStatus key | string value | numerical value | - +=============================+===============+=================+ - | :data:`LpSolutionOptimal` | "Optimal Solution Found" | 1 | - +-----------------------------+---------------+-----------------+ - | :data:`LpSolutionNoSolutionFound` | "No Solution Found" | 0 | - +-----------------------------+---------------+-----------------+ - | :data:`LpSolutionStatusInfeasible` | "Infeasible" | -1 | - +-----------------------------+---------------+-----------------+ - | :data:`LpSolutionStatusUnbounded` | "Unbounded" | -2 | - +-----------------------------+---------------+-----------------+ - | :data:`LpSolutionIntegerFeasible` | "Integer Solution Found" | 2 | - +-----------------------------+---------------+-----------------+ - - - .. data:: LpSenses - - Dictionary of values for :attr:`~pulp.pulp.LpProblem.sense`: - - LpSenses = - {:data:`LpMaximize`:"Maximize", :data:`LpMinimize`:"Minimize"} + +.. data:: LpSenses + +Dictionary of values for :attr:`~pulp.pulp.LpProblem.sense`: + + LpSenses = + {:data:`LpMaximize`:"Maximize", :data:`LpMinimize`:"Minimize"} .. data:: LpMinimize diff --git a/doc/source/technical/pulp.rst b/doc/source/technical/pulp.rst index 95ee39fc..1b582c92 100644 --- a/doc/source/technical/pulp.rst +++ b/doc/source/technical/pulp.rst @@ -44,6 +44,9 @@ The LpProblem Class .. automethod:: roundSolution .. automethod:: setObjective .. automethod:: writeLP + .. automethod:: writeMPS + .. automethod:: to_json + .. automethod:: from_json Variables and Expressions ------------------------- @@ -68,7 +71,8 @@ integer. .. autoclass:: LpAffineExpression :show-inheritance: - + :members: + In brief, :math:`\textsf{LpAffineExpression([(x[i],a[i]) for i in I])} = \sum_{i \in I} a_i x_i` where (note the order): @@ -82,9 +86,11 @@ integer. Constraints ----------- + .. autoclass:: LpConstraint :show-inheritance: - :members: makeElasticSubProblem + :members: + .. autoclass:: FixedElasticSubProblem :show-inheritance: diff --git a/examples/Two_stage_Stochastic_GemstoneTools.py b/examples/Two_stage_Stochastic_GemstoneTools.py index 552297e7..7c60da89 100644 --- a/examples/Two_stage_Stochastic_GemstoneTools.py +++ b/examples/Two_stage_Stochastic_GemstoneTools.py @@ -101,7 +101,6 @@ for v in gemstoneprob.variables(): print(v.name, "=", v.varValue) production = [v.varValue for v in gemstoneprob.variables()] -production # The optimised objective function value is printed to the console print("Total price = ", pulp.value(gemstoneprob.objective)) diff --git a/pulp/apis/__init__.py b/pulp/apis/__init__.py index 23f18880..2ec1171c 100644 --- a/pulp/apis/__init__.py +++ b/pulp/apis/__init__.py @@ -67,6 +67,14 @@ def configSolvers(): def get_solver(solver, *args, **kwargs): + """ + Instantiates a solver from its name + + :param str solver: solver name to create + :param args: additional arguments to the solver + :param kwargs: additional keyword arguments to the solver + :return: solver of type :py:class:`LpSolver` + """ mapping = {k.name: k for k in _all_solvers} try: return mapping[solver](*args, **kwargs) @@ -78,6 +86,15 @@ def get_solver(solver, *args, **kwargs): def get_solver_from_dict(data): + """ + Instantiates a solver from a dictionary with its data + + :param dict data: a dictionary with, at least an "solver" key with the name + of the solver to create + :return: a solver of type :py:class:`LpSolver` + :raises PulpSolverError: if the dictionary does not have the "solver" key + :rtype: LpSolver + """ solver = data.pop('solver', None) if solver is None: raise PulpSolverError('The json file has no solver attribute.') @@ -85,12 +102,26 @@ def get_solver_from_dict(data): def get_solver_from_json(filename): + """ + Instantiates a solver from a json file with its data + + :param str filename: name of the json file to read + :return: a solver of type :py:class:`LpSolver` + :rtype: LpSolver + """ with open(filename, 'r') as f: data = json.load(f) return get_solver_from_dict(data) def list_solvers(onlyAvailable=False): + """ + List the names of all the existing solvers in PuLP + + :param bool onlyAvailable: if True, only show the available solvers + :return: list of solver names + :rtype: list + """ solvers = [s() for s in _all_solvers] if onlyAvailable: return [solver.name for solver in solvers if solver.available()] diff --git a/pulp/apis/xpress_api.py b/pulp/apis/xpress_api.py index 732b1cbb..c11a049b 100644 --- a/pulp/apis/xpress_api.py +++ b/pulp/apis/xpress_api.py @@ -38,14 +38,14 @@ def __init__(self, maxSeconds=None, targetGap=None, heurFreq=None, """ Initializes the Xpress solver. - @param maxSeconds: the maximum time that the Optimizer will run before it terminates - @param targetGap: global search will terminate if: + :param maxSeconds: the maximum time that the Optimizer will run before it terminates + :param targetGap: global search will terminate if: abs(MIPOBJVAL - BESTBOUND) <= MIPRELSTOP * BESTBOUND - @param heurFreq: the frequency at which heuristics are used in the tree search - @param heurStra: heuristic strategy - @param coverCuts: the number of rounds of lifted cover inequalities at the top node - @param preSolve: whether presolving should be performed before the main algorithm - @param options: Adding more options, e.g. options = ["NODESELECTION=1", "HEURDEPTH=5"] + :param heurFreq: the frequency at which heuristics are used in the tree search + :param heurStra: heuristic strategy + :param coverCuts: the number of rounds of lifted cover inequalities at the top node + :param preSolve: whether presolving should be performed before the main algorithm + :param options: Adding more options, e.g. options = ["NODESELECTION=1", "HEURDEPTH=5"] More about Xpress options and control parameters please see http://tomopt.com/docs/xpress/tomlab_xpress008.php """ diff --git a/pulp/constants.py b/pulp/constants.py index 8e4c4457..4dc3389d 100644 --- a/pulp/constants.py +++ b/pulp/constants.py @@ -27,7 +27,7 @@ This file contains the constant definitions for PuLP Note that hopefully these will be changed into something more pythonic """ -VERSION = '2.1' +VERSION = '2.2' EPS = 1e-7 # variable categories @@ -65,16 +65,16 @@ LpSolution = { LpSolutionNoSolutionFound: "No Solution Found", LpSolutionOptimal: "Optimal Solution Found", - LpSolutionIntegerFeasible: "Integer Solution Found", + LpSolutionIntegerFeasible: "Solution Found", LpSolutionInfeasible: "No Solution Exists", - LpSolutionUnbounded: "Unbounded" + LpSolutionUnbounded: "Solution is Unbounded" } LpStatusToSolution = { - LpStatusNotSolved: LpSolutionNoSolutionFound, + LpStatusNotSolved: LpSolutionInfeasible, LpStatusOptimal: LpSolutionOptimal, LpStatusInfeasible: LpSolutionInfeasible, LpStatusUnbounded: LpSolutionUnbounded, - LpStatusUndefined: LpSolutionNoSolutionFound, + LpStatusUndefined: LpSolutionInfeasible, } # constraint sense diff --git a/pulp/pulp.py b/pulp/pulp.py index a4b9e8ca..45d4d3e0 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -252,7 +252,9 @@ def __init__(self, name, lowBound = None, upBound = None, def to_dict(self): """ Exports a variable into a dictionary with its relevant information - :return: + + :return: a dictionary with the variable information + :rtype: dict """ return dict(lowBound=self.lowBound, upBound=self.upBound, cat=self.cat, varValue=self.varValue, dj=self.dj, name=self.name) @@ -262,10 +264,11 @@ def from_dict(cls, dj=None, varValue=None, **kwargs): """ Initializes a variable object from information that comes from a dictionary (kwargs) - :param dj: - :param varValue: - :param kwargs: - :return: + :param dj: shadow price of the variable + :param float varValue: the value to set the variable + :param kwargs: arguments to initialize the variable + :return: a :py:class:`LpVariable` + :rtype: :LpVariable """ var = cls(**kwargs) var.dj = dj @@ -296,10 +299,7 @@ def matrix(self, name, indexs, lowBound = None, upBound = None, cat = const.LpCo def dicts(self, name, indexs, lowBound = None, upBound = None, cat = const.LpContinuous, indexStart = []): """ - Creates a dictionary of LP variables - - This function creates a dictionary of LP Variables with the specified - associated parameters. + This function creates a dictionary of :py:class:`LpVariable` with the specified associated parameters. :param name: The prefix to the name of each LP variable created :param indexs: A list of strings of the keys to the dictionary of LP @@ -311,7 +311,7 @@ def dicts(self, name, indexs, lowBound = None, upBound = None, cat = const.LpCon :param cat: The category these variables are in, Integer or Continuous(default) - :return: A dictionary of LP Variables + :return: A dictionary of :py:class:`LpVariable` """ if not isinstance(indexs, tuple): indexs = (indexs,) if "%" not in name: name += "_%s" * len(indexs) @@ -421,10 +421,10 @@ def valueOrDefault(self): return 0 def valid(self, eps): - if self.varValue == None: return False - if self.upBound != None and self.varValue > self.upBound + eps: + if self.varValue is None: return False + if self.upBound is not None and self.varValue > self.upBound + eps: return False - if self.lowBound != None and self.varValue < self.lowBound - eps: + if self.lowBound is not None and self.varValue < self.lowBound - eps: return False if self.cat == const.LpInteger and abs(round(self.varValue) - self.varValue) > eps: return False @@ -447,13 +447,13 @@ def isInteger(self): return self.cat == const.LpInteger def isFree(self): - return self.lowBound == None and self.upBound == None + return self.lowBound is None and self.upBound is None def isConstant(self): - return self.lowBound != None and self.upBound == self.lowBound + return self.lowBound is not None and self.upBound == self.lowBound def isPositive(self): - return self.lowBound == 0 and self.upBound == None + return self.lowBound == 0 and self.upBound is None def asCplexLpVariable(self): if self.isFree(): return self.name + " free" @@ -467,7 +467,7 @@ def asCplexLpVariable(self): else: s= "%.12g <= " % self.lowBound s += self.name - if self.upBound != None: + if self.upBound is not None: s += " <= %.12g" % self.upBound return s @@ -493,9 +493,14 @@ def addVariableToConstraints(self,e): constraint.addVariable(self,coeff) def setInitialValue(self, val, check=True): - """sets the initial value of the Variable to val - may of may not be supported by the solver - if check is True: we confirm the value is really possible + """ + sets the initial value of the variable to `val` + May be used for warmStart a solver, if supported by the solver + + :param float val: value to set to variable + :param bool check: if True, we check if the value fits inside the variable bounds + :return: True if the value was set + :raises ValueError: if check=True and the value does not fit inside the bounds """ lb = self.lowBound ub = self.upBound @@ -514,7 +519,7 @@ def setInitialValue(self, val, check=True): def fixValue(self): """ changes lower bound and upper bound to the initial value if exists. - :return: + :return: None """ self._lowbound_unfix = self.lowBound self._upbound_unfix = self.upBound @@ -523,11 +528,17 @@ def fixValue(self): self.bounds(val, val) def isFixed(self): - return self.upBound == self.lowBound + """ + + :return: True if upBound and lowBound are the same + :rtype: bool + """ + return self.isConstant() def unfixValue(self): self.bounds(self._lowbound_original, self._upbound_original) + class LpAffineExpression(_DICT_TYPE): """ A linear combination of :class:`LpVariables`. @@ -872,6 +883,13 @@ def __eq__(self, other): return LpConstraint(self - other, const.LpConstraintEQ) def to_dict(self): + """ + exports the :py:class:`LpAffineExpression` into a list of dictionaries with the coefficients + it does not export the constant + + :return: list of dictionaries with the coefficients + :rtype: list + """ return [dict(name=k.name, value=v) for k, v in self.items()] @@ -1054,7 +1072,8 @@ def makeElasticSubProblem(self, *args, **kwargs): def to_dict(self): """ exports constraint information into a dictionary - :return: + + :return: dictionary with all the constraint information """ return dict(sense=self.sense, pi=self.pi, @@ -1066,8 +1085,9 @@ def to_dict(self): def from_dict(cls, _dict): """ Initializes a constraint object from a dictionary with necessary information - :param _dict: dictionary with data - :return: + + :param dict _dict: dictionary with data + :return: a new :py:class:`LpConstraint` """ const = cls(e=_dict['coefficients'], rhs=-_dict['constant'], name=_dict['name'], sense=_dict['sense']) const.pi = _dict['pi'] @@ -1186,7 +1206,6 @@ def __init__(self, name = "NoName", sense = const.LpMinimize): self._variable_ids = {} #old school using dict.keys() for a set self.dummyVar = None - # locals self.lastUnused = 0 @@ -1248,6 +1267,7 @@ def to_dict(self): So it requires to have unique names for variables. :return: dictionary with model data + :rtype: dict """ try: self.checkDuplicateVars() @@ -1255,16 +1275,17 @@ def to_dict(self): raise const.PulpError("Duplicated names found in variables:\nto export the model, variable names need to be unique") variables = self.variables() return \ - dict(objective=dict(name=self.objective.name, coefficients=self.objective.to_dict()), - constraints=[v.to_dict() for v in self.constraints.values()], - variables=[v.to_dict() for v in variables], - parameters=dict(name=self.name, - sense=self.sense, - status=self.status, - sol_status=self.sol_status), - sos1=self.sos1, - sos2=self.sos2 - ) + dict( + objective=dict(name=self.objective.name, coefficients=self.objective.to_dict()), + constraints=[v.to_dict() for v in self.constraints.values()], + variables=[v.to_dict() for v in variables], + parameters=dict(name=self.name, + sense=self.sense, + status=self.status, + sol_status=self.sol_status), + sos1=self.sos1, + sos2=self.sos2 + ) @classmethod def from_dict(cls, _dict): @@ -1273,7 +1294,7 @@ def from_dict(cls, _dict): And returns a dictionary of variables and a problem object :param _dict: dictionary with the model stored - :return: + :return: a tuple with a dictionary of variables and a :py:class:`LpProblem` """ # we instantiate the problem @@ -1309,11 +1330,26 @@ def edit_const(const): return var, pb def to_json(self, filename, *args, **kwargs): + """ + Creates a json file from the LpProblem information + + :param str filename: filename to write json + :param args: additional arguments for json function + :param kwargs: additional keyword arguments for json function + :return: None + """ with open(filename, 'w') as f: json.dump(self.to_dict(), f, *args, **kwargs) @classmethod def from_json(cls, filename): + """ + Creates a new Lp Problem from a json file with information + + :param str filename: json file name + :return: a tuple with a dictionary of variables and an LpProblem + :rtype: (dict, :py:class:`LpProblem`) + """ with open(filename, 'r') as f: data = json.load(f) return cls.from_dict(data) @@ -1536,6 +1572,17 @@ def coefficients(self, translation = None): return coefs def writeMPS(self, filename, mpsSense = 0, rename = 0, mip = 1): + """ + Writes an mps files from the problem information + + :param str filename: name of the file to write + :param int mpsSense: + :param bool rename: if True, normalized names are used for variables and constraints + :param mip: variables and variable renames + :return: + Side Effects: + - The file is created. + """ wasNone, dummyVar = self.fixObjective() if mpsSense == 0: mpsSense = self.sense cobj = self.objective @@ -1649,8 +1696,8 @@ def writeLP(self, filename, writeSOS = 1, mip = 1, max_length=100): This function writes the specifications (objective function, constraints, variables) of the defined Lp problem to a file. - :param filename: the name of the file to be created. - return variables + :param str filename: the name of the file to be created. + :return: variables Side Effects: - The file is created. """ @@ -1726,6 +1773,11 @@ def writeLP(self, filename, writeSOS = 1, mip = 1, max_length=100): return vs def checkDuplicateVars(self): + """ + Checks if there are at least two variables with the same name + :return: 1 + :raises `const.PulpError`: if there ar duplicates + """ vs = self.variables() repeated_names = {} @@ -1738,6 +1790,12 @@ def checkDuplicateVars(self): return 1 def checkLengthVars(self, max_length): + """ + Checks if variables have names smaller than `max_length` + :param int max_length: max size for variable name + :return: + :raises const.PulpError: if there is at least one variable that has a long name + """ vs = self.variables() long_names = [v.name for v in vs if len(v.name) > max_length] if long_names: @@ -1879,16 +1937,24 @@ def resolve(self, solver = None, **kwargs): else: return self.solve(solver = solver, **kwargs) - def setSolver(self,solver = LpSolverDefault): + def setSolver(self, solver = LpSolverDefault): """Sets the Solver for this problem useful if you are using resolve """ self.solver = solver def numVariables(self): + """ + + :return: number of variables in model + """ return len(self._variable_ids) def numConstraints(self): + """ + + :return: number of constraints in model + """ return len(self.constraints) def getSense(self): diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index 8c80c2f9..d7a48f3e 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -675,7 +675,7 @@ def test_export_dict_LP(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" data = prob.to_dict() - var1, prob1 = prob.from_dict(data) + var1, prob1 = LpProblem.from_dict(data) x, y, z, w = [var1[name] for name in ['x', 'y', 'z', 'w']] print("\t Testing continuous LP solution") pulpTestCheck(prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0}) @@ -694,7 +694,7 @@ def test_export_json_LP(self): prob += w >= 0, "c4" filename = name + '.json' prob.to_json(filename, indent=4) - var1, prob1 = prob.from_json(filename) + var1, prob1 = LpProblem.from_json(filename) try: os.remove(filename) except: @@ -713,7 +713,7 @@ def test_export_dict_MIP(self): prob += x + z >= 10, "c2" prob += -y + z == 7.5, "c3" data = prob.to_dict() - var1, prob1 = prob.from_dict(data) + var1, prob1 = LpProblem.from_dict(data) x, y, z = [var1[name] for name in ['x', 'y', 'z']] print("\t Testing MIP solution") pulpTestCheck(prob1, self.solver, [const.LpStatusOptimal], {x: 3, y: -0.5, z: 7}) @@ -730,7 +730,7 @@ def test_export_dict_max(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" data = prob.to_dict() - var1, prob1 = prob.from_dict(data) + var1, prob1 = LpProblem.from_dict(data) x, y, z, w = [var1[name] for name in ['x', 'y', 'z', 'w']] print("\t Testing maximize continuous LP solution") pulpTestCheck(prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: 1, z: 8, w: 0})