Skip to content

Commit 0452b63

Browse files
committed
Convert qplib to ommx
1 parent e8b77ab commit 0452b63

File tree

4 files changed

+291
-11
lines changed

4 files changed

+291
-11
lines changed

rust/ommx/src/mps/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ fn convert_dvars(mps: &Mps) -> (Vec<v1::DecisionVariable>, HashMap<ColumnName, u
7070
let id = i as u64;
7171
name_id_map.insert(var_name.clone(), id);
7272
dvars.push(v1::DecisionVariable {
73-
id: i as u64,
73+
id,
7474
kind,
7575
bound: Some(bound),
7676
name: Some(var_name.0.clone()),

rust/ommx/src/qplib.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
use std::{io::Read, path::Path};
1+
use prost::Message;
2+
use std::path::Path;
23

4+
mod convert;
35
mod parser;
46

5-
use parser::*;
7+
use parser::QplibFile;
68

79
/// Reads and parses the file at the given path as a gzipped MPS file.
810
pub fn load_file(path: impl AsRef<Path>) -> Result<crate::v1::Instance, QplibParseError> {
9-
let data = Qplib::from_file(path)?;
10-
// TODO
11-
// convert::convert(data)
11+
let data = QplibFile::from_file(path)?;
12+
convert::convert(data)
1213
}
1314

1415
pub fn load_file_bytes(path: impl AsRef<Path>) -> Result<Vec<u8>, QplibParseError> {

rust/ommx/src/qplib/convert.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use itertools::izip;
2+
use num::Zero;
3+
4+
use super::{
5+
parser::{ObjSense, QplibFile, VarType},
6+
QplibParseError,
7+
};
8+
use crate::v1;
9+
use std::collections::HashMap;
10+
11+
pub fn convert(mut qplib: QplibFile) -> Result<v1::Instance, QplibParseError> {
12+
qplib.apply_infinity_threshold();
13+
let description = convert_description(&qplib);
14+
let decision_variables = convert_dvars(&qplib);
15+
let objective = convert_objective(&qplib);
16+
let constraints = convert_constraints(&qplib);
17+
Ok(v1::Instance {
18+
description: Some(description),
19+
decision_variables,
20+
objective: Some(objective),
21+
constraints,
22+
sense: convert_sense(qplib.sense),
23+
..Default::default()
24+
})
25+
}
26+
27+
fn convert_description(qplib: &QplibFile) -> v1::instance::Description {
28+
// currently only gets the name
29+
let name = if qplib.name.is_empty() {
30+
None
31+
} else {
32+
Some(qplib.name.clone())
33+
};
34+
let description = qplib.problem_type.to_string();
35+
v1::instance::Description {
36+
name,
37+
description: Some(description),
38+
..Default::default()
39+
}
40+
}
41+
42+
fn convert_dvars(qplib: &QplibFile) -> Vec<v1::DecisionVariable> {
43+
let QplibFile {
44+
var_types,
45+
lower_bounds,
46+
upper_bounds,
47+
var_names,
48+
..
49+
} = qplib;
50+
let mut dvars = Vec::with_capacity(var_types.len());
51+
for (i, (t, &lower, &upper)) in izip!(var_types, lower_bounds, upper_bounds).enumerate() {
52+
let id = i as u64;
53+
let name = var_names.get(&i).cloned();
54+
let kind = match t {
55+
VarType::Continuous => v1::decision_variable::Kind::Continuous as i32,
56+
VarType::Integer => v1::decision_variable::Kind::Integer as i32,
57+
VarType::Binary => v1::decision_variable::Kind::Binary as i32,
58+
};
59+
let bound = v1::Bound { lower, upper };
60+
dvars.push(v1::DecisionVariable {
61+
id,
62+
kind,
63+
bound: Some(bound),
64+
name,
65+
..Default::default()
66+
})
67+
}
68+
dvars
69+
}
70+
71+
fn convert_objective(qplib: &QplibFile) -> v1::Function {
72+
let quadratic = to_quadratic(&qplib.q0_non_zeroes);
73+
let linear = if qplib.default_b0 == 0.0 {
74+
to_linear(&qplib.b0_non_defaults)
75+
} else {
76+
// non-zero default value: transform into dense vec using the default value and update
77+
// with non-defaults
78+
let mut terms: Vec<_> = (0..qplib.num_vars as u64)
79+
.map(|i| v1::linear::Term {
80+
id: i,
81+
coefficient: qplib.default_b0,
82+
})
83+
.collect();
84+
for (&i, &coeff) in qplib.b0_non_defaults.iter() {
85+
terms[i].coefficient = coeff;
86+
}
87+
// remove any terms which may have been explicitly set to 0.0
88+
terms.retain(|t| t.coefficient != 0.0);
89+
v1::Linear {
90+
terms,
91+
constant: 0.0, // constant will be added later
92+
}
93+
};
94+
// simplify function to linear/constant if appropriate terms not
95+
// present
96+
wrap_function(quadratic, linear, qplib.obj_constant)
97+
}
98+
99+
fn convert_constraints(qplib: &QplibFile) -> Vec<v1::Constraint> {
100+
let QplibFile {
101+
num_constraints,
102+
qs_non_zeroes,
103+
bs_non_zeroes,
104+
constr_lower_cs,
105+
constr_upper_cs,
106+
constr_names,
107+
..
108+
} = qplib;
109+
// technically num_constraints is only a lower bound on the capacity
110+
// required as one Qplib constraint might equal 2 ommx constraints.
111+
let mut constraints = Vec::with_capacity(*num_constraints);
112+
for (i, (qs, bs, &lower_c, &upper_c)) in izip!(
113+
qs_non_zeroes,
114+
bs_non_zeroes,
115+
constr_lower_cs,
116+
constr_upper_cs
117+
)
118+
.enumerate()
119+
{
120+
let mut quadratic = to_quadratic(qs);
121+
let mut linear = to_linear(bs);
122+
let name = constr_names
123+
.get(&i)
124+
.cloned()
125+
.unwrap_or_else(|| format!("Qplib_constr_{i}"));
126+
// QPLIB constraints are two-sided, as in, `c_l <= expr <= c_u`.
127+
// To represent a one-sided constraint, c_l or c_u are set to the infinity
128+
// threshold. This means a single constraint translates to potentially 2
129+
// OMMX constraints.
130+
//
131+
// Currently we don't perform any checks for equality constraints
132+
// (represented in QPLIB by setting `c_l` and `c_u` to the same value)
133+
//
134+
// We don't create a `v1::Constraint` for sides where `c` is infinity.
135+
//
136+
// ID translation scheme (subject to potential change):
137+
//
138+
// - The `<= c_u` side is given the same ID as the constraint in the
139+
// QPLIB file.
140+
//
141+
// - The `>= c_l` side is given a new ID, which is `num_constraints +
142+
// id`. Hence in a QPLIB file with 10 constraints, the `c_l` side of
143+
// constraint 0 is 10; for constraint 1 it's 11, and so on.
144+
//
145+
// This scheme means the resulting OMMX instance will have
146+
// non-contiguous constraint IDs if some constraints have no valid `<=
147+
// c_u` side.
148+
if upper_c != f64::INFINITY {
149+
// move upper_c to the LHS multiplied by -1.
150+
let func = wrap_function(quadratic.clone(), linear.clone(), -upper_c);
151+
constraints.push(v1::Constraint {
152+
id: i as u64,
153+
equality: v1::Equality::LessThanOrEqualToZero as i32,
154+
function: Some(func),
155+
name: Some(format!("{name} [c_u]")),
156+
..Default::default()
157+
});
158+
}
159+
160+
if lower_c != f64::NEG_INFINITY {
161+
// multiply ALL coefficients by -1, move constant to LHS
162+
quadratic.values.iter_mut().for_each(|v| *v *= -1.);
163+
linear.terms.iter_mut().for_each(|t| t.coefficient *= -1.);
164+
let func = wrap_function(quadratic, linear, lower_c);
165+
constraints.push(v1::Constraint {
166+
id: (num_constraints + i) as u64,
167+
equality: v1::Equality::LessThanOrEqualToZero as i32,
168+
function: Some(func),
169+
name: Some(format!("{name} [c_l]")),
170+
..Default::default()
171+
});
172+
}
173+
}
174+
constraints
175+
}
176+
177+
fn to_quadratic(coeff_map: &HashMap<(usize, usize), f64>) -> v1::Quadratic {
178+
let mut rows = Vec::with_capacity(coeff_map.len());
179+
let mut columns = Vec::with_capacity(coeff_map.len());
180+
let mut values = Vec::with_capacity(coeff_map.len());
181+
for ((row, col), val) in coeff_map.iter() {
182+
rows.push(*row as u64);
183+
columns.push(*col as u64);
184+
values.push(*val);
185+
}
186+
v1::Quadratic {
187+
rows,
188+
columns,
189+
values,
190+
linear: None,
191+
}
192+
}
193+
194+
fn to_linear(coeffs: &HashMap<usize, f64>) -> v1::Linear {
195+
let terms: Vec<_> = coeffs
196+
.iter()
197+
.map(|(id, coeff)| v1::linear::Term {
198+
id: *id as u64,
199+
coefficient: *coeff,
200+
})
201+
.collect();
202+
v1::Linear {
203+
terms,
204+
constant: 0.0,
205+
}
206+
}
207+
208+
fn wrap_function(mut quad: v1::Quadratic, mut linear: v1::Linear, constant: f64) -> v1::Function {
209+
let func = if quad.is_zero() {
210+
if linear.terms.is_empty() {
211+
v1::function::Function::Constant(constant)
212+
} else {
213+
linear.constant = constant;
214+
v1::function::Function::Linear(linear)
215+
}
216+
} else {
217+
linear.constant = constant;
218+
quad.linear = Some(linear);
219+
v1::function::Function::Quadratic(quad)
220+
};
221+
v1::Function {
222+
function: Some(func),
223+
}
224+
}
225+
226+
fn convert_sense(sense: ObjSense) -> i32 {
227+
match sense {
228+
ObjSense::Minimize => v1::instance::Sense::Minimize as i32,
229+
ObjSense::Maximize => v1::instance::Sense::Maximize as i32,
230+
}
231+
}

rust/ommx/src/qplib/parser.rs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use crate::error::QplibParseError;
1+
use super::QplibParseError;
22
use std::collections::HashMap;
33
use std::{
4+
fmt::Display,
45
fs,
56
io::{self, BufRead, Read},
67
path::Path,
@@ -205,6 +206,12 @@ impl QplibFile {
205206
#[derive(Default, Debug)]
206207
pub struct ProblemType(ProbObjKind, ProbVarKind, ProbConstrKind);
207208

209+
impl Display for ProblemType {
210+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211+
write!(f, "{}{}{}", self.0, self.1, self.2)
212+
}
213+
}
214+
208215
#[derive(Clone, Copy, Default, Debug, PartialEq)]
209216
pub enum ProbObjKind {
210217
Linear,
@@ -214,6 +221,18 @@ pub enum ProbObjKind {
214221
Quadratic, // generic case
215222
}
216223

224+
impl Display for ProbObjKind {
225+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226+
let c = match self {
227+
ProbObjKind::Linear => 'L',
228+
ProbObjKind::DiagonalC => 'D',
229+
ProbObjKind::ConcaveOrConvex => 'C',
230+
ProbObjKind::Quadratic => 'Q',
231+
};
232+
write!(f, "{c}")
233+
}
234+
}
235+
217236
#[derive(Clone, Copy, Default, Debug, PartialEq)]
218237
pub enum ProbVarKind {
219238
Continuous,
@@ -224,6 +243,19 @@ pub enum ProbVarKind {
224243
General,
225244
}
226245

246+
impl Display for ProbVarKind {
247+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248+
let c = match self {
249+
ProbVarKind::Continuous => 'C',
250+
ProbVarKind::Binary => 'B',
251+
ProbVarKind::Mixed => 'M',
252+
ProbVarKind::Integer => 'I',
253+
ProbVarKind::General => 'G',
254+
};
255+
write!(f, "{c}")
256+
}
257+
}
258+
227259
#[derive(Clone, Copy, Default, Debug, PartialEq)]
228260
pub enum ProbConstrKind {
229261
None,
@@ -235,6 +267,20 @@ pub enum ProbConstrKind {
235267
Quadratic,
236268
}
237269

270+
impl Display for ProbConstrKind {
271+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272+
let c = match self {
273+
ProbConstrKind::None => 'N',
274+
ProbConstrKind::Box => 'B',
275+
ProbConstrKind::Linear => 'L',
276+
ProbConstrKind::DiagonalConvex => 'D',
277+
ProbConstrKind::Convex => 'C',
278+
ProbConstrKind::Quadratic => 'Q',
279+
};
280+
write!(f, "{c}")
281+
}
282+
}
283+
238284
impl FromStr for ProblemType {
239285
type Err = QplibParseError;
240286

@@ -346,11 +392,13 @@ fn integer_to_binary(
346392
/// the form `c_l <= expr <= c_u`. When a problem has to define a constraint
347393
/// that is only `expr <= c_u`, for example, they set `c_l` to infinity.
348394
///
349-
/// Ommx, however, only allows constraints to be `<= 0` or `>= 0`. The first
350-
/// step in handlign this is splitting the constraint into into `expr <= c_u`
351-
/// and `expr >= c_l`. Mathematically, we don't need special handling for this
352-
/// and can just split _all_ constraints into both kinds. But we choose to check
395+
/// Ommx, however, only allows constraints to be `<= 0` or `= 0`. The first step
396+
/// in handlign this is splitting the constraint into into `expr <= c_u` and
397+
/// `expr >= c_l`. Mathematically, we don't need special handling for this and
398+
/// can just split _all_ constraints into both kinds. But we choose to check
353399
/// this so the number of trivial constraints passed on is greatly reduced.
400+
/// Later handling will be responsible for transforming constraints into a `<=
401+
/// 0` form.
354402
///
355403
/// The intent here to reduce the size of the instance by not adding the full
356404
/// `c_l` and `c_u` lists, removing all infinties. For problems with lots of

0 commit comments

Comments
 (0)