diff --git a/.gitmodules b/.gitmodules index 3eb51e3..f37a0ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "scripts/solvers/matlab/sdpt3"] path = scripts/solvers/matlab/sdpt3 url = https://github.com/sqlp/sdpt3 +[submodule "problems/NETLIB"] + path = problems/NETLIB + url = https://github.com/ozy4dm/lp-data-netlib.git diff --git a/config/problem_registry.yaml b/config/problem_registry.yaml index 4e4922b..90209cf 100644 --- a/config/problem_registry.yaml +++ b/config/problem_registry.yaml @@ -1174,3 +1174,763 @@ problem_libraries: library_name: "SDPLIB" for_test_flag: false known_objective_value: 1.33115e+02 + + # ============================================================================ + # NETLIB Library - LP Benchmark Problems (94 problems) + # Source: https://github.com/ozy4dm/lp-data-netlib + # Format: MPS (Mathematical Programming System) + # ============================================================================ + + # Small test problems (for_test_flag: true) + afiro: + display_name: "AFIRO (NETLIB)" + file_path: "problems/NETLIB/mps_files/afiro.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: true + + sc50a: + display_name: "SC50A (NETLIB)" + file_path: "problems/NETLIB/mps_files/sc50a.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: true + + sc50b: + display_name: "SC50B (NETLIB)" + file_path: "problems/NETLIB/mps_files/sc50b.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: true + + blend: + display_name: "BLEND (NETLIB)" + file_path: "problems/NETLIB/mps_files/blend.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: true + + kb2: + display_name: "KB2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/kb2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: true + + # Remaining NETLIB problems (alphabetical order) + netlib_25fv47: + display_name: "25FV47 (NETLIB)" + file_path: "problems/NETLIB/mps_files/25fv47.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + netlib_80bau3b: + display_name: "80BAU3B (NETLIB)" + file_path: "problems/NETLIB/mps_files/80bau3b.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + adlittle: + display_name: "ADLITTLE (NETLIB)" + file_path: "problems/NETLIB/mps_files/adlittle.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + agg: + display_name: "AGG (NETLIB)" + file_path: "problems/NETLIB/mps_files/agg.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + agg2: + display_name: "AGG2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/agg2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + agg3: + display_name: "AGG3 (NETLIB)" + file_path: "problems/NETLIB/mps_files/agg3.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + bandm: + display_name: "BANDM (NETLIB)" + file_path: "problems/NETLIB/mps_files/bandm.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + beaconfd: + display_name: "BEACONFD (NETLIB)" + file_path: "problems/NETLIB/mps_files/beaconfd.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + bnl1: + display_name: "BNL1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/bnl1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + bnl2: + display_name: "BNL2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/bnl2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + boeing1: + display_name: "BOEING1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/boeing1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + boeing2: + display_name: "BOEING2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/boeing2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + bore3d: + display_name: "BORE3D (NETLIB)" + file_path: "problems/NETLIB/mps_files/bore3d.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + brandy: + display_name: "BRANDY (NETLIB)" + file_path: "problems/NETLIB/mps_files/brandy.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + capri: + display_name: "CAPRI (NETLIB)" + file_path: "problems/NETLIB/mps_files/capri.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + cycle: + display_name: "CYCLE (NETLIB)" + file_path: "problems/NETLIB/mps_files/cycle.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + czprob: + display_name: "CZPROB (NETLIB)" + file_path: "problems/NETLIB/mps_files/czprob.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + d2q06c: + display_name: "D2Q06C (NETLIB)" + file_path: "problems/NETLIB/mps_files/d2q06c.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + d6cube: + display_name: "D6CUBE (NETLIB)" + file_path: "problems/NETLIB/mps_files/d6cube.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + degen2: + display_name: "DEGEN2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/degen2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + degen3: + display_name: "DEGEN3 (NETLIB)" + file_path: "problems/NETLIB/mps_files/degen3.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + dfl001: + display_name: "DFL001 (NETLIB)" + file_path: "problems/NETLIB/mps_files/dfl001.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + e226: + display_name: "E226 (NETLIB)" + file_path: "problems/NETLIB/mps_files/e226.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + etamacro: + display_name: "ETAMACRO (NETLIB)" + file_path: "problems/NETLIB/mps_files/etamacro.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + fffff800: + display_name: "FFFFF800 (NETLIB)" + file_path: "problems/NETLIB/mps_files/fffff800.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + finnis: + display_name: "FINNIS (NETLIB)" + file_path: "problems/NETLIB/mps_files/finnis.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + fit1d: + display_name: "FIT1D (NETLIB)" + file_path: "problems/NETLIB/mps_files/fit1d.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + fit1p: + display_name: "FIT1P (NETLIB)" + file_path: "problems/NETLIB/mps_files/fit1p.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + fit2d: + display_name: "FIT2D (NETLIB)" + file_path: "problems/NETLIB/mps_files/fit2d.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + fit2p: + display_name: "FIT2P (NETLIB)" + file_path: "problems/NETLIB/mps_files/fit2p.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + forplan: + display_name: "FORPLAN (NETLIB)" + file_path: "problems/NETLIB/mps_files/forplan.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ganges: + display_name: "GANGES (NETLIB)" + file_path: "problems/NETLIB/mps_files/ganges.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + gfrd_pnc: + display_name: "GFRD-PNC (NETLIB)" + file_path: "problems/NETLIB/mps_files/gfrd-pnc.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + greenbea: + display_name: "GREENBEA (NETLIB)" + file_path: "problems/NETLIB/mps_files/greenbea.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + greenbeb: + display_name: "GREENBEB (NETLIB)" + file_path: "problems/NETLIB/mps_files/greenbeb.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + grow15: + display_name: "GROW15 (NETLIB)" + file_path: "problems/NETLIB/mps_files/grow15.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + grow22: + display_name: "GROW22 (NETLIB)" + file_path: "problems/NETLIB/mps_files/grow22.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + grow7: + display_name: "GROW7 (NETLIB)" + file_path: "problems/NETLIB/mps_files/grow7.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + israel: + display_name: "ISRAEL (NETLIB)" + file_path: "problems/NETLIB/mps_files/israel.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + lotfi: + display_name: "LOTFI (NETLIB)" + file_path: "problems/NETLIB/mps_files/lotfi.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + maros: + display_name: "MAROS (NETLIB)" + file_path: "problems/NETLIB/mps_files/maros.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + maros_r7: + display_name: "MAROS-R7 (NETLIB)" + file_path: "problems/NETLIB/mps_files/maros-r7.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + modszk1: + display_name: "MODSZK1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/modszk1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + nesm: + display_name: "NESM (NETLIB)" + file_path: "problems/NETLIB/mps_files/nesm.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + perold: + display_name: "PEROLD (NETLIB)" + file_path: "problems/NETLIB/mps_files/perold.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilot: + display_name: "PILOT (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilot.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilot_ja: + display_name: "PILOT.JA (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilot.ja.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilot_we: + display_name: "PILOT.WE (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilot.we.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilot4: + display_name: "PILOT4 (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilot4.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilot87: + display_name: "PILOT87 (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilot87.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + pilotnov: + display_name: "PILOTNOV (NETLIB)" + file_path: "problems/NETLIB/mps_files/pilotnov.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + recipe: + display_name: "RECIPE (NETLIB)" + file_path: "problems/NETLIB/mps_files/recipe.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sc105: + display_name: "SC105 (NETLIB)" + file_path: "problems/NETLIB/mps_files/sc105.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sc205: + display_name: "SC205 (NETLIB)" + file_path: "problems/NETLIB/mps_files/sc205.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scagr25: + display_name: "SCAGR25 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scagr25.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scagr7: + display_name: "SCAGR7 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scagr7.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scfxm1: + display_name: "SCFXM1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scfxm1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scfxm2: + display_name: "SCFXM2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scfxm2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scfxm3: + display_name: "SCFXM3 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scfxm3.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scorpion: + display_name: "SCORPION (NETLIB)" + file_path: "problems/NETLIB/mps_files/scorpion.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scrs8: + display_name: "SCRS8 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scrs8.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scsd1: + display_name: "SCSD1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scsd1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scsd6: + display_name: "SCSD6 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scsd6.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + scsd8: + display_name: "SCSD8 (NETLIB)" + file_path: "problems/NETLIB/mps_files/scsd8.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sctap1: + display_name: "SCTAP1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/sctap1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sctap2: + display_name: "SCTAP2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/sctap2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sctap3: + display_name: "SCTAP3 (NETLIB)" + file_path: "problems/NETLIB/mps_files/sctap3.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + seba: + display_name: "SEBA (NETLIB)" + file_path: "problems/NETLIB/mps_files/seba.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + share1b: + display_name: "SHARE1B (NETLIB)" + file_path: "problems/NETLIB/mps_files/share1b.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + share2b: + display_name: "SHARE2B (NETLIB)" + file_path: "problems/NETLIB/mps_files/share2b.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + shell: + display_name: "SHELL (NETLIB)" + file_path: "problems/NETLIB/mps_files/shell.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship04l: + display_name: "SHIP04L (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship04l.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship04s: + display_name: "SHIP04S (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship04s.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship08l: + display_name: "SHIP08L (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship08l.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship08s: + display_name: "SHIP08S (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship08s.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship12l: + display_name: "SHIP12L (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship12l.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + ship12s: + display_name: "SHIP12S (NETLIB)" + file_path: "problems/NETLIB/mps_files/ship12s.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + sierra: + display_name: "SIERRA (NETLIB)" + file_path: "problems/NETLIB/mps_files/sierra.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + stair: + display_name: "STAIR (NETLIB)" + file_path: "problems/NETLIB/mps_files/stair.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + standata: + display_name: "STANDATA (NETLIB)" + file_path: "problems/NETLIB/mps_files/standata.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + standgub: + display_name: "STANDGUB (NETLIB)" + file_path: "problems/NETLIB/mps_files/standgub.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + standmps: + display_name: "STANDMPS (NETLIB)" + file_path: "problems/NETLIB/mps_files/standmps.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + stocfor1: + display_name: "STOCFOR1 (NETLIB)" + file_path: "problems/NETLIB/mps_files/stocfor1.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + stocfor2: + display_name: "STOCFOR2 (NETLIB)" + file_path: "problems/NETLIB/mps_files/stocfor2.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + netlib_truss: + display_name: "TRUSS (NETLIB)" + file_path: "problems/NETLIB/mps_files/truss.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + tuff: + display_name: "TUFF (NETLIB)" + file_path: "problems/NETLIB/mps_files/tuff.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + vtp_base: + display_name: "VTP.BASE (NETLIB)" + file_path: "problems/NETLIB/mps_files/vtp.base.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + wood1p: + display_name: "WOOD1P (NETLIB)" + file_path: "problems/NETLIB/mps_files/wood1p.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false + + woodw: + display_name: "WOODW (NETLIB)" + file_path: "problems/NETLIB/mps_files/woodw.mps" + file_type: "mps" + library_name: "NETLIB" + problem_type: "LP" + for_test_flag: false diff --git a/problems/NETLIB b/problems/NETLIB new file mode 160000 index 0000000..56257ee --- /dev/null +++ b/problems/NETLIB @@ -0,0 +1 @@ +Subproject commit 56257eea85b433ce6aa67d26156b36385318fd6f diff --git a/scripts/data_loaders/python/mps_loader.py b/scripts/data_loaders/python/mps_loader.py new file mode 100644 index 0000000..aac5214 --- /dev/null +++ b/scripts/data_loaders/python/mps_loader.py @@ -0,0 +1,468 @@ +""" +MPS File Loader for Linear Programming problems. + +This module provides functionality to load and parse optimization problems +stored in MPS (Mathematical Programming System) format, a standard format +for linear programming problems commonly used by Netlib and other LP libraries. + +MPS Format Structure: +- NAME: Problem name +- ROWS: Row (constraint) definitions with types: + - N: Objective function (free row) + - E: Equality constraint (=) + - L: Less-than-or-equal constraint (<=) + - G: Greater-than-or-equal constraint (>=) +- COLUMNS: Variable coefficients in constraints +- RHS: Right-hand side values +- BOUNDS: Variable bounds (optional) +- RANGES: Range constraints (optional) +- ENDATA: End of data + +The problem format is: min c'x subject to Ax {<=,=,>=} b, l <= x <= u +""" + +import sys +from pathlib import Path +from typing import Any, Dict + +import numpy as np +from scipy.sparse import lil_matrix + +# Add project root to path for imports +project_root = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(project_root)) + +from scripts.data_loaders.problem_loader import ProblemData +from scripts.utils.logger import get_logger + +logger = get_logger("mps_loader") + + +class MPSLoader: + """Loader for optimization problems in MPS format.""" + + def __init__(self): + """Initialize the MPS loader.""" + pass + + def load(self, file_path: str, problem_name: str = None) -> ProblemData: + """ + Load problem from .mps file. + + Args: + file_path: Path to the .mps file + problem_name: Optional problem name (if not provided, extracted from file) + + Returns: + ProblemData object + """ + if not Path(file_path).exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + logger.info(f"Loading MPS file: {file_path}") + + # Parse MPS file + mps_data = self._parse_mps_file(file_path) + + # Use provided name, MPS NAME field, or filename as fallback + if problem_name is None: + problem_name = mps_data.get("name", Path(file_path).stem) + + # Convert to ProblemData + problem_data = self._convert_to_problem_data(mps_data, problem_name) + + logger.info(f"Successfully loaded MPS problem: {problem_data}") + return problem_data + + def _parse_mps_file(self, file_path: str) -> Dict[str, Any]: + """ + Parse MPS format file. + + Args: + file_path: Path to MPS file + + Returns: + Dictionary containing parsed MPS data + """ + mps_data = { + "name": "", + "rows": {}, # row_name -> type (N, E, L, G) + "row_order": [], # ordered list of row names + "columns": {}, # col_name -> {row_name: coef} + "col_order": [], # ordered list of column names + "rhs": {}, # row_name -> value + "bounds": {}, # col_name -> {'lo': val, 'up': val, 'type': type} + "ranges": {}, # row_name -> value + "obj_name": None, # name of objective row + } + + with open(file_path, "r") as f: + lines = f.readlines() + + current_section = None + + for line in lines: + # Skip empty lines and comments + line = line.rstrip("\n\r") + if not line or line.startswith("*"): + continue + + # Check for section headers + upper_line = line.upper().strip() + if upper_line.startswith("NAME"): + mps_data["name"] = line.split()[1] if len(line.split()) > 1 else "" + current_section = "NAME" + continue + elif upper_line == "ROWS": + current_section = "ROWS" + continue + elif upper_line == "COLUMNS": + current_section = "COLUMNS" + continue + elif upper_line == "RHS": + current_section = "RHS" + continue + elif upper_line == "BOUNDS": + current_section = "BOUNDS" + continue + elif upper_line == "RANGES": + current_section = "RANGES" + continue + elif upper_line == "ENDATA": + break + + # Parse section content + if current_section == "ROWS": + self._parse_rows_line(line, mps_data) + elif current_section == "COLUMNS": + self._parse_columns_line(line, mps_data) + elif current_section == "RHS": + self._parse_rhs_line(line, mps_data) + elif current_section == "BOUNDS": + self._parse_bounds_line(line, mps_data) + elif current_section == "RANGES": + self._parse_ranges_line(line, mps_data) + + return mps_data + + def _parse_rows_line(self, line: str, mps_data: Dict[str, Any]): + """Parse a line from ROWS section.""" + parts = line.split() + if len(parts) >= 2: + row_type = parts[0].upper() + row_name = parts[1] + mps_data["rows"][row_name] = row_type + mps_data["row_order"].append(row_name) + if row_type == "N" and mps_data["obj_name"] is None: + mps_data["obj_name"] = row_name + + def _parse_columns_line(self, line: str, mps_data: Dict[str, Any]): + """Parse a line from COLUMNS section.""" + parts = line.split() + if len(parts) < 3: + return + + col_name = parts[0] + + # Initialize column if not seen before + if col_name not in mps_data["columns"]: + mps_data["columns"][col_name] = {} + mps_data["col_order"].append(col_name) + + # Parse pairs of (row_name, coefficient) + i = 1 + while i < len(parts) - 1: + row_name = parts[i] + try: + coef = float(parts[i + 1]) + mps_data["columns"][col_name][row_name] = coef + i += 2 + except (ValueError, IndexError): + break + + def _parse_rhs_line(self, line: str, mps_data: Dict[str, Any]): + """Parse a line from RHS section.""" + parts = line.split() + if len(parts) < 3: + return + + # First field is RHS name (usually ignored), then pairs of (row, value) + i = 1 + while i < len(parts) - 1: + row_name = parts[i] + try: + value = float(parts[i + 1]) + mps_data["rhs"][row_name] = value + i += 2 + except (ValueError, IndexError): + break + + def _parse_bounds_line(self, line: str, mps_data: Dict[str, Any]): + """Parse a line from BOUNDS section.""" + parts = line.split() + if len(parts) < 3: + return + + bound_type = parts[0].upper() + # parts[1] is bound name (usually ignored) + col_name = parts[2] + + if col_name not in mps_data["bounds"]: + mps_data["bounds"][col_name] = {"lo": 0.0, "up": np.inf, "type": "default"} + + if bound_type == "LO": # Lower bound + mps_data["bounds"][col_name]["lo"] = float(parts[3]) + elif bound_type == "UP": # Upper bound + mps_data["bounds"][col_name]["up"] = float(parts[3]) + elif bound_type == "FX": # Fixed value + val = float(parts[3]) + mps_data["bounds"][col_name]["lo"] = val + mps_data["bounds"][col_name]["up"] = val + mps_data["bounds"][col_name]["type"] = "fixed" + elif bound_type == "FR": # Free variable + mps_data["bounds"][col_name]["lo"] = -np.inf + mps_data["bounds"][col_name]["up"] = np.inf + mps_data["bounds"][col_name]["type"] = "free" + elif bound_type == "MI": # Minus infinity (lower bound) + mps_data["bounds"][col_name]["lo"] = -np.inf + elif bound_type == "PL": # Plus infinity (upper bound) + mps_data["bounds"][col_name]["up"] = np.inf + elif bound_type == "BV": # Binary variable + mps_data["bounds"][col_name]["lo"] = 0.0 + mps_data["bounds"][col_name]["up"] = 1.0 + mps_data["bounds"][col_name]["type"] = "binary" + + def _parse_ranges_line(self, line: str, mps_data: Dict[str, Any]): + """Parse a line from RANGES section.""" + parts = line.split() + if len(parts) < 3: + return + + # First field is range name (usually ignored), then pairs of (row, value) + i = 1 + while i < len(parts) - 1: + row_name = parts[i] + try: + value = float(parts[i + 1]) + mps_data["ranges"][row_name] = value + i += 2 + except (ValueError, IndexError): + break + + def _convert_to_problem_data(self, mps_data: Dict[str, Any], problem_name: str) -> ProblemData: + """ + Convert parsed MPS data to unified SeDuMi-style ProblemData format. + + This converts LP problems to SeDuMi standard form: + min c'x + s.t. A_eq @ x = b_eq + x in K (cone constraints) + + All inequality constraints are converted to equality constraints with + slack variables, and variable bounds are handled through the cone structure. + + Args: + mps_data: Parsed MPS data + problem_name: Name for the problem + + Returns: + ProblemData object in SeDuMi format + """ + n_orig_cols = len(mps_data["col_order"]) + + # Build column index mapping + col_idx = {name: i for i, name in enumerate(mps_data["col_order"])} + + # Separate rows by type + obj_rows = [] + eq_rows = [] + le_rows = [] + ge_rows = [] + + for row_name in mps_data["row_order"]: + row_type = mps_data["rows"][row_name] + if row_type == "N": + obj_rows.append(row_name) + elif row_type == "E": + eq_rows.append(row_name) + elif row_type == "L": + le_rows.append(row_name) + elif row_type == "G": + ge_rows.append(row_name) + + # Get variable bounds + var_bounds = [] + for col_name in mps_data["col_order"]: + if col_name in mps_data["bounds"]: + bound_info = mps_data["bounds"][col_name] + var_bounds.append((bound_info["lo"], bound_info["up"])) + else: + # Default: x >= 0 (standard LP convention) + var_bounds.append((0.0, np.inf)) + + # Count additional variables and constraints needed + n_le = len(le_rows) + n_ge = len(ge_rows) + n_ineq = n_le + n_ge # Number of slack variables needed + + # Count upper bound constraints (finite upper bounds) + n_upper_bounds = sum(1 for lo, up in var_bounds if up < np.inf) + + # Total variables: original + slack for inequalities + slack for upper bounds + n_slack_ineq = n_ineq + n_slack_upper = n_upper_bounds + n_total_vars = n_orig_cols + n_slack_ineq + n_slack_upper + + # Total constraints: equality + inequality (with slack) + upper bound constraints + n_orig_eq = len(eq_rows) + n_total_constraints = n_orig_eq + n_ineq + n_upper_bounds + + # Build objective vector c (extended with zeros for slack variables) + c = np.zeros(n_total_vars) + obj_name = mps_data["obj_name"] + if obj_name: + for col_name, coeffs in mps_data["columns"].items(): + if obj_name in coeffs: + c[col_idx[col_name]] = coeffs[obj_name] + + # Build unified constraint matrix A_eq and RHS b_eq + A_eq = lil_matrix((n_total_constraints, n_total_vars)) + b_eq = np.zeros(n_total_constraints) + + row_idx = 0 + slack_idx = n_orig_cols # Index for next slack variable + + # 1. Original equality constraints (E rows) + for row_name in eq_rows: + for col_name, coeffs in mps_data["columns"].items(): + if row_name in coeffs: + A_eq[row_idx, col_idx[col_name]] = coeffs[row_name] + b_eq[row_idx] = mps_data["rhs"].get(row_name, 0.0) + row_idx += 1 + + # 2. Less-than-or-equal constraints (L rows): Ax + s = b, s >= 0 + for row_name in le_rows: + for col_name, coeffs in mps_data["columns"].items(): + if row_name in coeffs: + A_eq[row_idx, col_idx[col_name]] = coeffs[row_name] + # Add slack variable + A_eq[row_idx, slack_idx] = 1.0 + b_eq[row_idx] = mps_data["rhs"].get(row_name, 0.0) + row_idx += 1 + slack_idx += 1 + + # 3. Greater-than-or-equal constraints (G rows): Ax - s = b, s >= 0 + for row_name in ge_rows: + for col_name, coeffs in mps_data["columns"].items(): + if row_name in coeffs: + A_eq[row_idx, col_idx[col_name]] = coeffs[row_name] + # Subtract slack variable + A_eq[row_idx, slack_idx] = -1.0 + b_eq[row_idx] = mps_data["rhs"].get(row_name, 0.0) + row_idx += 1 + slack_idx += 1 + + # 4. Upper bound constraints: x + s_up = u, s_up >= 0 + for var_idx, (_lo, up) in enumerate(var_bounds): + if up < np.inf: + A_eq[row_idx, var_idx] = 1.0 + A_eq[row_idx, slack_idx] = 1.0 + b_eq[row_idx] = up + row_idx += 1 + slack_idx += 1 + + A_eq = A_eq.tocsr() + + # Handle lower bounds by shifting variables + # For variables with lo != 0: x_new = x - lo, so A_eq stays same, b_eq adjusts + c_shift = 0.0 + for var_idx, (lo, _up) in enumerate(var_bounds): + if lo != 0 and lo > -np.inf: + # x = x_new + lo, so c'x = c'x_new + c*lo + c_shift += c[var_idx] * lo + # Ax = A(x_new + lo) = Ax_new + A*lo, so b = b - A*lo + b_eq -= A_eq[:, var_idx].toarray().flatten() * lo + + A_eq = A_eq.toarray() + + # Count free variables (variables with lo = -inf) + n_free_vars = 0 + n_nonneg_vars = 0 + for lo, _up in var_bounds: + if lo == -np.inf: + n_free_vars += 1 + else: + n_nonneg_vars += 1 + + # Add slack variables to nonneg count + n_nonneg_vars += n_slack_ineq + n_slack_upper + + # Create cone structure for SeDuMi format + cone_structure = {"free_vars": n_free_vars, "nonneg_vars": n_nonneg_vars, "soc_cones": [], "sdp_cones": []} + + # Create metadata + metadata = { + "source": "MPS file", + "format": "MPS (converted to SeDuMi)", + "original_name": mps_data["name"], + "objective_row": obj_name, + "objective_shift": c_shift, + "constraint_counts": { + "original_equality": n_orig_eq, + "original_less_equal": n_le, + "original_greater_equal": n_ge, + "upper_bound_constraints": n_upper_bounds, + "total": n_total_constraints, + }, + "original_dimensions": {"variables": n_orig_cols, "constraints": n_orig_eq + n_le + n_ge}, + "sedumi_dimensions": {"variables": n_total_vars, "constraints": n_total_constraints}, + } + + logger.info( + f"Converted {problem_name}: LP problem " + f"({n_orig_cols} original vars -> {n_total_vars} total vars, " + f"{n_total_constraints} constraints)" + ) + + # Reshape for consistency with other loaders + c = c.reshape(-1, 1) + b_eq = b_eq.reshape(-1, 1) + + return ProblemData( + name=problem_name, + problem_class="LP", + c=c, + A_eq=A_eq, + b_eq=b_eq, + A_ub=None, + b_ub=None, + bounds=None, # Bounds handled through SeDuMi cone structure + cone_structure=cone_structure, + metadata=metadata, + ) + + +if __name__ == "__main__": + # Test script + import sys + + if len(sys.argv) < 2: + print("Usage: python mps_loader.py ") + sys.exit(1) + + file_path = sys.argv[1] + + try: + loader = MPSLoader() + problem = loader.load(file_path) + print(f"Loaded problem: {problem}") + print(f" Variables: {problem._num_variables}") + print(f" Constraints: {problem._num_constraints}") + print(f" Bounds: {len(problem.bounds) if problem.bounds else 0} variables with bounds") + + except Exception as e: + print(f"Error loading problem: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) diff --git a/scripts/data_loaders/python/problem_interface.py b/scripts/data_loaders/python/problem_interface.py index 6935901..4b3baae 100644 --- a/scripts/data_loaders/python/problem_interface.py +++ b/scripts/data_loaders/python/problem_interface.py @@ -30,6 +30,7 @@ from scripts.data_loaders.problem_loader import ProblemData from scripts.data_loaders.python.dat_loader import DATLoader from scripts.data_loaders.python.mat_loader import MATLoader +from scripts.data_loaders.python.mps_loader import MPSLoader from scripts.utils.logger import get_logger logger = get_logger("problem_interface") @@ -48,8 +49,8 @@ class ProblemInterface: FORMAT_LOADERS = { "mat": MATLoader, "dat-s": DATLoader, + "mps": MPSLoader, # Future loaders can be added here - # "mps": MPSLoader, # "qps": QPSLoader, # "python": PythonLoader, } diff --git a/tests/integration/test_problem_registry.py b/tests/integration/test_problem_registry.py index 8cdcaf2..24faa59 100644 --- a/tests/integration/test_problem_registry.py +++ b/tests/integration/test_problem_registry.py @@ -11,7 +11,7 @@ REGISTRY_PATH = PROJECT_ROOT / "config" / "problem_registry.yaml" REQUIRED_KEYS = {"display_name", "file_path", "file_type", "library_name"} -KNOWN_LIBRARIES = {"DIMACS", "SDPLIB"} +KNOWN_LIBRARIES = {"DIMACS", "SDPLIB", "NETLIB"} @pytest.fixture(scope="module") @@ -74,8 +74,13 @@ def test_has_fast_test_problems(problems): def test_problem_files_exist(problems): - # Skip when submodules (problems/DIMACS, problems/SDPLIB) are not checked out - if not any(any((PROJECT_ROOT / "problems" / lib).iterdir()) for lib in KNOWN_LIBRARIES): + # Skip when submodules are not checked out (all must be present to run this test) + submodule_dirs = { + "DIMACS": PROJECT_ROOT / "problems" / "DIMACS" / "data", + "SDPLIB": PROJECT_ROOT / "problems" / "SDPLIB" / "data", + "NETLIB": PROJECT_ROOT / "problems" / "NETLIB" / "mps_files", + } + if not all(d.exists() and any(d.iterdir()) for d in submodule_dirs.values()): pytest.skip("Problem library submodules not checked out") missing = [name for name, config in problems.items() if not (PROJECT_ROOT / config["file_path"]).exists()] diff --git a/tests/unit/test_problem_interface.py b/tests/unit/test_problem_interface.py index 0bc5d1d..5e0a9c6 100644 --- a/tests/unit/test_problem_interface.py +++ b/tests/unit/test_problem_interface.py @@ -105,4 +105,4 @@ def test_statistics_counts(self, interface): assert stats["libraries"] == {"SDPLIB": 1, "DIMACS": 1, "OTHER": 1} assert stats["test_problems"] == 1 assert stats["file_formats"] == {"dat-s": 1, "mat": 1, "qps": 1} - assert set(stats["supported_formats"]) == {"mat", "dat-s"} + assert set(stats["supported_formats"]) == {"mat", "dat-s", "mps"}