Skip to content

Add LP format reader; accept .lp wherever .mps is accepted#1120

Draft
mlubin wants to merge 1 commit into
NVIDIA:mainfrom
mlubin:lp-format
Draft

Add LP format reader; accept .lp wherever .mps is accepted#1120
mlubin wants to merge 1 commit into
NVIDIA:mainfrom
mlubin:lp-format

Conversation

@mlubin
Copy link
Copy Markdown
Contributor

@mlubin mlubin commented Apr 17, 2026

What

A new LP-format parser sits alongside the existing MPS parser. Every
cuOpt entry point that takes an input file (the cuopt_cli CLI,
cuOptReadProblem in the C API, the Python ParseLp / ParseMps
wrappers, and the self-hosted client) now accepts LP files via a
case-insensitive extension dispatcher:

Extension Parser
.lp, .lp.gz, .lp.bz2 LP
.mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 MPS / QPS
anything else rejected

The unified entry point is parse_problem<i_t, f_t>(path) in
cuopt/linear_programming/io/parser.hpp. parse_lp and
parse_lp_from_string (string-input counterpart, mirroring
parse_mps_from_string) are exposed alongside their MPS siblings.

LP format scope

The parser accepts the conventional LP dialect implemented by most
commercial optimization solvers (not the lpsolve variant):

  • LP, MIP, QP.
  • Semi-continuous variables via a Semi-Continuous section; each
    listed variable requires a finite upper bound.
  • Quadratic constraints (QCQP), <= only. Quadratic terms appear
    inside [ ... ]. The convention differs between objective and
    constraint:
    • Objective bracket must be followed by / 2 (LP file uses the
      0.5 x^T Q x convention).
    • Constraint bracket must not be followed by / 2 (coefficients
      at face value, x^T Q x).

SOS constraints, piecewise-linear objectives, general constraints, and
user cuts raise a clear ValidationError.

Compressed inputs (.lp.gz, .lp.bz2) are handled by the same
dlopen-based zlib / libbz2 path that already serves .mps.gz /
.mps.bz2; the helper was extracted to cpp/src/io/file_to_string.{hpp,cpp}.

Notable cleanups along the way

  • cython_mps_parser.{hpp,cpp}cython_parser.{hpp,cpp} (now exposes
    both call_parse_mps and call_parse_lp).
  • mps_parser_test.cppparser_test.cpp (consolidated; covers both
    parsers and the dispatcher).
  • Error messages and the CUOPT_MPS_FILE_ERROR substring check
    generic-ified from "MPS file" → "input file".

Tests

  • 129 C++ gtest cases in parser_test.cpp covering MPS, LP,
    semi-continuous, quadratic objective, quadratic constraint, the
    extension dispatcher, and unsupported-section rejection. Compressed
    LP fixtures (good-mps-1.lp.{gz,bz2}) exercise the shared
    decompression path.
  • C API: c_api.read_lp_file_by_extension confirms
    cuOptReadProblem dispatches to the LP parser by extension.
  • Python: test_parse_lp_basic, test_parse_lp_rejects_unsupported_section,
    test_parse_lp_and_parse_mps_agree_on_trivial_problem.

Docs

User-facing docs (cuopt-cli, cuopt-c/lp-qp-milp) state the supported
extensions and link to the API reference. The full LP-format spec
(scope, conventions, rejections) lives in the parse_lp /
parse_lp_from_string docstrings in parser.hpp and the ParseLp
docstring in parser.py.

Size

44 files changed (20 added, 21 modified, 1 deleted, 2 renamed); ~5.6k
insertions / ~1.7k deletions, the bulk being the new LP parser
(lp_parser.cpp, ~1.1k lines) and consolidated tests (parser_test.cpp).

@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented Apr 17, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@mlubin
Copy link
Copy Markdown
Contributor Author

mlubin commented Apr 18, 2026

/ok to test 870a37f

@rg20 rg20 added feature request New feature or request non-breaking Introduces a non-breaking change labels Apr 20, 2026
@rg20 rg20 added this to the 26.06 milestone Apr 20, 2026
Comment thread cpp/src/io/lp_parser.cpp
if (lower == "such" && name_equals_ci(t2, "that")) return true;
if (lower == "st" || lower == "s.t.") return true;
if (lower == "lazy" && name_equals_ci(t2, "constraints")) return true;
if (lower == "user" && name_equals_ci(t2, "cuts")) return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not support "user cuts"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're not supported; added a test to confirm. Recognizing the section here allows us to print a more helpful error message, I believe.

Comment thread cpp/src/io/lp_parser.cpp Outdated
@mlubin
Copy link
Copy Markdown
Contributor Author

mlubin commented Apr 20, 2026

/ok to test cf03235

@mlubin
Copy link
Copy Markdown
Contributor Author

mlubin commented Apr 20, 2026

/ok to test 003ecff

Comment thread cpp/src/io/lp_parser.cpp
// Purely linear term inside the brackets — permitted as long as the
// surrounding /2 convention is respected (the linear term is scaled
// the same way as the quadratic ones).
out_linear.push_back({i1, sign * coeff});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was skimming this to see if there was anything of interest for JuMP's reader. We don't support linear terms inside [].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sounds like there's no reason for us to allow this.

Comment thread cpp/src/io/lp_parser.cpp Outdated
// Require the "/ 2" suffix after a quadratic objective expression.
// Without it there is no ambiguity-free way to tell whether the user
// meant /2 and forgot vs. intended bare coefficients, so we enforce the
// stricter form.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple solvers treat the ]/2 as optional in the objective. I don't remember if I just copied them, or if there were some common instances where this happened.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed multiple solvers treat ]/2 as optional in the objective, but it seems scary to me. CPLEX says it's required, IIRC. I'd rather disallow this unless there's a good reason otherwise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CPLEX says it's required

Oh, they all say it's required. But the files that they can actually read depart quite a lot from the written spec.

Comment thread cpp/src/io/lp_parser.cpp

// A negative upper bound requires an explicitly stated lower bound,
// otherwise the default lower of 0 would collide with the upper and make
// the variable silently infeasible. Flag this at parse time.
Copy link
Copy Markdown

@odow odow Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think if the upper bound is negative, we assume that the lower bound is -Inf. But I see now that Gurobi flags the model as infeasible.

x-ref jump-dev/MathOptInterface.jl#2994

@aliceb-nv aliceb-nv linked an issue Apr 24, 2026 that may be closed by this pull request
@mlubin mlubin force-pushed the lp-format branch 11 times, most recently from b277e5e to 98f9626 Compare May 15, 2026 20:43
@mlubin
Copy link
Copy Markdown
Contributor Author

mlubin commented May 15, 2026

/ok to test 98f9626

cuOptReadProblem, cuopt_cli, the Python ParseLp() wrapper, and the
self-hosted client now dispatch on the input filename: a case-insensitive
".lp" suffix routes to a new LP parser; everything else (including .mps,
.mps.gz, .mps.bz2, and extensionless inputs) continues to use parse_mps.

The LP parser supports LP, MIP, and QP problems in the conventional LP
dialect used by most commercial solvers (not the lpsolve variant). SOS,
PWL, semi-continuous, user-cut, and general-constraint sections raise a
ValidationError rather than silently mis-parsing. Quadratic-constraint
support (QCMATRIX) remains MPS-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Miles Lubin <mlubin@nvidia.com>
@mlubin
Copy link
Copy Markdown
Contributor Author

mlubin commented May 15, 2026

/ok to test 92ffa8f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Add support for reading .lp files

5 participants