mod compressed;
mod convert;
mod format;
mod parser;
#[cfg(test)]
mod tests;
pub use compressed::is_gzipped;
pub use format::{format, to_string};
use crate::{Equality, InstanceClass, InstanceClassClause, Kind, PolynomialRequirement, Sense};
use parser::*;
use std::{collections::BTreeSet, io::Read, path::Path, sync::LazyLock};
fn preflight(instance: &crate::Instance) -> crate::Result<()> {
static INPUT_CLASS: LazyLock<InstanceClass> = LazyLock::new(|| {
let quadratic = PolynomialRequirement::at_most(2);
InstanceClassClause::new(
"MPS",
BTreeSet::from([Kind::Binary, Kind::Integer, Kind::Continuous]),
quadratic,
BTreeSet::from([Sense::Minimize, Sense::Maximize]),
)
.with_regular_constraint(Equality::EqualToZero, quadratic)
.with_regular_constraint(Equality::LessThanOrEqualToZero, quadratic)
.into()
});
let report = INPUT_CLASS.check_membership(instance);
crate::ensure!(
report.is_member(),
{ report = %report },
"Instance is outside the MPS input class:\n{report}",
);
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn parse(reader: impl Read) -> crate::Result<crate::Instance> {
let mps_data = Mps::parse(reader)?;
convert::convert(mps_data)
}
#[tracing::instrument(skip_all)]
pub fn load(path: impl AsRef<Path>) -> crate::Result<crate::Instance> {
let mps_data = Mps::load(path)?;
convert::convert(mps_data)
}
#[tracing::instrument(skip_all, fields(compress))]
pub fn save(
instance: &crate::Instance,
out_path: impl AsRef<Path>,
compress: bool,
) -> crate::Result<()> {
preflight(instance)?;
let path = std::path::absolute(out_path.as_ref())?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::File::options()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
if compress {
let mut writer = flate2::write::GzEncoder::new(file, flate2::Compression::new(5));
format::format(instance, &mut writer)?;
} else {
format::format(instance, &mut file)?;
}
Ok(())
}
#[cfg(test)]
mod save_tests {
use super::*;
use crate::{linear, DecisionVariable, Function, Instance, Sense, VariableID};
use std::collections::BTreeMap;
fn unsupported_instance() -> Instance {
let id = VariableID::from(1);
Instance::new(
Sense::Minimize,
Function::from(linear!(id)).abs(),
BTreeMap::from([(id, DecisionVariable::continuous())]),
BTreeMap::new(),
)
.unwrap()
}
#[test]
fn unsupported_model_does_not_truncate_existing_destination() {
let instance = unsupported_instance();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("existing.mps");
std::fs::write(&path, b"existing content").unwrap();
let error = save(&instance, &path, false).unwrap_err();
assert!(error
.to_string()
.contains("objective function is not polynomial"));
assert_eq!(std::fs::read(path).unwrap(), b"existing content");
}
#[test]
fn unsupported_model_does_not_create_destination() {
let instance = unsupported_instance();
let directory = tempfile::tempdir().unwrap();
for compress in [false, true] {
let parent = directory.path().join(format!("missing-{compress}"));
let path = parent.join("output.mps");
save(&instance, &path, compress).unwrap_err();
assert!(!parent.exists());
assert!(!path.exists());
}
}
}