A Rust library and CLI for parsing, analysing, modifying and writing Linear Programming (LP) and MPS files. The LP reader uses a Logos lexer and a LALRPOP grammar (lp.lalrpop).
Supported specifications: IBM CPLEX v22.1.1, FICO Xpress, Gurobi, Mosek.
- Parsing and writing: read LP or MPS, write LP (
writer) or MPS (mps::writer), so a problem read in one format can be written in either. Parse, modify, write and parse again gives back the modified model. - Modification: rename, update or remove objectives, constraints, variables, coefficients and RHS values.
ProblemBuilderbuilds a problem in code. - Variable types: continuous, integer, general, binary, semi-continuous and semi-integer, with lower, upper, double or free bounds.
- Analysis: statistics, density, sparsity, coefficient ranges, and issue detection with configurable thresholds.
- Diff (
difffeature): structural and numeric comparison of two problems, viaLpProblem::diffordiff::compare. - Serialisation (
serdefeature): serde support for the model; the CLI uses it for JSON and YAML output. - CSV export (
csvfeature):LpProblem::to_csvwrites one file per section. - Memory-mapped input (
mmapfeature):parser::MappedFilemaps a file instead of reading it into aString. - External solvers (
lp-solversfeature): an adapter to the lp-solvers crate (CBC, Gurobi, CPLEX, GLPK).
Beyond linear objectives, constraints, bounds and variable-type sections, the parser, both writers, the diff engine and the analysis understand:
| Feature | LP syntax | Model | MPS |
|---|---|---|---|
| Semi-integer variables | variable listed in both Generals (or Integers) and Semi-Continuous (CPLEX) |
VariableKind::SemiInteger |
SI bound (read and written) |
| Lazy constraints / user cuts | Lazy Constraints and User Cuts sections after Subject To (CPLEX) |
ordinary constraints tagged in LpProblem::constraint_classes (ConstraintClass::Lazy / UserCut) |
LAZYCONS / USERCUTS sections (read and written) |
| Indicator constraints | name: b = 1 -> x + y <= 3 (or b = 0) in Subject To / lazy sections (CPLEX, Gurobi); <-> and <- are not supported |
Constraint::Indicator |
INDICATORS section (IF row column value, read and written) |
| Quadratic objectives | obj: 2 x + [ x ^ 2 + 4 x * y ] / 2 (the / 2 is mandatory, as in CPLEX and Gurobi) |
Objective::quadratic (QuadraticTerm, stored with the / 2 applied) |
QUADOBJ / QMATRIX read, QUADOBJ written |
| Quadratic constraints | c: x + [ x ^ 2 + y ^ 2 ] <= 4 (no / 2) |
Constraint::Quadratic |
QCMATRIX (read and written) |
| General constraints | General Constraints section (Gurobi; also General Constrs, Gen Cons, GenConstrs): g: r = MAX ( x1 , x2 , 3 ), MIN, ABS ( x ), AND ( b1 , b2 ), OR; PWL is not supported |
Constraint::General (GeneralFunction) |
not supported: the writer returns an error, the reader skips GENCONS with a warning |
| Multi-objective attributes | Minimize multi-objectives, then OBJ0: Priority=2 Weight=1 AbsTol=0 RelTol=0 before each objective's expression (Gurobi; every attribute optional) |
Objective::attributes (ObjectiveAttributes) |
not supported: the writer returns an error unless allow_multiple_objectives is set (which already drops all but the first objective) |
A lone [ or ] is always a quadratic bracket, so it must be separated from neighbouring names by whitespace (names such as x[1] are unaffected). Likewise the parentheses and commas of a general constraint must be separated by whitespace, as Gurobi writes them, since (, ) and , are name characters.
The lp-solvers adapter refuses models containing indicator, quadratic or general constraints, or a quadratic objective, rather than silently dropping them. The lp_diff HiGHS solver refuses indicator, quadratic and general constraints, and passes a quadratic objective to HiGHS as a Hessian (continuous QPs only: HiGHS cannot solve MIQPs, and the IIS/ranging/presolve queries refuse QPs).
Add to Cargo.toml:
[dependencies]
lp_parser_rs = { version = "5.0.1", features = ["serde", "diff"] } # x-release-please-versionParse and inspect:
use lp_parser_rs::{parser::parse_file, problem::LpProblem};
use std::path::Path;
let content = parse_file(Path::new("problem.lp"))?;
let problem = LpProblem::parse(&content)?;
println!("{} objectives, {} constraints, {} variables",
problem.objective_count(), problem.constraint_count(), problem.variable_count());Modify and write:
use lp_parser_rs::{problem::LpProblem, writer::write_lp_string, model::VariableType};
let mut problem = LpProblem::parse(&std::fs::read_to_string("problem.lp")?)?;
problem.update_objective_coefficient("profit", "x1", 5.0)?;
problem.rename_objective("profit", "total_profit")?;
problem.update_constraint_coefficient("capacity", "x1", 2.0)?;
problem.update_constraint_rhs("capacity", 200.0)?;
problem.rename_variable("x1", "production_a")?;
problem.update_variable_type("production_a", VariableType::Integer)?;
std::fs::write("modified.lp", write_lp_string(&problem)?)?; // errors on names LP cannot representAvailable modification methods on LpProblem: update_objective_coefficient, rename_objective, remove_objective, update_constraint_coefficient, update_constraint_rhs, rename_constraint, remove_constraint, set_constraint_class, rename_variable, update_variable_type, remove_variable. Setting a coefficient to zero removes the term.
Writer options: write_lp_string_with_options(&problem, &LpWriterOptions { include_problem_name, max_line_length, decimal_precision, include_section_spacing }). decimal_precision defaults to None, which writes every number in its shortest exact (round-trip) form; Some(n) rounds to n decimal places.
cargo install lp_parser_rs --features cli,csv,diff,serde,lp-solvers
# Or from source
git clone https://github.com/dandxy89/lp_parser_rs.git
cd lp_parser_rs/rust && cargo build --release --features cli,csv,diff,serde,lp-solversEvery subcommand reads LP or MPS input, choosing the parser by file extension (.mps, case-insensitive, reads MPS; anything else reads LP).
| Flag | Description |
|---|---|
-v, --verbose |
Print progress details to stderr |
-q, --quiet |
Suppress non-essential output |
-h, --help / -V, --version |
Print help / version |
| Option | Default | Description |
|---|---|---|
<FILE> |
— | Path to the LP or MPS file (required) |
-o, --output <PATH> |
stdout | Write output to file |
-f, --format <FMT> |
text |
text, json (serde), yaml (serde) |
--pretty |
off | Pretty-print JSON (YAML is unaffected) |
lp_parser parse problem.lp
lp_parser parse problem.lp --format yaml -o problem.yamlAdds to the parse options:
| Option | Description |
|---|---|
--variables |
List all variables with their types |
--constraints |
List all constraints |
--objectives |
List all objectives |
lp_parser info problem.lp
lp_parser info problem.lp --variables --constraints --objectives
lp_parser info problem.lp --format json --prettyAdds to the parse options:
| Option | Default | Description |
|---|---|---|
--issues-only |
off | Skip full analysis; show warnings/errors only |
--large-coeff-threshold <F> |
1e9 |
Warn on coefficients larger than this |
--small-coeff-threshold <F> |
1e-9 |
Warn on non-zero coefficients smaller than this |
--large-rhs-threshold <F> |
1e9 |
Warn on RHS values larger than this in magnitude |
--ratio-threshold <F> |
1e6 |
Warn on coefficient scaling ratios above this |
Thresholds must be finite and greater than zero.
lp_parser analyze problem.lp
lp_parser analyze problem.lp --issues-only
lp_parser analyze problem.lp --large-coeff-threshold 1e8 --ratio-threshold 1e5
lp_parser analyze problem.lp --format yaml -o analysis.yamlExample output (abridged)
summary: { name: diet, sense: Minimize, objective_count: 1, constraint_count: 7, variable_count: 16, density: 0.571 }
variables: { type_distribution: { upper_bounded: 9, double_bounded: 7 }, discrete_variable_count: 0 }
constraints: { type_distribution: { equality: 7 }, rhs_range: { min: 30.0, max: 50000.0 } }
coefficients: { constraint_coeff_range: { min: 0.1, max: 3055.2 }, coefficient_ratio: 101840.0 }
issues: []| Option | Default | Description |
|---|---|---|
<FILE1> <FILE2> |
— | Base and comparison files (LP or MPS) |
-o, --output <PATH> |
stdout | Write output to file |
-f, --format <FMT> |
text |
text, json, yaml |
--pretty |
off | Pretty-print JSON (YAML is unaffected) |
--abs-tol <F> |
0.0 |
Absolute tolerance for numeric comparisons |
--rel-tol <F> |
0.0 |
Relative tolerance: |a-b| ≤ rel_tol · max(|a|, |b|) |
--rename <PATTERN> <REPL> |
— | Regex rewrite applied to names in both files before matching; repeatable |
lp_parser diff old.lp new.lp
lp_parser diff old.lp new.lp --abs-tol 1e-6 --rel-tol 1e-9
lp_parser diff old.lp new.lp --rename '\[\d+\]$' '[N]' --format json --prettyBoth diff and analyze follow the GNU diff exit-code convention, so a
pipeline can gate on unexpected model changes or structural errors:
diffexits0when the problems match,1when they differ,2on failure.analyzeexits0when clean,1when any error-severity issue is found,2on failure.
lp_parser diff baseline.lp candidate.lp --format json > model_diff.json || echo "model changed"
lp_parser analyze candidate.lp --issues-only || exit 1| Option | Default | Description |
|---|---|---|
<FILE> |
— | Path to the LP or MPS file (.mps extension reads MPS) |
-o, --output <PATH> |
stdout | Output file or directory (required for CSV) |
-f, --format <FMT> |
lp |
lp, mps, csv, json, yaml |
--pretty |
off | Pretty-print JSON (YAML is unaffected) |
--precision <N> |
exact | Round numbers to N decimal places (default: shortest exact form) |
--max-line-length <N> |
80 |
Line-wrap threshold for LP output |
--no-problem-name |
off | Omit problem-name comment in LP output |
--compact |
off | No section spacing |
lp_parser convert problem.lp --format lp --precision 4 --compact
lp_parser convert problem.lp --format csv --output ./out # writes constraints.csv, objectives.csv, variables.csv
lp_parser convert problem.lp --format json --pretty -o problem.json
lp_parser convert problem.lp --format mps -o problem.mps # LP -> MPS
lp_parser convert problem.mps --format mps -o rewritten.mps # MPS -> MPSCaveat (MPS integer defaults): an integer variable declared inside
'MARKER'INTORG/INTENDwith no explicit BOUNDS entry defaults to[0, 1], following the CPLEX MPS specification. Gurobi instead defaults such variables to[0, +inf), so the same file can yield different models in different readers. Declare bounds explicitly if the wider range is intended.
| Option | Default | Description |
|---|---|---|
<FILE> |
— | Path to the LP or MPS file |
-s, --solver <NAME> |
cbc |
cbc, glpk |
-o, --output <PATH> |
stdout | Write solution to file |
-f, --format <FMT> |
text |
text, json, yaml |
--pretty |
off | Pretty-print JSON |
lp_parser solve problem.lp
lp_parser solve problem.lp --solver glpk
lp_parser solve problem.lp --format json --prettyThe selected solver binary must be on your PATH. The compatibility layer rejects problems with more than one objective or with strict inequalities (<, >), ignores SOS constraints with a warning, and treats semi-continuous and semi-integer variables as continuous and integer respectively, also with a warning.
A terminal UI for exploring a single LP/MPS model or comparing two, with coefficient-level side-by-side diffs, fuzzy search, filtering, and integrated HiGHS solving. Built with ratatui. Pass one file to inspect a model, or two files to diff them.
The demo diffs an MPS file against an LP file: side-by-side constraint diffs, filters, sort and live tolerance cycling, fuzzy search, Numerics, the keybindings pop-up, and a HiGHS solve of both files with a solution diff.
cargo install --path tui
lp_diff model.lp # inspect a single model
lp_diff base.lp modified.lp # diff two files
lp_diff model.lp --summary # non-interactive single-model summary
lp_diff base.lp modified.lp --summary # non-interactive diff summarylp_diff accepts the same name-rewrite and numeric-tolerance flags as the CLI diff command. Active options are shown on the Summary panel (and in --summary output) so results are reproducible.
| Option | Default | Description |
|---|---|---|
--abs-tol <F> |
0.0 |
Absolute tolerance for RHS & coefficient comparisons |
--rel-tol <F> |
0.0 |
Relative tolerance: |a-b| ≤ rel_tol · max(|a|,|b|) |
--rename <PATTERN> <REPL> |
— | Regex rewrite applied to names in both files before matching; repeatable |
# Collapse indexed names (e.g. x[1,2,foo] / x[9,9,baz] → x[idx]) so structural diffs survive renumbering
lp_diff base.lp modified.lp --rename '\[\d+,\d+,[^]]*\]$' '[idx]'
# Hide near-equal RHS/coefficient drift
lp_diff base.lp modified.lp --abs-tol 1e-4 --rel-tol 1e-6
# Combine with --summary for scripting
lp_diff base.lp modified.lp --summary --rename '\[\d+\]$' '[idx]' --abs-tol 1e-6Highlights: three-panel layout, five sections (Summary / Variables / Constraints / Objectives / Numerics), filtering (a/+/-/m/=, plus o to ignore coefficient order), sort cycling (s), live tolerance cycling (t/T), telescope-style search (fuzzy, r: regex, s: substring), HiGHS solve-and-compare with infeasibility diagnosis (S), vim-style navigation and jumplist, clipboard yank (y/Y), CSV export (w), ? for full help.
See tui/README.md for the complete reference.
A Language Server Protocol implementation for .lp files: diagnostics, navigation, rename, hover, completion, inlay hints, code actions, formatting, semantic highlighting and more, for any LSP-capable editor (Neovim, Helix, Zed, VS Code).
cargo install --path lspSee lsp/README.md for the feature list, configuration reference and editor setup.
cargo insta test --all-features # run tests
cargo insta review # review snapshot changesTest data sources: Jplex, LPWriter.jl, Lp-Parser.
Contributions are welcome: please open a pull request.
