\* Rust · CLI · TUI · LSP · Python *\
Read LP and MPS files into a model you can inspect, edit, analyse, diff and write back out. The LP side is a LALRPOP grammar that follows the CPLEX 22.1.1, Gurobi, FICO Xpress and Mosek LP specifications.
\* everything below round-trips:
parse → modify → write → parse *\
Minimize
cost: 0.1 x1 + 0.2 x2 + 3055.2 x3
Subject To
capacity: 2 x1 + 4.5 x2 <= 200
demand: x1 + x2 >= 30
Bounds
0 <= x1 <= 40
x2 free
General
x3
End
What's here
Parse, inspect, modify and write LP and MPS problems from Rust. Diff, serde and external solvers are optional features.
Six subcommands: parse, info, analyze, diff, convert, solve. LP or MPS in; text, JSON or YAML out.
Explore one LP or MPS model, or diff two down to the coefficient. Solves with HiGHS in the background.
The Rust parser and writer from Python, with type stubs. pip install parse_lp.
Diagnostics, navigation, rename, completion, formatting, and code actions for .lp files in VS Code, Neovim, Helix, Zed, or any LSP client.
The Rust library
MAX, MIN, ABS, AND, OR; not PWL).LpProblem::parse_mps, mps::writer), so a model can move between the two formats.ProblemBuilder.diff feature) with absolute and relative tolerances and an optional name normaliser. The CLI and TUI build the normaliser from regex rules.Serialize and Deserialize on the model (serde feature), so any serde format works. The CLI writes JSON and YAML.lp-solvers feature). Quadratic, indicator and general constraints are refused rather than dropped.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());
use lp_parser_rs::{model::VariableType, problem::LpProblem, writer::write_lp_string};
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 represent
Modification methods on LpProblem: update_objective_coefficient, rename_objective, remove_objective, update_constraint_coefficient, update_constraint_rhs, rename_constraint, remove_constraint, rename_variable, update_variable_type, remove_variable.
Writer options live on LpWriterOptions (include_problem_name, max_line_length, decimal_precision, include_section_spacing) and are passed to write_lp_string_with_options. decimal_precision defaults to None, which writes each number in its shortest exact form.
| Feature | Adds |
|---|---|
| cli | Builds the lp_parser binary. diff, solve, CSV output and JSON/YAML output each also need the matching feature below. |
| csv | CSV export of constraints, objectives, and variables. |
| mmap | parser::MappedFile: memory-maps an input file so it can be parsed without first copying it into a String. |
| diff | LpProblem::diff and diff::compare. Turns on serde. |
| serde | Serde derives on problems, analyses and diffs. |
| lp-solvers | Adapter for the lp-solvers crate: CBC, GLPK, Gurobi, CPLEX. |
The command line
lp_parserEvery subcommand reads LP or MPS (a .mps extension reads MPS) and writes to stdout or a file (-o). -f picks text, JSON or YAML, and --pretty indents JSON. Global flags: -v/--verbose, -q/--quiet.
lp_parser parse problem.lp
lp_parser parse problem.lp --format yaml -o problem.yaml
Counts plus optional full listings via --variables, --constraints, --objectives.
lp_parser info problem.lp --variables --constraints --objectives
lp_parser info problem.lp --format json --pretty
| Option | Default | Description |
|---|---|---|
| --issues-only | off | Skip full analysis; show warnings/errors only |
| --large-coeff-threshold | 1e9 | Warn on coefficients larger than this |
| --small-coeff-threshold | 1e-9 | Warn on non-zero coefficients smaller than this |
| --large-rhs-threshold | 1e9 | Warn on right-hand sides larger in magnitude than this |
| --ratio-threshold | 1e6 | Warn on coefficient scaling ratios above this |
lp_parser analyze problem.lp --issues-only
lp_parser analyze problem.lp --large-coeff-threshold 1e8 --ratio-threshold 1e5
# example output
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 |
|---|---|---|
| --abs-tol | 0.0 | Absolute tolerance for numeric comparisons |
| --rel-tol | 0.0 | Relative tolerance: |a−b| ≤ rel_tol · max(|a|, |b|) |
| --rename P R | — | Regex rewrite applied to names in both files before matching; repeatable |
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 --pretty
Both diff and analyze follow GNU diff's exit codes, so CI can gate on them: diff exits 0 when the problems match, 1 when they differ and 2 on failure; analyze exits 0 when clean, 1 on an error-severity issue and 2 on failure.
Targets: lp, mps, csv, json, yaml. Without --precision, numbers are written in their shortest exact form. LP output honours --precision, --max-line-length, --no-problem-name, and --compact; MPS output honours --precision; CSV writes constraints.csv, objectives.csv, and variables.csv to a directory.
lp_parser convert problem.lp --format lp --precision 4 --compact
lp_parser convert problem.lp --format csv --output ./out
Solves with CBC (default) or GLPK, which must be on your PATH (-s/--solver). Multiple objectives, strict inequalities, quadratic terms, and indicator or general constraints are errors; SOS constraints are dropped with a warning.
lp_parser solve problem.lp --solver glpk --format json --pretty
The terminal UI
lp_diffA ratatui explorer and diff viewer for LP and MPS files. One file opens a single-model explorer; two files are diffed, in any mix of formats (lp_diff model.lp model.mps). It also solves with HiGHS, re-solves after an RHS edit, applies presolve-style rewrites and compares the result, and ranks the rows and columns most likely to be slowing a solve down.
r: regex, s: substring and c: content modes. There is also a command palette and a 100-entry jumplist.Editor support
lp-lspA language server for .lp files. It understands the CPLEX LP format and the Gurobi extensions the parser supports, and keeps working while a file is half-written. It talks LSP over stdio and takes no arguments, so any editor that can run a language server can use it.
MAX, MIN, ABS, AND, and OR.lp.analyze, lp.convertToMps, and lp.showModelStats.-- Neovim 0.11+: ~/.config/nvim/lsp/lp_lsp.lua
return {
cmd = { 'lp-lsp' },
filetypes = { 'lp' },
root_markers = { '.git' },
}
-- init.lua
vim.filetype.add({ extension = { lp = 'lp' } })
vim.lsp.enable('lp_lsp')
A VS Code extension lives in lsp/editors/vscode (build it from source). Helix needs a languages.toml entry, and Zed a small dev extension to register the language; the README has both.
Python bindings
parse_lpPython bindings to the Rust library, with type stubs (Python 3.9+). They cover parsing, data access, modification, LP and MPS writing, analysis with configurable thresholds, CSV export and diff between two problems.
from parse_lp import LpParser
parser = LpParser("problem.lp")
print(parser.name, parser.sense, parser.num_variables)
parser.update_objective_coefficient("OBJ", "x1", 5.0)
parser.rename_variable("x2", "production")
parser.update_constraint_rhs("C1", 100.0)
parser.save_to_file("modified_problem.lp")
analysis = parser.analyze()
print(analysis["summary"]["density"])
The constructor parses the file straight away; the format follows the extension (.mps reads MPS). LpParser.from_file(path, format="mps") overrides it, LpParser.from_string(text) parses in-memory text, and to_mps_string / save_to_mps write MPS back out.
Getting it
| Where | Command |
|---|---|
| Rust crate | lp_parser_rs = { version = "5.0.1", features = ["serde", "diff"] } |
| CLI | cargo install lp_parser_rs --features cli,csv,diff,serde,lp-solvers |
| TUI | cargo install --path tui (from a clone) |
| Language server | cargo install --path lsp (from a clone) |
| Python | pip install parse_lp |
Develop with cargo insta test --all-features and review snapshot changes with cargo insta review. Pull requests are welcome on GitHub.