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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
72 changes: 72 additions & 0 deletions rust/ommx/src/qplib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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>) -> Result<crate::v1::Instance, QplibParseError> {
let data = QplibFile::from_file(path)?;
convert::convert(data)
}

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

#[derive(Debug, thiserror::Error)]
pub enum QplibParseError {
#[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),
#[error(transparent)]
Io(#[from] std::io::Error),
Copy link
Member

Choose a reason for hiding this comment

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

IO error is not a parse error. Since std::io::Error does not contains which file is tried to opened, returning this error is not user-friendly. We should rather return anyhow::Result from function which may returns IO error with anyhow::Context to tell which file is opened.

For the actual parse errors like invalid var types, creating this ParseError and returned within anyhow::Error. Users can get the error as ParseError via downcast https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.downcast


// a little hack to allow us to add the line numbers
// as context to errors generated in `FromStr` impls
#[error("{inner} (at line {line_num})")]
WithLine {
line_num: usize,
inner: Box<QplibParseError>,
},
Copy link
Member

Choose a reason for hiding this comment

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

It would be better to convert WithLine as ParseError type, and other entries as separated enum, e.g. ParseErrorReason.

}

impl QplibParseError {
// 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 {
use QplibParseError::*;
match self {
e @ UnexpectedEndOfFile(_) => e,
e @ InvalidLine(_) => e,
WithLine { inner, .. } => WithLine { line_num, inner },
e => WithLine {
line_num,
inner: Box::new(e),
},
}
}
}

// 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 QplibParseError {
fn from(_: std::convert::Infallible) -> Self {
unreachable!()
}
}
Loading
Loading