diff --git a/src/lp_parts.rs b/src/lp_parts.rs index 72608fc..0ab055b 100644 --- a/src/lp_parts.rs +++ b/src/lp_parts.rs @@ -4,7 +4,7 @@ use pest::iterators::Pair; use crate::{ common::RuleExt, - model::{Constraint, LPDefinition, Objective, SOSClass, Sense, VariableType}, + model::{constraint::Constraint, lp_problem::LPProblem, objective::Objective, sense::Sense, sos::SOSClass, variable::VariableType}, Rule, }; @@ -84,7 +84,7 @@ fn get_bound(pair: Pair<'_, Rule>) -> Option<(&str, VariableType)> { #[allow(clippy::wildcard_enum_match_arm)] /// # Errors /// Returns an error if the `compose` fails -pub fn compose(pair: Pair<'_, Rule>, mut parsed: LPDefinition) -> anyhow::Result { +pub fn compose(pair: Pair<'_, Rule>, mut parsed: LPProblem) -> anyhow::Result { match pair.as_rule() { // Problem Name Rule::PROBLEM_NAME => return Ok(parsed.with_problem_name(pair.as_str())), diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index 2839e83..0000000 --- a/src/model.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::{ - collections::{hash_map::Entry, HashMap}, - str::FromStr, -}; - -use pest::iterators::Pairs; - -use crate::{ - common::{AsFloat, RuleExt}, - Rule, -}; - -#[derive(Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// A enum representing the bounds of a variable -pub enum VariableType { - /// Unbounded variable (-Infinity, +Infinity) - Free, - // Lower bounded variable - LB(f64), - // Upper bounded variable - UB(f64), - // Bounded variable - Bounded(f64, f64, bool), - // Integer variable [0, 1] - Integer, - // Binary variable - Binary, - #[default] - // General variable [0, +Infinity] - General, - // Semi-continuous - SemiContinuous, -} - -impl From for VariableType { - #[allow(clippy::wildcard_enum_match_arm, clippy::unreachable)] - fn from(value: Rule) -> Self { - match value { - Rule::INTEGERS => Self::Integer, - Rule::GENERALS => Self::General, - Rule::BINARIES => Self::Binary, - Rule::SEMI_CONTINUOUS => Self::SemiContinuous, - _ => unreachable!(), - } - } -} - -impl VariableType { - #[allow(clippy::wildcard_enum_match_arm)] - pub fn set_semi_continuous(&mut self) { - if let Self::Bounded(lb, ub, _) = self { - *self = Self::Bounded(*lb, *ub, true); - } - } -} - -#[derive(Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Objective { - pub name: String, - pub coefficients: Vec, -} - -#[derive(Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Coefficient { - pub var_name: String, - pub coefficient: f64, -} - -impl TryFrom> for Coefficient { - type Error = anyhow::Error; - - #[allow(clippy::unreachable, clippy::wildcard_enum_match_arm)] - fn try_from(values: Pairs<'_, Rule>) -> anyhow::Result { - let (mut value, mut var_name) = (1.0, String::new()); - for item in values { - match item.as_rule() { - r if r.is_numeric() => { - value *= item.as_float()?; - } - Rule::VARIABLE => { - var_name = item.as_str().to_string(); - } - _ => unreachable!("Unexpected rule encountered"), - } - } - Ok(Self { var_name, coefficient: value }) - } -} - -#[derive(Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum SOSClass { - S1, - S2, -} - -impl FromStr for SOSClass { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "s1" | "s1::" => Ok(Self::S1), - "s2" | "s2::" => Ok(Self::S2), - _ => Err(anyhow::anyhow!("Invalid SOS class: {}", s)), - } - } -} - -#[derive(Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Constraint { - Standard { name: String, coefficients: Vec, sense: String, rhs: f64 }, - SOS { name: String, kind: SOSClass, coefficients: Vec }, -} - -impl Constraint { - fn name(&self) -> String { - match self { - Self::Standard { name, .. } | Self::SOS { name, .. } => name.to_string(), - } - } - - fn coefficients(&self) -> &[Coefficient] { - match self { - Self::Standard { coefficients, .. } | Self::SOS { coefficients, .. } => coefficients, - } - } -} - -#[derive(Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Sense { - #[default] - Minimize, - Maximize, -} - -#[derive(Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct LPDefinition { - pub problem_name: String, - pub problem_sense: Sense, - pub variables: HashMap, - pub objectives: Vec, - pub constraints: HashMap, -} - -impl LPDefinition { - #[must_use] - pub fn with_problem_name(self, problem_name: &str) -> Self { - Self { problem_name: problem_name.to_string(), ..self } - } - - #[must_use] - pub fn with_sense(self, problem_sense: Sense) -> Self { - Self { problem_sense, ..self } - } - - pub fn add_variable(&mut self, name: &str) { - if !name.is_empty() { - self.variables.entry(name.to_string()).or_default(); - } - } - - pub fn set_var_bounds(&mut self, name: &str, kind: VariableType) { - if !name.is_empty() { - match self.variables.entry(name.to_string()) { - Entry::Occupied(k) if matches!(kind, VariableType::SemiContinuous) => { - k.into_mut().set_semi_continuous(); - } - Entry::Occupied(k) => *k.into_mut() = kind, - Entry::Vacant(k) => { - k.insert(kind); - } - } - } - } - - pub fn add_objective(&mut self, objectives: Vec) { - for ob in &objectives { - ob.coefficients.iter().for_each(|c| { - self.add_variable(&c.var_name); - }); - } - self.objectives = objectives; - } - - pub fn add_constraints(&mut self, constraints: Vec) { - for con in constraints { - let name = if con.name().is_empty() { format!("UnnamedConstraint:{}", self.constraints.len()) } else { con.name() }; - con.coefficients().iter().for_each(|c| { - self.add_variable(&c.var_name); - }); - self.constraints.entry(name).or_insert(con); - } - } -} diff --git a/src/model/coefficient.rs b/src/model/coefficient.rs new file mode 100644 index 0000000..5d3ec0c --- /dev/null +++ b/src/model/coefficient.rs @@ -0,0 +1,34 @@ +use pest::iterators::Pairs; + +use crate::{ + common::{AsFloat, RuleExt}, + Rule, +}; + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Coefficient { + pub var_name: String, + pub coefficient: f64, +} + +impl TryFrom> for Coefficient { + type Error = anyhow::Error; + + #[allow(clippy::unreachable, clippy::wildcard_enum_match_arm)] + fn try_from(values: Pairs<'_, Rule>) -> anyhow::Result { + let (mut value, mut var_name) = (1.0, String::new()); + for item in values { + match item.as_rule() { + r if r.is_numeric() => { + value *= item.as_float()?; + } + Rule::VARIABLE => { + var_name = item.as_str().to_string(); + } + _ => unreachable!("Unexpected rule encountered"), + } + } + Ok(Self { var_name, coefficient: value }) + } +} diff --git a/src/model/constraint.rs b/src/model/constraint.rs new file mode 100644 index 0000000..a251d77 --- /dev/null +++ b/src/model/constraint.rs @@ -0,0 +1,24 @@ +use super::{coefficient::Coefficient, sos::SOSClass}; + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Constraint { + Standard { name: String, coefficients: Vec, sense: String, rhs: f64 }, + SOS { name: String, kind: SOSClass, coefficients: Vec }, +} + +impl Constraint { + #[must_use] + pub fn name(&self) -> String { + match self { + Self::Standard { name, .. } | Self::SOS { name, .. } => name.to_string(), + } + } + + #[must_use] + pub fn coefficients(&self) -> &[Coefficient] { + match self { + Self::Standard { coefficients, .. } | Self::SOS { coefficients, .. } => coefficients, + } + } +} diff --git a/src/model/lp_problem.rs b/src/model/lp_problem.rs new file mode 100644 index 0000000..270fedf --- /dev/null +++ b/src/model/lp_problem.rs @@ -0,0 +1,64 @@ +use std::collections::{hash_map::Entry, HashMap}; + +use crate::model::{constraint::Constraint, objective::Objective, sense::Sense, variable::VariableType}; + +#[derive(Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct LPProblem { + pub problem_name: String, + pub problem_sense: Sense, + pub variables: HashMap, + pub objectives: Vec, + pub constraints: HashMap, +} + +impl LPProblem { + #[must_use] + pub fn with_problem_name(self, problem_name: &str) -> Self { + Self { problem_name: problem_name.to_string(), ..self } + } + + #[must_use] + pub fn with_sense(self, problem_sense: Sense) -> Self { + Self { problem_sense, ..self } + } + + pub fn add_variable(&mut self, name: &str) { + if !name.is_empty() { + self.variables.entry(name.to_string()).or_default(); + } + } + + pub fn set_var_bounds(&mut self, name: &str, kind: VariableType) { + if !name.is_empty() { + match self.variables.entry(name.to_string()) { + Entry::Occupied(k) if matches!(kind, VariableType::SemiContinuous) => { + k.into_mut().set_semi_continuous(); + } + Entry::Occupied(k) => *k.into_mut() = kind, + Entry::Vacant(k) => { + k.insert(kind); + } + } + } + } + + pub fn add_objective(&mut self, objectives: Vec) { + for ob in &objectives { + ob.coefficients.iter().for_each(|c| { + self.add_variable(&c.var_name); + }); + } + self.objectives = objectives; + } + + pub fn add_constraints(&mut self, constraints: Vec) { + for con in constraints { + let name = if con.name().is_empty() { format!("UnnamedConstraint:{}", self.constraints.len()) } else { con.name() }; + con.coefficients().iter().for_each(|c| { + self.add_variable(&c.var_name); + }); + self.constraints.entry(name).or_insert(con); + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..57c53a0 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,7 @@ +pub mod coefficient; +pub mod constraint; +pub mod lp_problem; +pub mod objective; +pub mod sense; +pub mod sos; +pub mod variable; diff --git a/src/model/objective.rs b/src/model/objective.rs new file mode 100644 index 0000000..7c17e72 --- /dev/null +++ b/src/model/objective.rs @@ -0,0 +1,8 @@ +use crate::model::coefficient::Coefficient; + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Objective { + pub name: String, + pub coefficients: Vec, +} diff --git a/src/model/sense.rs b/src/model/sense.rs new file mode 100644 index 0000000..b523031 --- /dev/null +++ b/src/model/sense.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Sense { + #[default] + Minimize, + Maximize, +} diff --git a/src/model/sos.rs b/src/model/sos.rs new file mode 100644 index 0000000..b6427a5 --- /dev/null +++ b/src/model/sos.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum SOSClass { + S1, + S2, +} + +impl FromStr for SOSClass { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "s1" | "s1::" => Ok(Self::S1), + "s2" | "s2::" => Ok(Self::S2), + _ => Err(anyhow::anyhow!("Invalid SOS class: {}", s)), + } + } +} diff --git a/src/model/variable.rs b/src/model/variable.rs new file mode 100644 index 0000000..2690646 --- /dev/null +++ b/src/model/variable.rs @@ -0,0 +1,46 @@ +use crate::Rule; + +#[derive(Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// A enum representing the bounds of a variable +pub enum VariableType { + /// Unbounded variable (-Infinity, +Infinity) + Free, + // Lower bounded variable + LB(f64), + // Upper bounded variable + UB(f64), + // Bounded variable + Bounded(f64, f64, bool), + // Integer variable [0, 1] + Integer, + // Binary variable + Binary, + #[default] + // General variable [0, +Infinity] + General, + // Semi-continuous + SemiContinuous, +} + +impl From for VariableType { + #[allow(clippy::wildcard_enum_match_arm, clippy::unreachable)] + fn from(value: Rule) -> Self { + match value { + Rule::INTEGERS => Self::Integer, + Rule::GENERALS => Self::General, + Rule::BINARIES => Self::Binary, + Rule::SEMI_CONTINUOUS => Self::SemiContinuous, + _ => unreachable!(), + } + } +} + +impl VariableType { + #[allow(clippy::wildcard_enum_match_arm)] + pub fn set_semi_continuous(&mut self) { + if let Self::Bounded(lb, ub, _) = self { + *self = Self::Bounded(*lb, *ub, true); + } + } +} diff --git a/src/parse.rs b/src/parse.rs index 3b58d00..d2d88d1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,7 +4,7 @@ use std::{ path::Path, }; -use crate::{lp_parts::compose, model::LPDefinition, LParser, Rule}; +use crate::{lp_parts::compose, model::lp_problem::LPProblem, LParser, Rule}; use pest::Parser; /// # Errors @@ -22,12 +22,12 @@ pub fn parse_file(path: &Path) -> anyhow::Result { /// # Errors /// Returns an error if the parse fails -pub fn parse_lp_file(contents: &str) -> anyhow::Result { +pub fn parse_lp_file(contents: &str) -> anyhow::Result { let mut parsed = LParser::parse(Rule::LP_FILE, contents)?; let Some(pair) = parsed.next() else { anyhow::bail!("Invalid LP file"); }; - let mut parsed_contents = LPDefinition::default(); + let mut parsed_contents = LPProblem::default(); for pair in pair.clone().into_inner() { parsed_contents = compose(pair, parsed_contents)?; } diff --git a/tests/test_from_file.rs b/tests/test_from_file.rs index f9647b0..b499b4e 100644 --- a/tests/test_from_file.rs +++ b/tests/test_from_file.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use congenial_enigma::{ - model::{LPDefinition, Sense}, + model::{lp_problem::LPProblem, sense::Sense}, parse::{parse_file, parse_lp_file}, }; @@ -65,7 +65,7 @@ fn invalid() { assert!(result.is_err()); } -fn read_file_from_resources(file_name: &str) -> anyhow::Result { +fn read_file_from_resources(file_name: &str) -> anyhow::Result { let mut file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); file_path.push(format!("resources/{file_name}")); let contents = parse_file(&file_path)?;