|
| 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 | +} |
0 commit comments