Skip to content
Get notified of new posts

Subscribe

* indicates required

Intuit Mailchimp

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_variable instead of LpVariable (mandatory).
  • lpSum_vars and lpSum_vars_coefs instead of just lpSum (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.

pip install pulp[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 ty instead of mypy to 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.

Comments