Get notified of new posts
PuLP 4.0.0 release¶
Many months after deciding to revamp the PuLP library, I've finally decided to publish the stable version 4.0.
In this blog post I'll include a summary of all the (many positive) changes that come with this new version, as I fear they'll be hidden away in commits, PRs and HISTORY.md files forever otherwise.
TL;DR, check the migration guide here: migration guide
Introduction¶
I've already written a couple of blog posts, one as context for PuLP, the other one about the mayor change that required a big release: the implementation of a rust backend.
I've tried to be mindful of how these traumatic changes may cause issues for users and I included deprecation warnings in the last stable version for a while. I have no idea if these warnings are read by people or not, but I hope they are!
Rust core + efficiency¶
As already discussed in the other entry, PuLP now includes a Rust core for most of the internals. I'm quite happy with the way it works. And it's fast! I was initially disappointed that it "only" matched pyomo in efficiency. But I think that's not too bad for being as flexible as PuLP is.
As I mentioned in the previous post, some API changes are mandatory and others are recommended:
lp.add_variableinstead ofLpVariable(mandatory).lpSum_varsandlpSum_vars_coefsinstead of justlpSum(recommended).
No CBC solver inside¶
PuLP used to come with a CBC binary included inside the project. This caused several headaches: people thinking PuLP was a solver, people asking for support for CBC, the binaries included not being compatible with every single OS, etc. Sometimes the file permissions caused issues.
From now on, CBC will have to explicitly be installed via a PuLP option or a separate binary. The cbc option now installs the cbcbox package, which installs cbc.
I believe this may result in great dissatisfaction for students, teachers and newbies looking for a quick modeller+solver. I'm sorry about that.
LpSolveStats + orloge¶
A recurrent issue (almost as often as the CBC ones above) was the inconsistency of the status messages in PuLP. There were only a few statuses (optimal, infeasible, unknown, unbounded) and they were not enough to represent all the information that comes from a solver.
At some point this was patched (by me) with a "solution status" that was mostly redundant but helped to know if the solver had returned a feasible solution, which is a very common check to do when solving a large problem.
But a solver has a lot of information to give!
First, it provides the reason why it stopped: time, gap, nodes, memory, etc. This was never passed to the user.
Secondly, it provides very long log files with information on most things: pre-processing, model reduction, cuts applied, time to find feasible solutions, gap, best bounds and objective evolution over time, among many other things.
In order to get as much information as possible in a standard way, solvers in PuLP now return a common LpSolveStats object with normalized information, including: the reason the solver stopped, the existence of a feasible solution, the parsed logs, etc. To get logs, PuLP now includes orloge as a dependency. Currently log parsing is supported for: GUROBI, CPLEX, CBC and CP-SAT (this last one thanks to cpsat-logutils).
This change will likely break a lot of code that relied on LpProblem.solve() returning an integer with an (obscure) solver status. But the change should be easy to fix. I also decided to avoid storing the solving stats (time, status, etc.) inside the LpProblem since they're really the output of a solver run.
This change and its dependencies also made me have to move up the dependency into python 3.12.
New solvers: CP-SAT¶
I decided to include CP-SAT, a well-known hybrid open source portfolio solver developed by Google better known because of its CP solver based on SAT. PuLP only offers CP-SAT via an Integer Programming (IP) interface so no Interval, Element or Resource constraints (so far). Still with these limitations I've seen it performing well against open source MIP solvers.
Refactoring of files¶
PuLP used to be a pretty much "two-file project" when I started maintaining it: a file called pulp.py and a file called solvers.py. Shortly after joining the project, I split solvers.py into each solver so that maintaining more solvers became sane. And it helped other contributors add many new solvers, actually.
It was finally time for the pulp.py file to be split into many files: one file per class. This makes it insanely easier to navigate the project with an IDE.
We also now have one unit test file per solver, so it's easier to personalize tests for each API.
Type hinting¶
Most of the project is now type hinted.
Lost functionality¶
The migration to Rust required checking everything and prioritizing. This forced me to remove some little-known, almost undocumented functionality. Mainly: "column-based modelling", which made it easier to model column-generation models where you declare the constraints first and then add the variables.
I personally never used this and I suspect most people didn't either. Still, I'm sure I will get some random angry person who will swear it was the only reason they were using PuLP.
Many other minor things that happened¶
- XPRESS solver was updated.
- HiGHS solver now accepts warm starts.
- We now use
tyinstead ofmypyto check types.
What may change in the future¶
Now that we have a Rust core, we could potentially accept C-based APIs to solvers. I don't think this is really needed in most cases, although some solvers have a more complete API for C than for Python.
End¶
I hope this new version of the library brings more joy than headaches to its users. And I hope I'm able to find the time to continue improving it. As always, I'm open to new ideas on functionality to add to it! Write to me on github, or by email.