Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QPLIB format parser #250

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/ommx/ommx/_ommx_rust.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def evaluate_linear(function: bytes, state: bytes) -> tuple[float, set[int]]: ..
def evaluate_polynomial(function: bytes, state: bytes) -> tuple[float, set[int]]: ...
def evaluate_quadratic(function: bytes, state: bytes) -> tuple[float, set[int]]: ...
def load_mps_bytes(path: str) -> bytes: ...
def load_qplib_bytes(path: str) -> bytes: ...
def miplib2017_instance_annotations() -> dict[str, dict[str, str]]: ...
def partial_evaluate_constraint(obj: bytes, state: bytes) -> tuple[bytes, set[int]]: ...
def partial_evaluate_function(
Expand Down
5 changes: 5 additions & 0 deletions python/ommx/ommx/qplib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .v1 import Instance


def load_file(path: str) -> Instance:
return Instance.load_qplib(path)
5 changes: 5 additions & 0 deletions python/ommx/ommx/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ def write_mps(self, path: str):
"""
_ommx_rust.write_mps_file(self.to_bytes(), path)

@staticmethod
def load_qplib(path: str) -> Instance:
bytes = _ommx_rust.load_qplib_bytes(path)
return Instance.from_bytes(bytes)

def add_user_annotation(
self, key: str, value: str, *, annotation_namespace: str = "org.ommx.user."
):
Expand Down
5 changes: 5 additions & 0 deletions python/ommx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod evaluate;
mod instance;
mod message;
mod mps;
mod qplib;

pub use artifact::*;
pub use builder::*;
Expand All @@ -15,6 +16,7 @@ pub use evaluate::*;
pub use instance::*;
pub use message::*;
pub use mps::*;
pub use qplib::*;

use pyo3::prelude::*;

Expand Down Expand Up @@ -59,6 +61,9 @@ fn _ommx_rust(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(load_mps_bytes, m)?)?;
m.add_function(wrap_pyfunction!(write_mps_file, m)?)?;

// Qplib
m.add_function(wrap_pyfunction!(load_qplib_bytes, m)?)?;

// Dataset
m.add_function(wrap_pyfunction!(miplib2017_instance_annotations, m)?)?;
Ok(())
Expand Down
15 changes: 15 additions & 0 deletions python/ommx/src/qplib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use anyhow::Result;
use pyo3::{
prelude::*,
types::{PyBytes, PyString},
};

#[cfg_attr(feature = "stub_gen", pyo3_stub_gen::derive::gen_stub_pyfunction)]
#[pyfunction(name = "load_qplib_bytes")]
pub fn load_qplib_bytes<'py>(
py: Python<'py>,
path: Bound<PyString>,
) -> Result<Bound<'py, PyBytes>> {
let instance = ommx::qplib::load_file_bytes(path.to_str()?)?;
Ok(PyBytes::new_bound(py, &instance))
}
44 changes: 44 additions & 0 deletions python/ommx/tests/example.qplib
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
! ---------------
! example problem
! ---------------
MIPBAND # problem name
QML # problem is a mixed-integer quadratic program
Minimize # minimize the objective function
3 # variables
2 # general linear constraints
5 # nonzeros in lower triangle of Q^0
1 1 2.0 5 lines row & column index & value of nonzero in lower triangle Q^0
2 1 -1.0 |
2 2 2.0 |
3 2 -1.0 |
3 3 2.0 |
-0.2 default value for entries in b_0
1 # non default entries in b_0
2 -0.4 1 line of index & value of non-default values in b_0
0.0 value of q^0
4 # nonzeros in vectors b^i (i=1,...,m)
1 1 1.0 4 lines constraint, index & value of nonzero in b^i (i=1,...,m)
1 2 1.0 |
2 1 1.0 |
2 3 1.0 |
1.0E+20 infinity
1.0 default value for entries in c_l
0 # non default entries in c_l
1.0E+20 default value for entries in c_u
0 # non default entries in c_u
0.0 default value for entries in l
0 # non default entries in l
1.0 default value for entries in u
1 # non default entries in u
2 2.0 1 line of non-default indices and values in u
0 default variable type is continuous
1 # non default variable types
3 2 variable 3 is binary
1.0 default value for initial values for x
0 # non default entries in x
0.0 default value for initial values for y
0 # non default entries in y
0.0 default value for initial values for z
0 # non default entries in z
0 # non default names for variables
0 # non default names for constraints
1 change: 1 addition & 0 deletions rust/ommx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub use prost::Message;
pub mod artifact;
pub mod dataset;
pub mod mps;
pub mod qplib;
pub mod random;

// Internal modules
Expand Down
2 changes: 1 addition & 1 deletion rust/ommx/src/mps/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ fn convert_dvars(mps: &Mps) -> (Vec<v1::DecisionVariable>, HashMap<ColumnName, u
let id = i as u64;
name_id_map.insert(var_name.clone(), id);
dvars.push(v1::DecisionVariable {
id: i as u64,
id,
kind,
bound: Some(bound),
name: Some(var_name.0.clone()),
Expand Down
81 changes: 81 additions & 0 deletions rust/ommx/src/qplib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use prost::Message;
use std::path::Path;

mod convert;
mod parser;

pub use parser::QplibFile;

/// Reads and parses the file at the given path as a gzipped MPS file.
pub fn load_file(path: impl AsRef<Path>) -> anyhow::Result<crate::v1::Instance> {
let data = QplibFile::from_file(path)?;
let converted = convert::convert(data)?;
Ok(converted)
}

pub fn load_file_bytes(path: impl AsRef<Path>) -> anyhow::Result<Vec<u8>> {
let instance = load_file(path)?;
Ok(instance.encode_to_vec())
}

#[derive(Debug, thiserror::Error)]
#[error("{reason} (at line {line_num})")]
pub struct QplibParseError {
line_num: usize,
reason: ParseErrorReason,
}

impl QplibParseError {
// generic "invalid line" error
fn invalid_line(line_num: usize) -> Self {
Self {
line_num,
reason: ParseErrorReason::InvalidLine(line_num),
}
}

fn unexpected_eof(line_num: usize) -> Self {
Self {
line_num,
reason: ParseErrorReason::UnexpectedEndOfFile(line_num),
}
}
}

#[derive(Debug, thiserror::Error)]
pub enum ParseErrorReason {
#[error("Invalid problem type: {0}")]
InvalidProblemType(String),
#[error("Invalid OBJSENSE: {0}")]
InvalidObjSense(String),
#[error("Invalid variable type: {0}")]
InvalidVarType(String),
#[error("Unexpected end of file at line {0}")]
UnexpectedEndOfFile(usize),
#[error("Line {0} did not match expected formatting")]
InvalidLine(usize),
#[error(transparent)]
ParseInt(#[from] std::num::ParseIntError),
#[error(transparent)]
ParseFloat(#[from] std::num::ParseFloatError),
}

impl ParseErrorReason {
// This is a method to make it easier to add the line number at which an
// error occurred in the qplib parser.
pub(crate) fn with_line(self, line_num: usize) -> QplibParseError {
QplibParseError {
line_num,
reason: self,
}
}
}

// Workaround to the fact that `String`'s `FromStr` impl has error
// type `Infallible`. As the conversion can't fail, by definition,
// this will never be called and no panic will ever happen
impl From<std::convert::Infallible> for ParseErrorReason {
fn from(_: std::convert::Infallible) -> Self {
unreachable!()
}
}
Loading
Loading